New UI state system (#71)
* Convert MediaFragment to use new UiState * Convert PersonDetailFragment to use new UiState * Load PersonDetail data on start * Convert FavoriteFragment to use new UiState * Convert SeasonFragment to use new UiState * Convert SearchResultFragment to use new UiState * Convert EpisodeBottomSheetFragment to use new UiState (WIP) * Convert EpisodeBottomSheetFragment to use new UiState (Part 2) * Convert LibraryFragment to use new UiState * Convert DownloadFragment to use new UiState * Convert HomeFragment to use new UiState * Convert MediaInfoFragment to use new UiState (WIP) * Convert MediaInfoViewModel to use new UiState (Part 2) * Convert ServerSelectViewModel to use new UiState (Semi) * Fix MediaInfoFragment for downloaded movies
This commit is contained in:
parent
00dbe8198e
commit
c645ee3b81
40 changed files with 2396 additions and 2271 deletions
|
@ -6,11 +6,7 @@ import androidx.databinding.BindingAdapter
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||
import dev.jdtech.jellyfin.adapters.CollectionListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.DownloadsListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.EpisodeItem
|
||||
import dev.jdtech.jellyfin.adapters.EpisodeListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.FavoritesListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.HomeEpisodeListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.HomeItem
|
||||
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
||||
|
@ -20,7 +16,6 @@ import dev.jdtech.jellyfin.adapters.ViewListAdapter
|
|||
import dev.jdtech.jellyfin.api.JellyfinApi
|
||||
import dev.jdtech.jellyfin.database.Server
|
||||
import dev.jdtech.jellyfin.models.DownloadSection
|
||||
import dev.jdtech.jellyfin.models.FavoriteSection
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
|
@ -32,12 +27,6 @@ fun bindServers(recyclerView: RecyclerView, data: List<Server>?) {
|
|||
adapter.submitList(data)
|
||||
}
|
||||
|
||||
@BindingAdapter("views")
|
||||
fun bindViews(recyclerView: RecyclerView, data: List<HomeItem>?) {
|
||||
val adapter = recyclerView.adapter as ViewListAdapter
|
||||
adapter.submitList(data)
|
||||
}
|
||||
|
||||
@BindingAdapter("items")
|
||||
fun bindItems(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
||||
val adapter = recyclerView.adapter as ViewItemListAdapter
|
||||
|
@ -68,12 +57,6 @@ fun bindItemBackdropById(imageView: ImageView, itemId: UUID) {
|
|||
imageView.loadImage("/items/$itemId/Images/${ImageType.BACKDROP}")
|
||||
}
|
||||
|
||||
@BindingAdapter("collections")
|
||||
fun bindCollections(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
||||
val adapter = recyclerView.adapter as CollectionListAdapter
|
||||
adapter.submitList(data)
|
||||
}
|
||||
|
||||
@BindingAdapter("people")
|
||||
fun bindPeople(recyclerView: RecyclerView, data: List<BaseItemPerson>?) {
|
||||
val adapter = recyclerView.adapter as PersonListAdapter
|
||||
|
@ -87,12 +70,6 @@ fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) {
|
|||
.posterDescription(person.name)
|
||||
}
|
||||
|
||||
@BindingAdapter("episodes")
|
||||
fun bindEpisodes(recyclerView: RecyclerView, data: List<EpisodeItem>?) {
|
||||
val adapter = recyclerView.adapter as EpisodeListAdapter
|
||||
adapter.submitList(data)
|
||||
}
|
||||
|
||||
@BindingAdapter("homeEpisodes")
|
||||
fun bindHomeEpisodes(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
||||
val adapter = recyclerView.adapter as HomeEpisodeListAdapter
|
||||
|
@ -136,18 +113,6 @@ fun bindSeasonPoster(imageView: ImageView, seasonId: UUID) {
|
|||
imageView.loadImage("/items/${seasonId}/Images/${ImageType.PRIMARY}")
|
||||
}
|
||||
|
||||
@BindingAdapter("favoriteSections")
|
||||
fun bindFavoriteSections(recyclerView: RecyclerView, data: List<FavoriteSection>?) {
|
||||
val adapter = recyclerView.adapter as FavoritesListAdapter
|
||||
adapter.submitList(data)
|
||||
}
|
||||
|
||||
@BindingAdapter("downloadSections")
|
||||
fun bindDownloadSections(recyclerView: RecyclerView, data: List<DownloadSection>?) {
|
||||
val adapter = recyclerView.adapter as DownloadsListAdapter
|
||||
adapter.submitList(data)
|
||||
}
|
||||
|
||||
private fun ImageView.loadImage(url: String, errorPlaceHolderId: Int? = null): View {
|
||||
val api = JellyfinApi.getInstance(context.applicationContext)
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.navigation.fragment.NavHostFragment
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.databinding.ActivityMainTvBinding
|
||||
import dev.jdtech.jellyfin.tv.ui.HomeFragmentDirections
|
||||
import dev.jdtech.jellyfin.utils.loadDownloadLocation
|
||||
import dev.jdtech.jellyfin.viewmodels.MainViewModel
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -24,6 +25,8 @@ internal class MainActivityTv : FragmentActivity() {
|
|||
supportFragmentManager.findFragmentById(R.id.tv_nav_host) as NavHostFragment
|
||||
val navController = navHostFragment.navController
|
||||
|
||||
loadDownloadLocation(applicationContext)
|
||||
|
||||
viewModel.navigateToAddServer.observe(this, {
|
||||
if (it) {
|
||||
navController.navigate(HomeFragmentDirections.actionHomeFragmentToAddServerFragment())
|
||||
|
|
|
@ -4,21 +4,23 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.R
|
||||
import dev.jdtech.jellyfin.adapters.DownloadEpisodeListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.DownloadViewItemListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.DownloadsListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.*
|
||||
import dev.jdtech.jellyfin.databinding.FragmentDownloadBinding
|
||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.models.PlayerItem
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.viewmodels.DownloadViewModel
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -27,14 +29,14 @@ class DownloadFragment : Fragment() {
|
|||
private lateinit var binding: FragmentDownloadBinding
|
||||
private val viewModel: DownloadViewModel by viewModels()
|
||||
|
||||
private lateinit var errorDialog: ErrorDialogFragment
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentDownloadBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = viewModel
|
||||
binding.downloadsRecyclerView.adapter = DownloadsListAdapter(
|
||||
DownloadViewItemListAdapter.OnClickListener { item ->
|
||||
navigateToMediaInfoFragment(item)
|
||||
|
@ -42,40 +44,56 @@ class DownloadFragment : Fragment() {
|
|||
navigateToEpisodeBottomSheetFragment(item)
|
||||
})
|
||||
|
||||
viewModel.finishedLoading.observe(viewLifecycleOwner, { isFinished ->
|
||||
binding.loadingIndicator.visibility = if (isFinished) View.GONE else View.VISIBLE
|
||||
})
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.downloadsRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
||||
binding.downloadsRecyclerView.visibility = View.VISIBLE
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is DownloadViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||
is DownloadViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
is DownloadViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||
viewModel.loadData()
|
||||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog")
|
||||
errorDialog.show(parentFragmentManager, "errordialog")
|
||||
}
|
||||
|
||||
viewModel.downloadSections.observe(viewLifecycleOwner, { sections ->
|
||||
if (sections.isEmpty()) {
|
||||
binding.noDownloadsText.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.noDownloadsText.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal(uiState: DownloadViewModel.UiState.Normal) {
|
||||
uiState.apply {
|
||||
binding.noDownloadsText.isVisible = downloadSections.isEmpty()
|
||||
|
||||
val adapter = binding.downloadsRecyclerView.adapter as DownloadsListAdapter
|
||||
adapter.submitList(downloadSections)
|
||||
}
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.downloadsRecyclerView.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.loadingIndicator.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: DownloadViewModel.UiState.Error) {
|
||||
val error = uiState.message ?: resources.getString(R.string.unknown_error)
|
||||
errorDialog = ErrorDialogFragment(error)
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.downloadsRecyclerView.isVisible = false
|
||||
binding.errorLayout.errorPanel.isVisible = true
|
||||
checkIfLoginRequired(error)
|
||||
}
|
||||
|
||||
private fun navigateToMediaInfoFragment(item: PlayerItem) {
|
||||
findNavController().navigate(
|
||||
DownloadFragmentDirections.actionDownloadFragmentToMediaInfoFragment(
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package dev.jdtech.jellyfin.fragments
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.TypedValue
|
||||
import android.view.LayoutInflater
|
||||
|
@ -9,18 +8,22 @@ 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.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.requestDownload
|
||||
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.*
|
||||
|
||||
|
@ -39,13 +42,10 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
): View {
|
||||
binding = EpisodeBottomSheetBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.playButton.setOnClickListener {
|
||||
binding.playButton.setImageResource(android.R.color.transparent)
|
||||
binding.progressCircular.visibility = View.VISIBLE
|
||||
viewModel.item.value?.let {
|
||||
binding.progressCircular.isVisible = true
|
||||
viewModel.item?.let {
|
||||
if (!args.isOffline) {
|
||||
playerViewModel.loadPlayerItems(it)
|
||||
} else {
|
||||
|
@ -54,6 +54,19 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -61,79 +74,46 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
viewModel.item.observe(viewLifecycleOwner, { episode ->
|
||||
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.visibility = View.VISIBLE
|
||||
}
|
||||
binding.communityRating.visibility = when (episode.communityRating != null) {
|
||||
false -> View.GONE
|
||||
true -> View.VISIBLE
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.played.observe(viewLifecycleOwner, {
|
||||
val drawable = when (it) {
|
||||
true -> R.drawable.ic_check_filled
|
||||
false -> R.drawable.ic_check
|
||||
}
|
||||
|
||||
binding.checkButton.setImageResource(drawable)
|
||||
})
|
||||
|
||||
viewModel.favorite.observe(viewLifecycleOwner, {
|
||||
val drawable = when (it) {
|
||||
true -> R.drawable.ic_heart_filled
|
||||
false -> R.drawable.ic_heart
|
||||
}
|
||||
|
||||
binding.favoriteButton.setImageResource(drawable)
|
||||
})
|
||||
|
||||
viewModel.downloaded.observe(viewLifecycleOwner, {
|
||||
val drawable = when (it) {
|
||||
true -> R.drawable.ic_download_filled
|
||||
false -> R.drawable.ic_download
|
||||
}
|
||||
|
||||
binding.downloadButton.setImageResource(drawable)
|
||||
})
|
||||
|
||||
viewModel.downloadEpisode.observe(viewLifecycleOwner, {
|
||||
if (it) {
|
||||
requestDownload(Uri.parse(viewModel.downloadRequestItem.uri), viewModel.downloadRequestItem, this)
|
||||
viewModel.doneDownloadEpisode()
|
||||
}
|
||||
})
|
||||
|
||||
if(!args.isOffline){
|
||||
if(!args.isOffline) {
|
||||
val episodeId: UUID = args.episodeId
|
||||
|
||||
binding.checkButton.setOnClickListener {
|
||||
when (viewModel.played.value) {
|
||||
true -> viewModel.markAsUnplayed(episodeId)
|
||||
false -> viewModel.markAsPlayed(episodeId)
|
||||
when (viewModel.played) {
|
||||
true -> {
|
||||
viewModel.markAsUnplayed(episodeId)
|
||||
binding.checkButton.setImageResource(R.drawable.ic_check)
|
||||
}
|
||||
false -> {
|
||||
viewModel.markAsPlayed(episodeId)
|
||||
binding.checkButton.setImageResource(R.drawable.ic_check_filled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.favoriteButton.setOnClickListener {
|
||||
when (viewModel.favorite.value) {
|
||||
true -> viewModel.unmarkAsFavorite(episodeId)
|
||||
false -> viewModel.markAsFavorite(episodeId)
|
||||
when (viewModel.favorite) {
|
||||
true -> {
|
||||
viewModel.unmarkAsFavorite(episodeId)
|
||||
binding.favoriteButton.setImageResource(R.drawable.ic_heart)
|
||||
}
|
||||
false -> {
|
||||
viewModel.markAsFavorite(episodeId)
|
||||
binding.favoriteButton.setImageResource(R.drawable.ic_heart_filled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.downloadButton.setOnClickListener {
|
||||
binding.downloadButton.isEnabled = false
|
||||
viewModel.loadDownloadRequestItem(episodeId)
|
||||
binding.downloadButton.setImageResource(android.R.color.transparent)
|
||||
binding.progressDownload.isVisible = true
|
||||
}
|
||||
|
||||
binding.deleteButton.visibility = View.GONE
|
||||
binding.deleteButton.isVisible = false
|
||||
|
||||
viewModel.loadEpisode(episodeId)
|
||||
}else {
|
||||
} else {
|
||||
val playerItem = args.playerItem!!
|
||||
viewModel.loadEpisode(playerItem)
|
||||
|
||||
|
@ -143,14 +123,67 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
|||
findNavController().navigate(R.id.downloadFragment)
|
||||
}
|
||||
|
||||
binding.checkButton.visibility = View.GONE
|
||||
binding.favoriteButton.visibility = View.GONE
|
||||
binding.downloadButton.visibility = View.GONE
|
||||
binding.checkButton.isVisible = false
|
||||
binding.favoriteButton.isVisible = false
|
||||
binding.downloadButtonWrapper.isVisible = false
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Check icon
|
||||
val checkDrawable = when (played) {
|
||||
true -> R.drawable.ic_check_filled
|
||||
false -> R.drawable.ic_check
|
||||
}
|
||||
binding.checkButton.setImageResource(checkDrawable)
|
||||
|
||||
// Favorite icon
|
||||
val favoriteDrawable = when (favorite) {
|
||||
true -> R.drawable.ic_heart_filled
|
||||
false -> R.drawable.ic_heart
|
||||
}
|
||||
binding.favoriteButton.setImageResource(favoriteDrawable)
|
||||
|
||||
// Download icon
|
||||
val downloadDrawable = when (downloaded) {
|
||||
true -> R.drawable.ic_download_filled
|
||||
false -> R.drawable.ic_download
|
||||
}
|
||||
binding.downloadButton.setImageResource(downloadDrawable)
|
||||
|
||||
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.message
|
||||
}
|
||||
|
||||
private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) {
|
||||
navigateToPlayerActivity(items.items.toTypedArray())
|
||||
binding.playButton.setImageDrawable(
|
||||
|
|
|
@ -5,7 +5,11 @@ import androidx.fragment.app.Fragment
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.R
|
||||
|
@ -16,7 +20,9 @@ import dev.jdtech.jellyfin.databinding.FragmentFavoriteBinding
|
|||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.viewmodels.FavoriteViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class FavoriteFragment : Fragment() {
|
||||
|
@ -24,14 +30,14 @@ class FavoriteFragment : Fragment() {
|
|||
private lateinit var binding: FragmentFavoriteBinding
|
||||
private val viewModel: FavoriteViewModel by viewModels()
|
||||
|
||||
private lateinit var errorDialog: ErrorDialogFragment
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentFavoriteBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = viewModel
|
||||
binding.favoritesRecyclerView.adapter = FavoritesListAdapter(
|
||||
ViewItemListAdapter.OnClickListener { item ->
|
||||
navigateToMediaInfoFragment(item)
|
||||
|
@ -39,40 +45,56 @@ class FavoriteFragment : Fragment() {
|
|||
navigateToEpisodeBottomSheetFragment(item)
|
||||
})
|
||||
|
||||
viewModel.finishedLoading.observe(viewLifecycleOwner, { isFinished ->
|
||||
binding.loadingIndicator.visibility = if (isFinished) View.GONE else View.VISIBLE
|
||||
})
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.favoritesRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
||||
binding.favoritesRecyclerView.visibility = View.VISIBLE
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is FavoriteViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||
is FavoriteViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
is FavoriteViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||
viewModel.loadData()
|
||||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog")
|
||||
errorDialog.show(parentFragmentManager, "errordialog")
|
||||
}
|
||||
|
||||
viewModel.favoriteSections.observe(viewLifecycleOwner, { sections ->
|
||||
if (sections.isEmpty()) {
|
||||
binding.noFavoritesText.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.noFavoritesText.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal(uiState: FavoriteViewModel.UiState.Normal) {
|
||||
uiState.apply {
|
||||
binding.noFavoritesText.isVisible = favoriteSections.isEmpty()
|
||||
|
||||
val adapter = binding.favoritesRecyclerView.adapter as FavoritesListAdapter
|
||||
adapter.submitList(favoriteSections)
|
||||
}
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.favoritesRecyclerView.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.loadingIndicator.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: FavoriteViewModel.UiState.Error) {
|
||||
val error = uiState.message ?: resources.getString(R.string.unknown_error)
|
||||
errorDialog = ErrorDialogFragment(error)
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.favoritesRecyclerView.isVisible = false
|
||||
binding.errorLayout.errorPanel.isVisible = true
|
||||
checkIfLoginRequired(error)
|
||||
}
|
||||
|
||||
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
|
||||
findNavController().navigate(
|
||||
FavoriteFragmentDirections.actionFavoriteFragmentToMediaInfoFragment(
|
||||
|
|
|
@ -12,7 +12,9 @@ import android.widget.Toast.LENGTH_LONG
|
|||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.R
|
||||
|
@ -28,9 +30,9 @@ import dev.jdtech.jellyfin.models.ContentType.TVSHOW
|
|||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.utils.contentType
|
||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel
|
||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel.Loading
|
||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel.LoadingError
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class HomeFragment : Fragment() {
|
||||
|
@ -38,6 +40,8 @@ class HomeFragment : Fragment() {
|
|||
private lateinit var binding: FragmentHomeBinding
|
||||
private val viewModel: HomeViewModel by viewModels()
|
||||
|
||||
private lateinit var errorDialog: ErrorDialogFragment
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
|
@ -66,9 +70,6 @@ class HomeFragment : Fragment() {
|
|||
): View {
|
||||
binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = viewModel
|
||||
|
||||
setupView()
|
||||
bindState()
|
||||
|
||||
|
@ -84,6 +85,7 @@ class HomeFragment : Fragment() {
|
|||
private fun setupView() {
|
||||
binding.refreshLayout.setOnRefreshListener {
|
||||
viewModel.refreshData()
|
||||
// binding.refreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
binding.viewsRecyclerView.adapter = ViewListAdapter(
|
||||
|
@ -99,48 +101,54 @@ class HomeFragment : Fragment() {
|
|||
.show()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun bindState() {
|
||||
viewModel.onStateUpdate(lifecycleScope) { state ->
|
||||
when (state) {
|
||||
is Loading -> bindLoading(state)
|
||||
is LoadingError -> bindError(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindError(state: LoadingError) {
|
||||
checkIfLoginRequired(state.message)
|
||||
binding.errorLayout.errorPanel.isVisible = true
|
||||
binding.viewsRecyclerView.isVisible = false
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(state.message).show(
|
||||
parentFragmentManager,
|
||||
"errordialog"
|
||||
)
|
||||
}
|
||||
|
||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||
viewModel.refreshData()
|
||||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
errorDialog.show(parentFragmentManager, "errordialog")
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindLoading(state: Loading) {
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
binding.viewsRecyclerView.isVisible = true
|
||||
private fun bindState() {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is HomeViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||
is HomeViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
is HomeViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.loadingIndicator.visibility = when {
|
||||
state.inProgress && binding.refreshLayout.isRefreshing -> View.GONE
|
||||
state.inProgress -> View.VISIBLE
|
||||
else -> {
|
||||
private fun bindUiStateNormal(uiState: HomeViewModel.UiState.Normal) {
|
||||
uiState.apply {
|
||||
val adapter = binding.viewsRecyclerView.adapter as ViewListAdapter
|
||||
adapter.submitList(uiState.homeItems)
|
||||
}
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
View.GONE
|
||||
binding.viewsRecyclerView.isVisible = true
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.loadingIndicator.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: HomeViewModel.UiState.Error) {
|
||||
val error = uiState.message ?: getString(R.string.unknown_error)
|
||||
errorDialog = ErrorDialogFragment(error)
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.refreshLayout.isRefreshing = false
|
||||
binding.viewsRecyclerView.isVisible = false
|
||||
binding.errorLayout.errorPanel.isVisible = true
|
||||
checkIfLoginRequired(error)
|
||||
}
|
||||
|
||||
private fun navigateToLibraryFragment(view: dev.jdtech.jellyfin.models.View) {
|
||||
|
|
|
@ -3,8 +3,12 @@ package dev.jdtech.jellyfin.fragments
|
|||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
|
@ -16,6 +20,7 @@ import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
|||
import dev.jdtech.jellyfin.dialogs.SortDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.SortBy
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import java.lang.IllegalArgumentException
|
||||
|
@ -26,9 +31,10 @@ class LibraryFragment : Fragment() {
|
|||
|
||||
private lateinit var binding: FragmentLibraryBinding
|
||||
private val viewModel: LibraryViewModel by viewModels()
|
||||
|
||||
private val args: LibraryFragmentArgs by navArgs()
|
||||
|
||||
private lateinit var errorDialog: ErrorDialogFragment
|
||||
|
||||
@Inject
|
||||
lateinit var sp: SharedPreferences
|
||||
|
||||
|
@ -67,47 +73,38 @@ class LibraryFragment : Fragment() {
|
|||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentLibraryBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.itemsRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
||||
binding.itemsRecyclerView.visibility = View.VISIBLE
|
||||
}
|
||||
})
|
||||
|
||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||
viewModel.loadItems(args.libraryId, args.libraryType)
|
||||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
||||
errorDialog.show(
|
||||
parentFragmentManager,
|
||||
"errordialog"
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.finishedLoading.observe(viewLifecycleOwner, {
|
||||
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
|
||||
})
|
||||
|
||||
binding.itemsRecyclerView.adapter =
|
||||
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item ->
|
||||
navigateToMediaInfoFragment(item)
|
||||
})
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
when (uiState) {
|
||||
is LibraryViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||
is LibraryViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
is LibraryViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting options
|
||||
val sortBy = SortBy.fromString(sp.getString("sortBy", SortBy.defaultValue.name)!!)
|
||||
val sortOrder = try {
|
||||
|
@ -118,6 +115,30 @@ class LibraryFragment : Fragment() {
|
|||
|
||||
viewModel.loadItems(args.libraryId, args.libraryType, sortBy = sortBy, sortOrder = sortOrder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal(uiState: LibraryViewModel.UiState.Normal) {
|
||||
val adapter = binding.itemsRecyclerView.adapter as ViewItemListAdapter
|
||||
adapter.submitList(uiState.items)
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.itemsRecyclerView.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.loadingIndicator.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: LibraryViewModel.UiState.Error) {
|
||||
val error = uiState.message ?: getString(R.string.unknown_error)
|
||||
errorDialog = ErrorDialogFragment(error)
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.itemsRecyclerView.isVisible = false
|
||||
binding.errorLayout.errorPanel.isVisible = true
|
||||
checkIfLoginRequired(error)
|
||||
}
|
||||
|
||||
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
|
||||
findNavController().navigate(
|
||||
|
|
|
@ -3,8 +3,12 @@ package dev.jdtech.jellyfin.fragments
|
|||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.R
|
||||
|
@ -13,7 +17,9 @@ import dev.jdtech.jellyfin.databinding.FragmentMediaBinding
|
|||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.viewmodels.MediaViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MediaFragment : Fragment() {
|
||||
|
@ -23,6 +29,8 @@ class MediaFragment : Fragment() {
|
|||
|
||||
private var originalSoftInputMode: Int? = null
|
||||
|
||||
private lateinit var errorDialog: ErrorDialogFragment
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setHasOptionsMenu(true)
|
||||
|
@ -56,37 +64,30 @@ class MediaFragment : Fragment() {
|
|||
): View {
|
||||
binding = FragmentMediaBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = viewModel
|
||||
binding.viewsRecyclerView.adapter =
|
||||
CollectionListAdapter(CollectionListAdapter.OnClickListener { library ->
|
||||
navigateToLibraryFragment(library)
|
||||
})
|
||||
|
||||
viewModel.finishedLoading.observe(viewLifecycleOwner, {
|
||||
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
|
||||
})
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.viewsRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
||||
binding.viewsRecyclerView.visibility = View.VISIBLE
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is MediaViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||
is MediaViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
is MediaViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||
viewModel.loadData()
|
||||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
||||
parentFragmentManager,
|
||||
"errordialog"
|
||||
)
|
||||
errorDialog.show(parentFragmentManager, "errordialog")
|
||||
}
|
||||
|
||||
return binding.root
|
||||
|
@ -105,6 +106,29 @@ class MediaFragment : Fragment() {
|
|||
originalSoftInputMode?.let { activity?.window?.setSoftInputMode(it) }
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal(uiState: MediaViewModel.UiState.Normal) {
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.viewsRecyclerView.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
val adapter = binding.viewsRecyclerView.adapter as CollectionListAdapter
|
||||
adapter.submitList(uiState.collections)
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.loadingIndicator.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: MediaViewModel.UiState.Error) {
|
||||
val error = uiState.message ?: resources.getString(R.string.unknown_error)
|
||||
errorDialog = ErrorDialogFragment(error)
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.viewsRecyclerView.isVisible = false
|
||||
binding.errorLayout.errorPanel.isVisible = true
|
||||
checkIfLoginRequired(error)
|
||||
|
||||
}
|
||||
|
||||
private fun navigateToLibraryFragment(library: BaseItemDto) {
|
||||
findNavController().navigate(
|
||||
MediaFragmentDirections.actionNavigationMediaToLibraryFragment(
|
||||
|
|
|
@ -8,23 +8,28 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.R
|
||||
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||
import dev.jdtech.jellyfin.bindBaseItemImage
|
||||
import dev.jdtech.jellyfin.bindItemBackdropImage
|
||||
import dev.jdtech.jellyfin.databinding.FragmentMediaInfoBinding
|
||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
|
||||
import dev.jdtech.jellyfin.models.PlayerItem
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.utils.requestDownload
|
||||
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.serializer.toUUID
|
||||
import timber.log.Timber
|
||||
|
@ -36,35 +41,39 @@ class MediaInfoFragment : Fragment() {
|
|||
private lateinit var binding: FragmentMediaInfoBinding
|
||||
private val viewModel: MediaInfoViewModel by viewModels()
|
||||
private val playerViewModel: PlayerViewModel by viewModels()
|
||||
|
||||
private val args: MediaInfoFragmentArgs by navArgs()
|
||||
|
||||
lateinit var errorDialog: ErrorDialogFragment
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.mediaInfoScrollview.visibility = View.GONE
|
||||
} else {
|
||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
||||
binding.mediaInfoScrollview.visibility = View.VISIBLE
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is MediaInfoViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||
is MediaInfoViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
is MediaInfoViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
}
|
||||
}
|
||||
if (!args.isOffline) {
|
||||
viewModel.loadData(args.itemId, args.itemType)
|
||||
} else {
|
||||
viewModel.loadData(args.playerItem!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if(args.itemType != "Movie") {
|
||||
binding.downloadButton.visibility = View.GONE
|
||||
|
@ -74,36 +83,6 @@ class MediaInfoFragment : Fragment() {
|
|||
viewModel.loadData(args.itemId, args.itemType)
|
||||
}
|
||||
|
||||
viewModel.downloadMedia.observe(viewLifecycleOwner, {
|
||||
if (it) {
|
||||
requestDownload(Uri.parse(viewModel.downloadRequestItem.uri), viewModel.downloadRequestItem, this)
|
||||
viewModel.doneDownloadMedia()
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.item.observe(viewLifecycleOwner, { item ->
|
||||
if (item.originalTitle != item.name) {
|
||||
binding.originalTitle.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.originalTitle.visibility = View.GONE
|
||||
}
|
||||
if (item.remoteTrailers.isNullOrEmpty()) {
|
||||
binding.trailerButton.visibility = View.GONE
|
||||
}
|
||||
binding.communityRating.visibility = when (item.communityRating != null) {
|
||||
true -> View.VISIBLE
|
||||
false -> View.GONE
|
||||
}
|
||||
Timber.d(item.seasonId.toString())
|
||||
})
|
||||
|
||||
viewModel.actors.observe(viewLifecycleOwner, { actors ->
|
||||
when (actors.isNullOrEmpty()) {
|
||||
false -> binding.actors.visibility = View.VISIBLE
|
||||
true -> binding.actors.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
|
||||
playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems ->
|
||||
when (playerItems) {
|
||||
is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(playerItems)
|
||||
|
@ -111,44 +90,17 @@ class MediaInfoFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
viewModel.played.observe(viewLifecycleOwner, {
|
||||
val drawable = when (it) {
|
||||
true -> R.drawable.ic_check_filled
|
||||
false -> R.drawable.ic_check
|
||||
}
|
||||
|
||||
binding.checkButton.setImageResource(drawable)
|
||||
})
|
||||
|
||||
viewModel.favorite.observe(viewLifecycleOwner, {
|
||||
val drawable = when (it) {
|
||||
true -> R.drawable.ic_heart_filled
|
||||
false -> R.drawable.ic_heart
|
||||
}
|
||||
|
||||
binding.favoriteButton.setImageResource(drawable)
|
||||
})
|
||||
|
||||
viewModel.downloaded.observe(viewLifecycleOwner, {
|
||||
val drawable = when (it) {
|
||||
true -> R.drawable.ic_download_filled
|
||||
false -> R.drawable.ic_download
|
||||
}
|
||||
|
||||
binding.downloadButton.setImageResource(drawable)
|
||||
})
|
||||
|
||||
binding.trailerButton.setOnClickListener {
|
||||
if (viewModel.item.value?.remoteTrailers.isNullOrEmpty()) return@setOnClickListener
|
||||
if (viewModel.item?.remoteTrailers.isNullOrEmpty()) return@setOnClickListener
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(viewModel.item.value?.remoteTrailers?.get(0)?.url)
|
||||
Uri.parse(viewModel.item?.remoteTrailers?.get(0)?.url)
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.nextUp.setOnClickListener {
|
||||
navigateToEpisodeBottomSheetFragment(viewModel.nextUp.value!!)
|
||||
navigateToEpisodeBottomSheetFragment(viewModel.nextUp!!)
|
||||
}
|
||||
|
||||
binding.seasonsRecyclerView.adapter =
|
||||
|
@ -166,9 +118,8 @@ class MediaInfoFragment : Fragment() {
|
|||
|
||||
binding.playButton.setOnClickListener {
|
||||
binding.playButton.setImageResource(android.R.color.transparent)
|
||||
binding.progressCircular.visibility = View.VISIBLE
|
||||
|
||||
viewModel.item.value?.let { item ->
|
||||
binding.progressCircular.isVisible = true
|
||||
viewModel.item?.let { item ->
|
||||
if (!args.isOffline) {
|
||||
playerViewModel.loadPlayerItems(item) {
|
||||
VideoVersionDialogFragment(item, playerViewModel).show(
|
||||
|
@ -188,16 +139,28 @@ class MediaInfoFragment : Fragment() {
|
|||
}
|
||||
|
||||
binding.checkButton.setOnClickListener {
|
||||
when (viewModel.played.value) {
|
||||
true -> viewModel.markAsUnplayed(args.itemId)
|
||||
false -> viewModel.markAsPlayed(args.itemId)
|
||||
when (viewModel.played) {
|
||||
true -> {
|
||||
viewModel.markAsUnplayed(args.itemId)
|
||||
binding.checkButton.setImageResource(R.drawable.ic_check)
|
||||
}
|
||||
false -> {
|
||||
viewModel.markAsPlayed(args.itemId)
|
||||
binding.checkButton.setImageResource(R.drawable.ic_check_filled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.favoriteButton.setOnClickListener {
|
||||
when (viewModel.favorite.value) {
|
||||
true -> viewModel.unmarkAsFavorite(args.itemId)
|
||||
false -> viewModel.markAsFavorite(args.itemId)
|
||||
when (viewModel.favorite) {
|
||||
true -> {
|
||||
viewModel.unmarkAsFavorite(args.itemId)
|
||||
binding.favoriteButton.setImageResource(R.drawable.ic_heart)
|
||||
}
|
||||
false -> {
|
||||
viewModel.markAsFavorite(args.itemId)
|
||||
binding.favoriteButton.setImageResource(R.drawable.ic_heart_filled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,23 +168,90 @@ class MediaInfoFragment : Fragment() {
|
|||
viewModel.loadDownloadRequestItem(args.itemId)
|
||||
}
|
||||
|
||||
binding.deleteButton.visibility = View.GONE
|
||||
|
||||
viewModel.loadData(args.itemId, args.itemType)
|
||||
binding.deleteButton.isVisible = false
|
||||
} else {
|
||||
binding.favoriteButton.visibility = View.GONE
|
||||
binding.checkButton.visibility = View.GONE
|
||||
binding.downloadButton.visibility = View.GONE
|
||||
binding.favoriteButton.isVisible = false
|
||||
binding.checkButton.isVisible = false
|
||||
binding.downloadButton.isVisible = false
|
||||
|
||||
binding.deleteButton.setOnClickListener {
|
||||
viewModel.deleteItem()
|
||||
findNavController().navigate(R.id.downloadFragment)
|
||||
}
|
||||
|
||||
viewModel.loadData(args.playerItem!!)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal(uiState: MediaInfoViewModel.UiState.Normal) {
|
||||
uiState.apply {
|
||||
binding.originalTitle.isVisible = item.originalTitle != item.name
|
||||
if (item.remoteTrailers.isNullOrEmpty()) {
|
||||
binding.trailerButton.isVisible = false
|
||||
}
|
||||
binding.communityRating.isVisible = item.communityRating != null
|
||||
binding.actors.isVisible = actors.isNotEmpty()
|
||||
|
||||
// Check icon
|
||||
val checkDrawable = when (played) {
|
||||
true -> R.drawable.ic_check_filled
|
||||
false -> R.drawable.ic_check
|
||||
}
|
||||
binding.checkButton.setImageResource(checkDrawable)
|
||||
|
||||
// Favorite icon
|
||||
val favoriteDrawable = when (favorite) {
|
||||
true -> R.drawable.ic_heart_filled
|
||||
false -> R.drawable.ic_heart
|
||||
}
|
||||
binding.favoriteButton.setImageResource(favoriteDrawable)
|
||||
|
||||
// Download icon
|
||||
val downloadDrawable = when (downloaded) {
|
||||
true -> R.drawable.ic_download_filled
|
||||
false -> R.drawable.ic_download
|
||||
}
|
||||
binding.downloadButton.setImageResource(downloadDrawable)
|
||||
binding.name.text = item.name
|
||||
binding.originalTitle.text = item.originalTitle
|
||||
if (dateString.isEmpty()) {
|
||||
binding.year.isVisible = false
|
||||
} else {
|
||||
binding.year.text = dateString
|
||||
}
|
||||
if (runTime.isEmpty()) {
|
||||
binding.playtime.isVisible = false
|
||||
} else {
|
||||
binding.playtime.text = runTime
|
||||
}
|
||||
binding.officialRating.text = item.officialRating
|
||||
binding.communityRating.text = item.communityRating.toString()
|
||||
binding.genresLayout.isVisible = item.genres?.isNotEmpty() ?: false
|
||||
binding.genres.text = genresString
|
||||
binding.directorLayout.isVisible = director != null
|
||||
binding.director.text = director?.name
|
||||
binding.writersLayout.isVisible = writers.isNotEmpty()
|
||||
binding.writers.text = writersString
|
||||
binding.description.text = item.overview
|
||||
binding.nextUpLayout.isVisible = nextUp != null
|
||||
binding.nextUpName.text = String.format(getString(R.string.episode_name_extended), nextUp?.parentIndexNumber, nextUp?.indexNumber, nextUp?.name)
|
||||
binding.seasonsLayout.isVisible = seasons.isNotEmpty()
|
||||
val seasonsAdapter = binding.seasonsRecyclerView.adapter as ViewItemListAdapter
|
||||
seasonsAdapter.submitList(seasons)
|
||||
val actorsAdapter = binding.peopleRecyclerView.adapter as PersonListAdapter
|
||||
actorsAdapter.submitList(actors)
|
||||
bindItemBackdropImage(binding.itemBanner, item)
|
||||
bindBaseItemImage(binding.nextUpImage, nextUp)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {}
|
||||
|
||||
private fun bindUiStateError(uiState: MediaInfoViewModel.UiState.Error) {
|
||||
val error = uiState.message ?: getString(R.string.unknown_error)
|
||||
binding.mediaInfoScrollview.isVisible = false
|
||||
binding.errorLayout.errorPanel.isVisible = true
|
||||
checkIfLoginRequired(error)
|
||||
}
|
||||
|
||||
private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) {
|
||||
navigateToPlayerActivity(items.items.toTypedArray())
|
||||
binding.playButton.setImageDrawable(
|
||||
|
|
|
@ -9,6 +9,9 @@ import androidx.core.view.isVisible
|
|||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.fragment.app.Fragment
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
|
@ -19,7 +22,9 @@ import dev.jdtech.jellyfin.databinding.FragmentPersonDetailBinding
|
|||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.viewmodels.PersonDetailViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class PersonDetailFragment : Fragment() {
|
||||
|
@ -29,15 +34,14 @@ internal class PersonDetailFragment : Fragment() {
|
|||
|
||||
private val args: PersonDetailFragmentArgs by navArgs()
|
||||
|
||||
private lateinit var errorDialog: ErrorDialogFragment
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentPersonDetailBinding.inflate(inflater, container, false)
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = viewModel
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
|
@ -47,42 +51,65 @@ internal class PersonDetailFragment : Fragment() {
|
|||
binding.moviesList.adapter = adapter()
|
||||
binding.showList.adapter = adapter()
|
||||
|
||||
viewModel.data.observe(viewLifecycleOwner) { data ->
|
||||
binding.name.text = data.name
|
||||
binding.overview.text = data.overview
|
||||
|
||||
setupOverviewExpansion()
|
||||
|
||||
bindItemImage(binding.personImage, data.dto)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is PersonDetailViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||
is PersonDetailViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
is PersonDetailViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
}
|
||||
}
|
||||
viewModel.loadData(args.personId)
|
||||
}
|
||||
|
||||
viewModel.finishedLoading.observe(viewLifecycleOwner, {
|
||||
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
|
||||
})
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.fragmentContent.visibility = View.GONE
|
||||
} else {
|
||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
||||
binding.fragmentContent.visibility = View.VISIBLE
|
||||
}
|
||||
})
|
||||
|
||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||
viewModel.loadData(args.personId)
|
||||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
||||
parentFragmentManager,
|
||||
"errordialog"
|
||||
)
|
||||
errorDialog.show(parentFragmentManager, "errordialog")
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.loadData(args.personId)
|
||||
private fun bindUiStateNormal(uiState: PersonDetailViewModel.UiState.Normal) {
|
||||
uiState.apply {
|
||||
binding.name.text = data.name
|
||||
binding.overview.text = data.overview
|
||||
setupOverviewExpansion()
|
||||
bindItemImage(binding.personImage, data.dto)
|
||||
|
||||
if (starredIn.movies.isNotEmpty()) {
|
||||
binding.movieLabel.isVisible = true
|
||||
val moviesAdapter = binding.moviesList.adapter as ViewItemListAdapter
|
||||
moviesAdapter.submitList(starredIn.movies)
|
||||
}
|
||||
if (starredIn.shows.isNotEmpty()) {
|
||||
binding.showLabel.isVisible = true
|
||||
val showsAdapter = binding.showList.adapter as ViewItemListAdapter
|
||||
showsAdapter.submitList(starredIn.shows)
|
||||
}
|
||||
}
|
||||
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.fragmentContent.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.loadingIndicator.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: PersonDetailViewModel.UiState.Error) {
|
||||
val error = uiState.message ?: resources.getString(R.string.unknown_error)
|
||||
errorDialog = ErrorDialogFragment(error)
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.fragmentContent.isVisible = false
|
||||
binding.errorLayout.errorPanel.isVisible = true
|
||||
checkIfLoginRequired(error)
|
||||
}
|
||||
|
||||
private fun adapter() = ViewItemListAdapter(
|
||||
|
@ -103,7 +130,6 @@ internal class PersonDetailFragment : Fragment() {
|
|||
binding.overviewGradient.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,11 @@ import androidx.fragment.app.Fragment
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
|
@ -17,24 +21,25 @@ import dev.jdtech.jellyfin.databinding.FragmentSearchResultBinding
|
|||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.viewmodels.SearchResultViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SearchResultFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentSearchResultBinding
|
||||
private val viewModel: SearchResultViewModel by viewModels()
|
||||
|
||||
private val args: SearchResultFragmentArgs by navArgs()
|
||||
|
||||
private lateinit var errorDialog: ErrorDialogFragment
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentSearchResultBinding.inflate(inflater, container, false)
|
||||
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
binding.viewModel = viewModel
|
||||
binding.searchResultsRecyclerView.adapter = FavoritesListAdapter(
|
||||
ViewItemListAdapter.OnClickListener { item ->
|
||||
navigateToMediaInfoFragment(item)
|
||||
|
@ -42,42 +47,58 @@ class SearchResultFragment : Fragment() {
|
|||
navigateToEpisodeBottomSheetFragment(item)
|
||||
})
|
||||
|
||||
viewModel.finishedLoading.observe(viewLifecycleOwner, { isFinished ->
|
||||
binding.loadingIndicator.visibility = if (isFinished) View.GONE else View.VISIBLE
|
||||
})
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.searchResultsRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
||||
binding.searchResultsRecyclerView.visibility = View.VISIBLE
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is SearchResultViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||
is SearchResultViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
is SearchResultViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
}
|
||||
}
|
||||
viewModel.loadData(args.query)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||
viewModel.loadData(args.query)
|
||||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog")
|
||||
errorDialog.show(parentFragmentManager, "errordialog")
|
||||
}
|
||||
|
||||
viewModel.sections.observe(viewLifecycleOwner, { sections ->
|
||||
if (sections.isEmpty()) {
|
||||
binding.noSearchResultsText.visibility = View.VISIBLE
|
||||
} else {
|
||||
binding.noSearchResultsText.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.loadData(args.query)
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal(uiState: SearchResultViewModel.UiState.Normal) {
|
||||
uiState.apply {
|
||||
binding.noSearchResultsText.isVisible = sections.isEmpty()
|
||||
|
||||
val adapter = binding.searchResultsRecyclerView.adapter as FavoritesListAdapter
|
||||
adapter.submitList(uiState.sections)
|
||||
}
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.searchResultsRecyclerView.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.loadingIndicator.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: SearchResultViewModel.UiState.Error) {
|
||||
val error = uiState.message ?: getString(R.string.unknown_error)
|
||||
errorDialog = ErrorDialogFragment(error)
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.searchResultsRecyclerView.isVisible = false
|
||||
binding.errorLayout.errorPanel.isVisible = true
|
||||
checkIfLoginRequired(error)
|
||||
}
|
||||
|
||||
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
|
||||
findNavController().navigate(
|
||||
FavoriteFragmentDirections.actionFavoriteFragmentToMediaInfoFragment(
|
||||
|
|
|
@ -5,7 +5,11 @@ import androidx.fragment.app.Fragment
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
|
@ -15,58 +19,81 @@ import dev.jdtech.jellyfin.databinding.FragmentSeasonBinding
|
|||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||
import dev.jdtech.jellyfin.viewmodels.SeasonViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SeasonFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentSeasonBinding
|
||||
private val viewModel: SeasonViewModel by viewModels()
|
||||
|
||||
private val args: SeasonFragmentArgs by navArgs()
|
||||
|
||||
private lateinit var errorDialog: ErrorDialogFragment
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentSeasonBinding.inflate(inflater, container, false)
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
||||
if (error != null) {
|
||||
checkIfLoginRequired(error)
|
||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
||||
binding.episodesRecyclerView.visibility = View.GONE
|
||||
} else {
|
||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
||||
binding.episodesRecyclerView.visibility = View.VISIBLE
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is SeasonViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||
is SeasonViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
is SeasonViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
}
|
||||
}
|
||||
viewModel.loadEpisodes(args.seriesId, args.seasonId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||
viewModel.loadEpisodes(args.seriesId, args.seasonId)
|
||||
}
|
||||
|
||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(parentFragmentManager, "errordialog")
|
||||
errorDialog.show(parentFragmentManager, "errordialog")
|
||||
}
|
||||
|
||||
viewModel.finishedLoading.observe(viewLifecycleOwner, {
|
||||
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
|
||||
})
|
||||
|
||||
binding.episodesRecyclerView.adapter =
|
||||
EpisodeListAdapter(EpisodeListAdapter.OnClickListener { episode ->
|
||||
navigateToEpisodeBottomSheetFragment(episode)
|
||||
}, args.seriesId, args.seriesName, args.seasonId, args.seasonName)
|
||||
|
||||
viewModel.loadEpisodes(args.seriesId, args.seasonId)
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal(uiState: SeasonViewModel.UiState.Normal) {
|
||||
uiState.apply {
|
||||
val adapter = binding.episodesRecyclerView.adapter as EpisodeListAdapter
|
||||
adapter.submitList(uiState.episodes)
|
||||
}
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.episodesRecyclerView.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {
|
||||
binding.loadingIndicator.isVisible = true
|
||||
binding.errorLayout.errorPanel.isVisible = false
|
||||
}
|
||||
|
||||
private fun bindUiStateError(uiState: SeasonViewModel.UiState.Error) {
|
||||
val error = uiState.message ?: getString(R.string.unknown_error)
|
||||
errorDialog = ErrorDialogFragment(error)
|
||||
binding.loadingIndicator.isVisible = false
|
||||
binding.episodesRecyclerView.isVisible = false
|
||||
binding.errorLayout.errorPanel.isVisible = true
|
||||
checkIfLoginRequired(error)
|
||||
}
|
||||
|
||||
private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) {
|
||||
|
|
|
@ -6,12 +6,16 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.databinding.FragmentServerSelectBinding
|
||||
import dev.jdtech.jellyfin.dialogs.DeleteServerDialogFragment
|
||||
import dev.jdtech.jellyfin.adapters.ServerGridAdapter
|
||||
import dev.jdtech.jellyfin.viewmodels.ServerSelectViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ServerSelectFragment : Fragment() {
|
||||
|
@ -44,11 +48,15 @@ class ServerSelectFragment : Fragment() {
|
|||
navigateToAddServerFragment()
|
||||
}
|
||||
|
||||
viewModel.navigateToMain.observe(viewLifecycleOwner, {
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onNavigateToMain(viewLifecycleOwner.lifecycleScope) {
|
||||
if (it) {
|
||||
navigateToMainActivity()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
@ -61,6 +69,5 @@ class ServerSelectFragment : Fragment() {
|
|||
|
||||
private fun navigateToMainActivity() {
|
||||
findNavController().navigate(ServerSelectFragmentDirections.actionServerSelectFragmentToHomeFragment())
|
||||
viewModel.doneNavigatingToMain()
|
||||
}
|
||||
}
|
|
@ -5,15 +5,17 @@ import dev.jdtech.jellyfin.models.CollectionType.HomeVideos
|
|||
import dev.jdtech.jellyfin.models.CollectionType.LiveTv
|
||||
import dev.jdtech.jellyfin.models.CollectionType.Music
|
||||
import dev.jdtech.jellyfin.models.CollectionType.Playlists
|
||||
import dev.jdtech.jellyfin.models.CollectionType.BoxSets
|
||||
|
||||
enum class CollectionType (val type: String) {
|
||||
HomeVideos("homevideos"),
|
||||
Music("music"),
|
||||
Playlists("playlists"),
|
||||
Books("books"),
|
||||
LiveTv("livetv")
|
||||
LiveTv("livetv"),
|
||||
BoxSets("boxsets")
|
||||
}
|
||||
|
||||
fun unsupportedCollections() = listOf(
|
||||
HomeVideos, Music, Playlists, Books, LiveTv
|
||||
HomeVideos, Music, Playlists, Books, LiveTv, BoxSets
|
||||
)
|
|
@ -11,12 +11,17 @@ import androidx.leanback.widget.ArrayObjectAdapter
|
|||
import androidx.leanback.widget.HeaderItem
|
||||
import androidx.leanback.widget.ListRow
|
||||
import androidx.leanback.widget.ListRowPresenter
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.R
|
||||
import dev.jdtech.jellyfin.adapters.HomeItem
|
||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class HomeFragment : BrowseSupportFragment() {
|
||||
|
@ -48,7 +53,22 @@ internal class HomeFragment : BrowseSupportFragment() {
|
|||
setOnClickListener { navigateToSettingsFragment() }
|
||||
}
|
||||
|
||||
viewModel.views().observe(viewLifecycleOwner) { homeItems ->
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is HomeViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||
is HomeViewModel.UiState.Loading -> Unit
|
||||
is HomeViewModel.UiState.Error -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal(uiState: HomeViewModel.UiState.Normal) {
|
||||
uiState.apply {
|
||||
rowsAdapter.clear()
|
||||
homeItems.map { section -> rowsAdapter.add(section.toListRow()) }
|
||||
}
|
||||
|
|
|
@ -11,22 +11,24 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
import dev.jdtech.jellyfin.R
|
||||
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
||||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||
import dev.jdtech.jellyfin.bindBaseItemImage
|
||||
import dev.jdtech.jellyfin.databinding.MediaDetailFragmentBinding
|
||||
import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
|
||||
import dev.jdtech.jellyfin.models.PlayerItem
|
||||
import dev.jdtech.jellyfin.tv.ui.MediaDetailViewModel.State.Movie
|
||||
import dev.jdtech.jellyfin.tv.ui.MediaDetailViewModel.State.TvShow
|
||||
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel.PlayerItemError
|
||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel.PlayerItems
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
|
||||
@AndroidEntryPoint
|
||||
|
@ -35,7 +37,6 @@ internal class MediaDetailFragment : Fragment() {
|
|||
private lateinit var binding: MediaDetailFragmentBinding
|
||||
|
||||
private val viewModel: MediaInfoViewModel by viewModels()
|
||||
private val detailViewModel: MediaDetailViewModel by viewModels()
|
||||
private val playerViewModel: PlayerViewModel by viewModels()
|
||||
|
||||
private val args: MediaDetailFragmentArgs by navArgs()
|
||||
|
@ -52,28 +53,29 @@ internal class MediaDetailFragment : Fragment() {
|
|||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = MediaDetailFragmentBinding.inflate(inflater)
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.viewModel = viewModel
|
||||
binding.item = detailViewModel.transformData(viewModel.item, resources) {
|
||||
bindActions(it)
|
||||
bindState(it)
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||
Timber.d("$uiState")
|
||||
when (uiState) {
|
||||
is MediaInfoViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||
is MediaInfoViewModel.UiState.Loading -> bindUiStateLoading()
|
||||
is MediaInfoViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val seasonsAdapter = ViewItemListAdapter(
|
||||
fixedWidth = true,
|
||||
onClickListener = ViewItemListAdapter.OnClickListener {})
|
||||
|
||||
viewModel.seasons.observe(viewLifecycleOwner) {
|
||||
seasonsAdapter.submitList(it)
|
||||
binding.seasonTitle.isVisible = true
|
||||
}
|
||||
|
||||
binding.seasonsRow.gridView.adapter = seasonsAdapter
|
||||
binding.seasonsRow.gridView.verticalSpacing = 25
|
||||
|
||||
|
@ -81,33 +83,110 @@ internal class MediaDetailFragment : Fragment() {
|
|||
Toast.makeText(requireContext(), "Not yet implemented", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
viewModel.actors.observe(viewLifecycleOwner) { cast ->
|
||||
castAdapter.submitList(cast)
|
||||
binding.castTitle.isVisible = cast.isNotEmpty()
|
||||
}
|
||||
|
||||
binding.castRow.gridView.adapter = castAdapter
|
||||
binding.castRow.gridView.verticalSpacing = 25
|
||||
}
|
||||
|
||||
private fun bindState(state: MediaDetailViewModel.State) {
|
||||
playerViewModel.onPlaybackRequested(lifecycleScope) { state ->
|
||||
when (state) {
|
||||
is PlayerItemError -> bindPlayerItemsError(state)
|
||||
is PlayerItems -> bindPlayerItems(state)
|
||||
playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems ->
|
||||
when (playerItems) {
|
||||
is PlayerItemError -> bindPlayerItemsError(playerItems)
|
||||
is PlayerItems -> bindPlayerItems(playerItems)
|
||||
}
|
||||
}
|
||||
|
||||
when (state.media) {
|
||||
is Movie -> binding.title.text = state.media.title
|
||||
is TvShow -> with(binding.subtitle) {
|
||||
binding.title.text = state.media.episode
|
||||
text = state.media.show
|
||||
isVisible = true
|
||||
binding.playButton.setOnClickListener {
|
||||
binding.playButton.setImageResource(android.R.color.transparent)
|
||||
binding.progressCircular.isVisible = true
|
||||
viewModel.item?.let { item ->
|
||||
playerViewModel.loadPlayerItems(item) {
|
||||
VideoVersionDialogFragment(item, playerViewModel).show(
|
||||
parentFragmentManager,
|
||||
"videoversiondialog"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.trailerButton.setOnClickListener {
|
||||
if (viewModel.item?.remoteTrailers.isNullOrEmpty()) return@setOnClickListener
|
||||
val intent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(viewModel.item?.remoteTrailers?.get(0)?.url)
|
||||
)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
binding.checkButton.setOnClickListener {
|
||||
when (viewModel.played) {
|
||||
true -> {
|
||||
viewModel.markAsUnplayed(args.itemId)
|
||||
binding.checkButton.setImageResource(R.drawable.ic_check)
|
||||
}
|
||||
false -> {
|
||||
viewModel.markAsPlayed(args.itemId)
|
||||
binding.checkButton.setImageResource(R.drawable.ic_check_filled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.favoriteButton.setOnClickListener {
|
||||
when (viewModel.favorite) {
|
||||
true -> {
|
||||
viewModel.unmarkAsFavorite(args.itemId)
|
||||
binding.favoriteButton.setImageResource(R.drawable.ic_heart)
|
||||
}
|
||||
false -> {
|
||||
viewModel.markAsFavorite(args.itemId)
|
||||
binding.favoriteButton.setImageResource(R.drawable.ic_heart_filled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.backButton.setOnClickListener { activity?.onBackPressed() }
|
||||
}
|
||||
|
||||
private fun bindUiStateNormal(uiState: MediaInfoViewModel.UiState.Normal) {
|
||||
uiState.apply {
|
||||
binding.seasonTitle.isVisible = seasons.isNotEmpty()
|
||||
val seasonsAdapter = binding.seasonsRow.gridView.adapter as ViewItemListAdapter
|
||||
seasonsAdapter.submitList(seasons)
|
||||
binding.castTitle.isVisible = actors.isNotEmpty()
|
||||
val actorsAdapter = binding.castRow.gridView.adapter as PersonListAdapter
|
||||
actorsAdapter.submitList(actors)
|
||||
|
||||
// Check icon
|
||||
val checkDrawable = when (played) {
|
||||
true -> R.drawable.ic_check_filled
|
||||
false -> R.drawable.ic_check
|
||||
}
|
||||
binding.checkButton.setImageResource(checkDrawable)
|
||||
|
||||
// Favorite icon
|
||||
val favoriteDrawable = when (favorite) {
|
||||
true -> R.drawable.ic_heart_filled
|
||||
false -> R.drawable.ic_heart
|
||||
}
|
||||
binding.favoriteButton.setImageResource(favoriteDrawable)
|
||||
|
||||
binding.title.text = item.name
|
||||
binding.subtitle.text = item.seriesName
|
||||
item.seriesName.let {
|
||||
binding.subtitle.text = it
|
||||
binding.subtitle.isVisible = true
|
||||
}
|
||||
binding.genres.text = genresString
|
||||
binding.year.text = dateString
|
||||
binding.playtime.text = runTime
|
||||
binding.officialRating.text = item.officialRating
|
||||
binding.communityRating.text = item.communityRating.toString()
|
||||
binding.description.text = item.overview
|
||||
bindBaseItemImage(binding.poster, item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindUiStateLoading() {}
|
||||
|
||||
private fun bindUiStateError(uiState: MediaInfoViewModel.UiState.Error) {}
|
||||
|
||||
private fun bindPlayerItems(items: PlayerItems) {
|
||||
navigateToPlayerActivity(items.items.toTypedArray())
|
||||
binding.playButton.setImageDrawable(
|
||||
|
@ -132,59 +211,6 @@ internal class MediaDetailFragment : Fragment() {
|
|||
binding.progressCircular.visibility = View.INVISIBLE
|
||||
}
|
||||
|
||||
private fun bindActions(state: MediaDetailViewModel.State) {
|
||||
binding.playButton.setOnClickListener {
|
||||
binding.progressCircular.isVisible = true
|
||||
viewModel.item.value?.let { item ->
|
||||
playerViewModel.loadPlayerItems(item) {
|
||||
VideoVersionDialogFragment(item, playerViewModel).show(
|
||||
parentFragmentManager,
|
||||
"videoversiondialog"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.trailerUrl != null) {
|
||||
with(binding.trailerButton) {
|
||||
isVisible = true
|
||||
setOnClickListener { playTrailer(state.trailerUrl) }
|
||||
}
|
||||
} else {
|
||||
binding.trailerButton.isVisible = false
|
||||
}
|
||||
|
||||
if (state.isPlayed) {
|
||||
with(binding.checkButton) {
|
||||
setImageDrawable(resources.getDrawable(R.drawable.ic_check_filled))
|
||||
setOnClickListener { viewModel.markAsUnplayed(args.itemId) }
|
||||
}
|
||||
} else {
|
||||
with(binding.checkButton) {
|
||||
setImageDrawable(resources.getDrawable(R.drawable.ic_check))
|
||||
setOnClickListener { viewModel.markAsPlayed(args.itemId) }
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isFavorite) {
|
||||
with(binding.favoriteButton) {
|
||||
setImageDrawable(resources.getDrawable(R.drawable.ic_heart_filled))
|
||||
setOnClickListener { viewModel.unmarkAsFavorite(args.itemId) }
|
||||
}
|
||||
} else {
|
||||
with(binding.favoriteButton) {
|
||||
setImageDrawable(resources.getDrawable(R.drawable.ic_heart))
|
||||
setOnClickListener { viewModel.markAsFavorite(args.itemId) }
|
||||
}
|
||||
}
|
||||
|
||||
binding.backButton.setOnClickListener { activity?.onBackPressed() }
|
||||
}
|
||||
|
||||
private fun playTrailer(url: String) {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url)))
|
||||
}
|
||||
|
||||
private fun navigateToPlayerActivity(
|
||||
playerItems: Array<PlayerItem>,
|
||||
) {
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package dev.jdtech.jellyfin.tv.ui
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.jdtech.jellyfin.R
|
||||
|
@ -14,38 +12,36 @@ import javax.inject.Inject
|
|||
internal class MediaDetailViewModel @Inject internal constructor() : ViewModel() {
|
||||
|
||||
fun transformData(
|
||||
data: LiveData<BaseItemDto>,
|
||||
data: BaseItemDto,
|
||||
resources: Resources,
|
||||
transformed: (State) -> Unit
|
||||
): LiveData<State> {
|
||||
return Transformations.map(data) { baseItemDto ->
|
||||
State(
|
||||
dto = baseItemDto,
|
||||
description = baseItemDto.overview.orEmpty(),
|
||||
year = baseItemDto.productionYear.toString(),
|
||||
officialRating = baseItemDto.officialRating.orEmpty(),
|
||||
communityRating = baseItemDto.communityRating.toString(),
|
||||
): State {
|
||||
return State(
|
||||
dto = data,
|
||||
description = data.overview.orEmpty(),
|
||||
year = data.productionYear.toString(),
|
||||
officialRating = data.officialRating.orEmpty(),
|
||||
communityRating = data.communityRating.toString(),
|
||||
runtimeMinutes = String.format(
|
||||
resources.getString(R.string.runtime_minutes),
|
||||
baseItemDto.runTimeTicks?.div(600_000_000)
|
||||
data.runTimeTicks?.div(600_000_000)
|
||||
),
|
||||
genres = baseItemDto.genres?.joinToString(" / ").orEmpty(),
|
||||
trailerUrl = baseItemDto.remoteTrailers?.firstOrNull()?.url,
|
||||
isPlayed = baseItemDto.userData?.played == true,
|
||||
isFavorite = baseItemDto.userData?.isFavorite == true,
|
||||
media = if (baseItemDto.type == MOVIE.type) {
|
||||
genres = data.genres?.joinToString(" / ").orEmpty(),
|
||||
trailerUrl = data.remoteTrailers?.firstOrNull()?.url,
|
||||
isPlayed = data.userData?.played == true,
|
||||
isFavorite = data.userData?.isFavorite == true,
|
||||
media = if (data.type == MOVIE.type) {
|
||||
State.Movie(
|
||||
title = baseItemDto.name.orEmpty()
|
||||
title = data.name.orEmpty()
|
||||
)
|
||||
} else {
|
||||
State.TvShow(
|
||||
episode = baseItemDto.episodeTitle ?: baseItemDto.name.orEmpty(),
|
||||
show = baseItemDto.seriesName.orEmpty()
|
||||
episode = data.episodeTitle ?: data.name.orEmpty(),
|
||||
show = data.seriesName.orEmpty()
|
||||
)
|
||||
}
|
||||
).also(transformed)
|
||||
}
|
||||
}
|
||||
|
||||
data class State(
|
||||
val dto: BaseItemDto,
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
|||
import android.net.Uri
|
||||
import android.os.Environment
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.fragment.app.Fragment
|
||||
import dev.jdtech.jellyfin.models.DownloadMetadata
|
||||
import dev.jdtech.jellyfin.models.DownloadRequestItem
|
||||
import dev.jdtech.jellyfin.models.PlayerItem
|
||||
|
@ -18,7 +17,7 @@ import java.util.UUID
|
|||
|
||||
var defaultStorage: File? = null
|
||||
|
||||
fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context: Fragment) {
|
||||
fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context: Context) {
|
||||
val downloadRequest = DownloadManager.Request(uri)
|
||||
.setTitle(downloadRequestItem.metadata.name)
|
||||
.setDescription("Downloading")
|
||||
|
@ -32,7 +31,7 @@ fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context:
|
|||
)
|
||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
if (!File(defaultStorage, downloadRequestItem.itemId.toString()).exists())
|
||||
downloadFile(downloadRequest, context.requireContext())
|
||||
downloadFile(downloadRequest, context)
|
||||
createMetadataFile(
|
||||
downloadRequestItem.metadata,
|
||||
downloadRequestItem.itemId)
|
||||
|
|
|
@ -1,50 +1,47 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.*
|
||||
import dev.jdtech.jellyfin.models.DownloadSection
|
||||
import dev.jdtech.jellyfin.utils.loadDownloadedEpisodes
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
|
||||
class DownloadViewModel : ViewModel() {
|
||||
private val _downloadSections = MutableLiveData<List<DownloadSection>>()
|
||||
val downloadSections: LiveData<List<DownloadSection>> = _downloadSections
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
|
||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
||||
sealed class UiState {
|
||||
data class Normal(val downloadSections: List<DownloadSection>) : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String?) : UiState()
|
||||
}
|
||||
|
||||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
init {
|
||||
loadData()
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
fun loadData() {
|
||||
_error.value = null
|
||||
_finishedLoading.value = false
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
val items = loadDownloadedEpisodes()
|
||||
if (items.isEmpty()) {
|
||||
_downloadSections.value = listOf()
|
||||
_finishedLoading.value = true
|
||||
uiState.emit(UiState.Normal(emptyList()))
|
||||
return@launch
|
||||
}
|
||||
val tempDownloadSections = mutableListOf<DownloadSection>()
|
||||
val downloadSections = mutableListOf<DownloadSection>()
|
||||
withContext(Dispatchers.Default) {
|
||||
DownloadSection(
|
||||
UUID.randomUUID(),
|
||||
"Episodes",
|
||||
items.filter { it.metadata?.type == "Episode"}).let {
|
||||
if (it.items.isNotEmpty()) tempDownloadSections.add(
|
||||
items.filter { it.metadata?.type == "Episode" }).let {
|
||||
if (it.items.isNotEmpty()) downloadSections.add(
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@ -52,17 +49,15 @@ class DownloadViewModel : ViewModel() {
|
|||
UUID.randomUUID(),
|
||||
"Movies",
|
||||
items.filter { it.metadata?.type == "Movie" }).let {
|
||||
if (it.items.isNotEmpty()) tempDownloadSections.add(
|
||||
if (it.items.isNotEmpty()) downloadSections.add(
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
_downloadSections.value = tempDownloadSections
|
||||
uiState.emit(UiState.Normal(downloadSections))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.toString()
|
||||
uiState.emit(UiState.Error(e.message))
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,23 +1,18 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.*
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.jdtech.jellyfin.models.DownloadMetadata
|
||||
import dev.jdtech.jellyfin.models.DownloadRequestItem
|
||||
import dev.jdtech.jellyfin.models.PlayerItem
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import dev.jdtech.jellyfin.utils.baseItemDtoToDownloadMetadata
|
||||
import dev.jdtech.jellyfin.utils.deleteDownloadedEpisode
|
||||
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
|
||||
import dev.jdtech.jellyfin.utils.itemIsDownloaded
|
||||
import dev.jdtech.jellyfin.utils.*
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.LocationType
|
||||
import timber.log.Timber
|
||||
import java.text.DateFormat
|
||||
import java.time.ZoneOffset
|
||||
|
@ -29,91 +24,126 @@ import javax.inject.Inject
|
|||
class EpisodeBottomSheetViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val jellyfinRepository: JellyfinRepository
|
||||
) : ViewModel() {
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
|
||||
private val _item = MutableLiveData<BaseItemDto>()
|
||||
val item: LiveData<BaseItemDto> = _item
|
||||
sealed class UiState {
|
||||
data class Normal(
|
||||
val episode: BaseItemDto,
|
||||
val runTime: String,
|
||||
val dateString: String,
|
||||
val played: Boolean,
|
||||
val favorite: Boolean,
|
||||
val downloaded: Boolean,
|
||||
val downloadEpisode: Boolean,
|
||||
) : UiState()
|
||||
|
||||
private val _runTime = MutableLiveData<String>()
|
||||
val runTime: LiveData<String> = _runTime
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String?) : UiState()
|
||||
}
|
||||
|
||||
private val _dateString = MutableLiveData<String>()
|
||||
val dateString: LiveData<String> = _dateString
|
||||
|
||||
private val _played = MutableLiveData<Boolean>()
|
||||
val played: LiveData<Boolean> = _played
|
||||
|
||||
private val _favorite = MutableLiveData<Boolean>()
|
||||
val favorite: LiveData<Boolean> = _favorite
|
||||
|
||||
private val _downloaded = MutableLiveData<Boolean>()
|
||||
val downloaded: LiveData<Boolean> = _downloaded
|
||||
|
||||
private val _downloadEpisode = MutableLiveData<Boolean>()
|
||||
val downloadEpisode: LiveData<Boolean> = _downloadEpisode
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
var item: BaseItemDto? = null
|
||||
var runTime: String = ""
|
||||
var dateString: String = ""
|
||||
var played: Boolean = false
|
||||
var favorite: Boolean = false
|
||||
var downloaded: Boolean = false
|
||||
var downloadEpisode: Boolean = false
|
||||
var playerItems: MutableList<PlayerItem> = mutableListOf()
|
||||
|
||||
lateinit var downloadRequestItem: DownloadRequestItem
|
||||
|
||||
fun loadEpisode(episodeId: UUID) {
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
_downloaded.value = itemIsDownloaded(episodeId)
|
||||
val item = jellyfinRepository.getItem(episodeId)
|
||||
_item.value = item
|
||||
_runTime.value = "${item.runTimeTicks?.div(600000000)} min"
|
||||
_dateString.value = getDateString(item)
|
||||
_played.value = item.userData?.played
|
||||
_favorite.value = item.userData?.isFavorite
|
||||
val tempItem = jellyfinRepository.getItem(episodeId)
|
||||
item = tempItem
|
||||
runTime = "${tempItem.runTimeTicks?.div(600000000)} min"
|
||||
dateString = getDateString(tempItem)
|
||||
played = tempItem.userData?.played == true
|
||||
favorite = tempItem.userData?.isFavorite == true
|
||||
downloaded = itemIsDownloaded(episodeId)
|
||||
uiState.emit(
|
||||
UiState.Normal(
|
||||
tempItem,
|
||||
runTime,
|
||||
dateString,
|
||||
played,
|
||||
favorite,
|
||||
downloaded,
|
||||
downloadEpisode
|
||||
)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
uiState.emit(UiState.Error(e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadEpisode(playerItem : PlayerItem){
|
||||
fun loadEpisode(playerItem: PlayerItem) {
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
playerItems.add(playerItem)
|
||||
_item.value = downloadMetadataToBaseItemDto(playerItem.metadata!!)
|
||||
item = downloadMetadataToBaseItemDto(playerItem.metadata!!)
|
||||
uiState.emit(
|
||||
UiState.Normal(
|
||||
item!!,
|
||||
runTime,
|
||||
dateString,
|
||||
played,
|
||||
favorite,
|
||||
downloaded,
|
||||
downloadEpisode
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun markAsPlayed(itemId: UUID) {
|
||||
viewModelScope.launch {
|
||||
jellyfinRepository.markAsPlayed(itemId)
|
||||
}
|
||||
_played.value = true
|
||||
played = true
|
||||
}
|
||||
|
||||
fun markAsUnplayed(itemId: UUID) {
|
||||
viewModelScope.launch {
|
||||
jellyfinRepository.markAsUnplayed(itemId)
|
||||
}
|
||||
_played.value = false
|
||||
played = false
|
||||
}
|
||||
|
||||
fun markAsFavorite(itemId: UUID) {
|
||||
viewModelScope.launch {
|
||||
jellyfinRepository.markAsFavorite(itemId)
|
||||
}
|
||||
_favorite.value = true
|
||||
favorite = true
|
||||
}
|
||||
|
||||
fun unmarkAsFavorite(itemId: UUID) {
|
||||
viewModelScope.launch {
|
||||
jellyfinRepository.unmarkAsFavorite(itemId)
|
||||
}
|
||||
_favorite.value = false
|
||||
favorite = false
|
||||
}
|
||||
|
||||
fun loadDownloadRequestItem(itemId: UUID) {
|
||||
viewModelScope.launch {
|
||||
loadEpisode(itemId)
|
||||
val episode = _item.value
|
||||
//loadEpisode(itemId)
|
||||
val episode = item
|
||||
val uri = jellyfinRepository.getStreamUrl(itemId, episode?.mediaSources?.get(0)?.id!!)
|
||||
Timber.d(uri)
|
||||
val metadata = baseItemDtoToDownloadMetadata(episode)
|
||||
downloadRequestItem = DownloadRequestItem(uri, itemId, metadata)
|
||||
_downloadEpisode.value = true
|
||||
downloadEpisode = true
|
||||
requestDownload(Uri.parse(downloadRequestItem.uri), downloadRequestItem, application)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,7 +163,7 @@ constructor(
|
|||
}
|
||||
|
||||
fun doneDownloadEpisode() {
|
||||
_downloadEpisode.value = false
|
||||
_downloaded.value = true
|
||||
downloadEpisode = false
|
||||
downloaded = true
|
||||
}
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.jdtech.jellyfin.models.FavoriteSection
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -20,40 +20,41 @@ class FavoriteViewModel
|
|||
constructor(
|
||||
private val jellyfinRepository: JellyfinRepository
|
||||
) : ViewModel() {
|
||||
private val _favoriteSections = MutableLiveData<List<FavoriteSection>>()
|
||||
val favoriteSections: LiveData<List<FavoriteSection>> = _favoriteSections
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
|
||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
||||
sealed class UiState {
|
||||
data class Normal(val favoriteSections: List<FavoriteSection>) : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String?) : UiState()
|
||||
}
|
||||
|
||||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
init {
|
||||
loadData()
|
||||
}
|
||||
|
||||
fun loadData() {
|
||||
_error.value = null
|
||||
_finishedLoading.value = false
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
val items = jellyfinRepository.getFavoriteItems()
|
||||
|
||||
if (items.isEmpty()) {
|
||||
_favoriteSections.value = listOf()
|
||||
_finishedLoading.value = true
|
||||
uiState.emit(UiState.Normal(emptyList()))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val tempFavoriteSections = mutableListOf<FavoriteSection>()
|
||||
val favoriteSections = mutableListOf<FavoriteSection>()
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
FavoriteSection(
|
||||
UUID.randomUUID(),
|
||||
"Movies",
|
||||
items.filter { it.type == "Movie" }).let {
|
||||
if (it.items.isNotEmpty()) tempFavoriteSections.add(
|
||||
if (it.items.isNotEmpty()) favoriteSections.add(
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@ -61,7 +62,7 @@ constructor(
|
|||
UUID.randomUUID(),
|
||||
"Shows",
|
||||
items.filter { it.type == "Series" }).let {
|
||||
if (it.items.isNotEmpty()) tempFavoriteSections.add(
|
||||
if (it.items.isNotEmpty()) favoriteSections.add(
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@ -69,18 +70,16 @@ constructor(
|
|||
UUID.randomUUID(),
|
||||
"Episodes",
|
||||
items.filter { it.type == "Episode" }).let {
|
||||
if (it.items.isNotEmpty()) tempFavoriteSections.add(
|
||||
if (it.items.isNotEmpty()) favoriteSections.add(
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_favoriteSections.value = tempFavoriteSections
|
||||
uiState.emit(UiState.Normal(favoriteSections))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.toString()
|
||||
uiState.emit(UiState.Error(e.message))
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,8 +2,6 @@ package dev.jdtech.jellyfin.viewmodels
|
|||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
@ -17,62 +15,49 @@ import dev.jdtech.jellyfin.repository.JellyfinRepository
|
|||
import dev.jdtech.jellyfin.utils.syncPlaybackProgress
|
||||
import dev.jdtech.jellyfin.utils.toView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject internal constructor(
|
||||
application: Application,
|
||||
private val application: Application,
|
||||
private val repository: JellyfinRepository
|
||||
) : ViewModel() {
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
|
||||
private val views = MutableLiveData<List<HomeItem>>()
|
||||
private val state = MutableSharedFlow<State>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
sealed class UiState {
|
||||
data class Normal(val homeItems: List<HomeItem>) : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String?) : UiState()
|
||||
}
|
||||
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
init {
|
||||
loadData(updateCapabilities = true)
|
||||
}
|
||||
|
||||
private val continueWatchingString = application.resources.getString(R.string.continue_watching)
|
||||
private val nextUpString = application.resources.getString(R.string.next_up)
|
||||
|
||||
fun views(): LiveData<List<HomeItem>> = views
|
||||
|
||||
fun onStateUpdate(
|
||||
scope: LifecycleCoroutineScope,
|
||||
collector: (State) -> Unit
|
||||
) {
|
||||
scope.launch { state.collect { collector(it) } }
|
||||
}
|
||||
|
||||
fun refreshData() = loadData(updateCapabilities = false)
|
||||
|
||||
private fun loadData(updateCapabilities: Boolean) {
|
||||
state.tryEmit(Loading(inProgress = true))
|
||||
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
if (updateCapabilities) repository.postCapabilities()
|
||||
|
||||
val updated = loadDynamicItems() + loadViews()
|
||||
views.postValue(updated)
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
syncPlaybackProgress(repository)
|
||||
}
|
||||
state.tryEmit(Loading(inProgress = false))
|
||||
uiState.emit(UiState.Normal(updated))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
state.tryEmit(LoadingError(e.toString()))
|
||||
uiState.emit(UiState.Error(e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,11 +68,11 @@ class HomeViewModel @Inject internal constructor(
|
|||
|
||||
val items = mutableListOf<HomeSection>()
|
||||
if (resumeItems.isNotEmpty()) {
|
||||
items.add(HomeSection(continueWatchingString, resumeItems))
|
||||
items.add(HomeSection(application.resources.getString(R.string.continue_watching), resumeItems))
|
||||
}
|
||||
|
||||
if (nextUpItems.isNotEmpty()) {
|
||||
items.add(HomeSection(nextUpString, nextUpItems))
|
||||
items.add(HomeSection(application.resources.getString(R.string.next_up), nextUpItems))
|
||||
}
|
||||
|
||||
items.map { Section(it) }
|
||||
|
@ -102,11 +87,6 @@ class HomeViewModel @Inject internal constructor(
|
|||
.map { (view, latest) -> view.toView().apply { items = latest } }
|
||||
.map { ViewItem(it) }
|
||||
}
|
||||
|
||||
sealed class State
|
||||
|
||||
data class LoadingError(val message: String) : State()
|
||||
data class Loading(val inProgress: Boolean) : State()
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import androidx.lifecycle.*
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import dev.jdtech.jellyfin.utils.SortBy
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
|
@ -14,16 +16,20 @@ import javax.inject.Inject
|
|||
@HiltViewModel
|
||||
class LibraryViewModel
|
||||
@Inject
|
||||
constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||
constructor(
|
||||
private val jellyfinRepository: JellyfinRepository
|
||||
) : ViewModel() {
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
|
||||
private val _items = MutableLiveData<List<BaseItemDto>>()
|
||||
val items: LiveData<List<BaseItemDto>> = _items
|
||||
sealed class UiState {
|
||||
data class Normal(val items: List<BaseItemDto>) : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String?) : UiState()
|
||||
}
|
||||
|
||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
||||
|
||||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
fun loadItems(
|
||||
parentId: UUID,
|
||||
|
@ -31,8 +37,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
sortBy: SortBy = SortBy.defaultValue,
|
||||
sortOrder: SortOrder = SortOrder.ASCENDING
|
||||
) {
|
||||
_error.value = null
|
||||
_finishedLoading.value = false
|
||||
Timber.d("$libraryType")
|
||||
val itemType = when (libraryType) {
|
||||
"movies" -> "Movie"
|
||||
|
@ -40,19 +44,19 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
else -> null
|
||||
}
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
_items.value = jellyfinRepository.getItems(
|
||||
val items = jellyfinRepository.getItems(
|
||||
parentId,
|
||||
includeTypes = if (itemType != null) listOf(itemType) else null,
|
||||
recursive = true,
|
||||
sortBy = sortBy,
|
||||
sortOrder = sortOrder
|
||||
)
|
||||
uiState.emit(UiState.Normal(items))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.toString()
|
||||
uiState.emit(UiState.Error(e.message))
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import android.app.Application
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
@ -13,7 +14,10 @@ import dev.jdtech.jellyfin.utils.baseItemDtoToDownloadMetadata
|
|||
import dev.jdtech.jellyfin.utils.deleteDownloadedEpisode
|
||||
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
|
||||
import dev.jdtech.jellyfin.utils.itemIsDownloaded
|
||||
import dev.jdtech.jellyfin.utils.requestDownload
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
|
@ -25,92 +29,134 @@ import javax.inject.Inject
|
|||
@HiltViewModel
|
||||
class MediaInfoViewModel
|
||||
@Inject
|
||||
constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||
constructor(
|
||||
private val application: Application,
|
||||
private val jellyfinRepository: JellyfinRepository
|
||||
) : ViewModel() {
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
|
||||
private val _item = MutableLiveData<BaseItemDto>()
|
||||
val item: LiveData<BaseItemDto> = _item
|
||||
sealed class UiState {
|
||||
data class Normal(
|
||||
val item: BaseItemDto,
|
||||
val actors: List<BaseItemPerson>,
|
||||
val director: BaseItemPerson?,
|
||||
val writers: List<BaseItemPerson>,
|
||||
val writersString: String,
|
||||
val genresString: String,
|
||||
val runTime: String,
|
||||
val dateString: String,
|
||||
val nextUp: BaseItemDto?,
|
||||
val seasons: List<BaseItemDto>,
|
||||
val played: Boolean,
|
||||
val favorite: Boolean,
|
||||
val downloaded: Boolean,
|
||||
) : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String?) : UiState()
|
||||
}
|
||||
|
||||
private val _actors = MutableLiveData<List<BaseItemPerson>>()
|
||||
val actors: LiveData<List<BaseItemPerson>> = _actors
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
private val _director = MutableLiveData<BaseItemPerson>()
|
||||
val director: LiveData<BaseItemPerson> = _director
|
||||
var item: BaseItemDto? = null
|
||||
private var actors: List<BaseItemPerson> = emptyList()
|
||||
private var director: BaseItemPerson? = null
|
||||
private var writers: List<BaseItemPerson> = emptyList()
|
||||
private var writersString: String = ""
|
||||
private var genresString: String = ""
|
||||
private var runTime: String = ""
|
||||
private var dateString: String = ""
|
||||
var nextUp: BaseItemDto? = null
|
||||
var seasons: List<BaseItemDto> = emptyList()
|
||||
var played: Boolean = false
|
||||
var favorite: Boolean = false
|
||||
private var downloaded: Boolean = false
|
||||
private var downloadMedia: Boolean = false
|
||||
|
||||
private val _writers = MutableLiveData<List<BaseItemPerson>>()
|
||||
val writers: LiveData<List<BaseItemPerson>> = _writers
|
||||
private val _writersString = MutableLiveData<String>()
|
||||
val writersString: LiveData<String> = _writersString
|
||||
|
||||
private val _genresString = MutableLiveData<String>()
|
||||
val genresString: LiveData<String> = _genresString
|
||||
|
||||
private val _runTime = MutableLiveData<String>()
|
||||
val runTime: LiveData<String> = _runTime
|
||||
|
||||
private val _dateString = MutableLiveData<String>()
|
||||
val dateString: LiveData<String> = _dateString
|
||||
|
||||
private val _nextUp = MutableLiveData<BaseItemDto>()
|
||||
val nextUp: LiveData<BaseItemDto> = _nextUp
|
||||
|
||||
private val _seasons = MutableLiveData<List<BaseItemDto>>()
|
||||
val seasons: LiveData<List<BaseItemDto>> = _seasons
|
||||
|
||||
private val _played = MutableLiveData<Boolean>()
|
||||
val played: LiveData<Boolean> = _played
|
||||
|
||||
private val _favorite = MutableLiveData<Boolean>()
|
||||
val favorite: LiveData<Boolean> = _favorite
|
||||
|
||||
private val _downloaded = MutableLiveData<Boolean>()
|
||||
val downloaded: LiveData<Boolean> = _downloaded
|
||||
|
||||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
|
||||
private val _downloadMedia = MutableLiveData<Boolean>()
|
||||
val downloadMedia: LiveData<Boolean> = _downloadMedia
|
||||
|
||||
lateinit var downloadRequestItem: DownloadRequestItem
|
||||
private lateinit var downloadRequestItem: DownloadRequestItem
|
||||
|
||||
lateinit var playerItem: PlayerItem
|
||||
|
||||
fun loadData(itemId: UUID, itemType: String) {
|
||||
_error.value = null
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
_downloaded.value = itemIsDownloaded(itemId)
|
||||
_item.value = jellyfinRepository.getItem(itemId)
|
||||
_actors.value = getActors(_item.value!!)
|
||||
_director.value = getDirector(_item.value!!)
|
||||
_writers.value = getWriters(_item.value!!)
|
||||
_writersString.value =
|
||||
_writers.value?.joinToString(separator = ", ") { it.name.toString() }
|
||||
_genresString.value = _item.value?.genres?.joinToString(separator = ", ")
|
||||
_runTime.value = "${_item.value?.runTimeTicks?.div(600000000)} min"
|
||||
_dateString.value = getDateString(_item.value!!)
|
||||
_played.value = _item.value?.userData?.played
|
||||
_favorite.value = _item.value?.userData?.isFavorite
|
||||
if (itemType == "Series" || itemType == "Episode") {
|
||||
_nextUp.value = getNextUp(itemId)
|
||||
_seasons.value = jellyfinRepository.getSeasons(itemId)
|
||||
val tempItem = jellyfinRepository.getItem(itemId)
|
||||
item = tempItem
|
||||
actors = getActors(tempItem)
|
||||
director = getDirector(tempItem)
|
||||
writers = getWriters(tempItem)
|
||||
writersString = writers.joinToString(separator = ", ") { it.name.toString() }
|
||||
genresString = tempItem.genres?.joinToString(separator = ", ") ?: ""
|
||||
runTime = "${tempItem.runTimeTicks?.div(600000000)} min"
|
||||
dateString = getDateString(tempItem)
|
||||
played = tempItem.userData?.played ?: false
|
||||
favorite = tempItem.userData?.isFavorite ?: false
|
||||
downloaded = itemIsDownloaded(itemId)
|
||||
if (itemType == "Series") {
|
||||
nextUp = getNextUp(itemId)
|
||||
seasons = jellyfinRepository.getSeasons(itemId)
|
||||
}
|
||||
uiState.emit(UiState.Normal(
|
||||
tempItem,
|
||||
actors,
|
||||
director,
|
||||
writers,
|
||||
writersString,
|
||||
genresString,
|
||||
runTime,
|
||||
dateString,
|
||||
nextUp,
|
||||
seasons,
|
||||
played,
|
||||
favorite,
|
||||
downloaded
|
||||
))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.toString()
|
||||
Timber.d(e)
|
||||
Timber.d(itemId.toString())
|
||||
uiState.emit(UiState.Error(e.message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadData(playerItem: PlayerItem) {
|
||||
this.playerItem = playerItem
|
||||
_item.value = downloadMetadataToBaseItemDto(playerItem.metadata!!)
|
||||
fun loadData(pItem: PlayerItem) {
|
||||
viewModelScope.launch {
|
||||
playerItem = pItem
|
||||
val tempItem = downloadMetadataToBaseItemDto(playerItem.metadata!!)
|
||||
item = tempItem
|
||||
actors = getActors(tempItem)
|
||||
director = getDirector(tempItem)
|
||||
writers = getWriters(tempItem)
|
||||
writersString = writers.joinToString(separator = ", ") { it.name.toString() }
|
||||
genresString = tempItem.genres?.joinToString(separator = ", ") ?: ""
|
||||
runTime = ""
|
||||
dateString = ""
|
||||
played = tempItem.userData?.played ?: false
|
||||
favorite = tempItem.userData?.isFavorite ?: false
|
||||
uiState.emit(UiState.Normal(
|
||||
tempItem,
|
||||
actors,
|
||||
director,
|
||||
writers,
|
||||
writersString,
|
||||
genresString,
|
||||
runTime,
|
||||
dateString,
|
||||
nextUp,
|
||||
seasons,
|
||||
played,
|
||||
favorite,
|
||||
downloaded
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getActors(item: BaseItemDto): List<BaseItemPerson>? {
|
||||
val actors: List<BaseItemPerson>?
|
||||
private suspend fun getActors(item: BaseItemDto): List<BaseItemPerson> {
|
||||
val actors: List<BaseItemPerson>
|
||||
withContext(Dispatchers.Default) {
|
||||
actors = item.people?.filter { it.type == "Actor" }
|
||||
actors = item.people?.filter { it.type == "Actor" } ?: emptyList()
|
||||
}
|
||||
return actors
|
||||
}
|
||||
|
@ -123,10 +169,10 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
return director
|
||||
}
|
||||
|
||||
private suspend fun getWriters(item: BaseItemDto): List<BaseItemPerson>? {
|
||||
val writers: List<BaseItemPerson>?
|
||||
private suspend fun getWriters(item: BaseItemDto): List<BaseItemPerson> {
|
||||
val writers: List<BaseItemPerson>
|
||||
withContext(Dispatchers.Default) {
|
||||
writers = item.people?.filter { it.type == "Writer" }
|
||||
writers = item.people?.filter { it.type == "Writer" } ?: emptyList()
|
||||
}
|
||||
return writers
|
||||
}
|
||||
|
@ -144,28 +190,28 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
viewModelScope.launch {
|
||||
jellyfinRepository.markAsPlayed(itemId)
|
||||
}
|
||||
_played.value = true
|
||||
played = true
|
||||
}
|
||||
|
||||
fun markAsUnplayed(itemId: UUID) {
|
||||
viewModelScope.launch {
|
||||
jellyfinRepository.markAsUnplayed(itemId)
|
||||
}
|
||||
_played.value = false
|
||||
played = false
|
||||
}
|
||||
|
||||
fun markAsFavorite(itemId: UUID) {
|
||||
viewModelScope.launch {
|
||||
jellyfinRepository.markAsFavorite(itemId)
|
||||
}
|
||||
_favorite.value = true
|
||||
favorite = true
|
||||
}
|
||||
|
||||
fun unmarkAsFavorite(itemId: UUID) {
|
||||
viewModelScope.launch {
|
||||
jellyfinRepository.unmarkAsFavorite(itemId)
|
||||
}
|
||||
_favorite.value = false
|
||||
favorite = false
|
||||
}
|
||||
|
||||
private fun getDateString(item: BaseItemDto): String {
|
||||
|
@ -191,21 +237,17 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
|||
|
||||
fun loadDownloadRequestItem(itemId: UUID) {
|
||||
viewModelScope.launch {
|
||||
val downloadItem = _item.value
|
||||
val downloadItem = item
|
||||
val uri =
|
||||
jellyfinRepository.getStreamUrl(itemId, downloadItem?.mediaSources?.get(0)?.id!!)
|
||||
val metadata = baseItemDtoToDownloadMetadata(downloadItem)
|
||||
downloadRequestItem = DownloadRequestItem(uri, itemId, metadata)
|
||||
_downloadMedia.value = true
|
||||
downloadMedia = true
|
||||
requestDownload(Uri.parse(downloadRequestItem.uri), downloadRequestItem, application)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteItem() {
|
||||
deleteDownloadedEpisode(playerItem.mediaSourceUri)
|
||||
}
|
||||
|
||||
fun doneDownloadMedia() {
|
||||
_downloadMedia.value = false
|
||||
_downloaded.value = true
|
||||
}
|
||||
}
|
|
@ -2,10 +2,12 @@ package dev.jdtech.jellyfin.viewmodels
|
|||
|
||||
import androidx.lifecycle.*
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.jdtech.jellyfin.models.unsupportedCollections
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
|
@ -15,38 +17,35 @@ constructor(
|
|||
private val jellyfinRepository: JellyfinRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _collections = MutableLiveData<List<BaseItemDto>>()
|
||||
val collections: LiveData<List<BaseItemDto>> = _collections
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
|
||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
||||
sealed class UiState {
|
||||
data class Normal(val collections: List<BaseItemDto>) : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String?) : UiState()
|
||||
}
|
||||
|
||||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
init {
|
||||
loadData()
|
||||
}
|
||||
|
||||
fun loadData() {
|
||||
_finishedLoading.value = false
|
||||
_error.value = null
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
val items = jellyfinRepository.getItems()
|
||||
_collections.value =
|
||||
items.filter {
|
||||
it.collectionType != "homevideos" &&
|
||||
it.collectionType != "music" &&
|
||||
it.collectionType != "playlists" &&
|
||||
it.collectionType != "boxsets" &&
|
||||
it.collectionType != "books"
|
||||
}
|
||||
val collections =
|
||||
items.filter { collection -> unsupportedCollections().none { it.type == collection.collectionType } }
|
||||
uiState.emit(UiState.Normal(collections))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.toString()
|
||||
uiState.emit(
|
||||
UiState.Error(e.message)
|
||||
)
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
@ -9,9 +8,10 @@ import dev.jdtech.jellyfin.models.ContentType.MOVIE
|
|||
import dev.jdtech.jellyfin.models.ContentType.TVSHOW
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import dev.jdtech.jellyfin.utils.contentType
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import timber.log.Timber
|
||||
import java.lang.Exception
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
@ -21,29 +21,30 @@ internal class PersonDetailViewModel @Inject internal constructor(
|
|||
private val jellyfinRepository: JellyfinRepository
|
||||
) : ViewModel() {
|
||||
|
||||
val data = MutableLiveData<PersonOverview>()
|
||||
val starredIn = MutableLiveData<StarredIn>()
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
|
||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
||||
sealed class UiState {
|
||||
data class Normal(val data: PersonOverview, val starredIn: StarredIn) : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String?) : UiState()
|
||||
}
|
||||
|
||||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
fun loadData(personId: UUID) {
|
||||
_error.value = null
|
||||
_finishedLoading.value = false
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
val personDetail = jellyfinRepository.getItem(personId)
|
||||
|
||||
data.postValue(
|
||||
PersonOverview(
|
||||
val data = PersonOverview(
|
||||
name = personDetail.name.orEmpty(),
|
||||
overview = personDetail.overview.orEmpty(),
|
||||
dto = personDetail
|
||||
)
|
||||
)
|
||||
|
||||
val items = jellyfinRepository.getPersonItems(
|
||||
personIds = listOf(personId),
|
||||
includeTypes = listOf(MOVIE, TVSHOW),
|
||||
|
@ -53,13 +54,12 @@ internal class PersonDetailViewModel @Inject internal constructor(
|
|||
val movies = items.filter { it.contentType() == MOVIE }
|
||||
val shows = items.filter { it.contentType() == TVSHOW }
|
||||
|
||||
starredIn.postValue(StarredIn(movies, shows))
|
||||
val starredIn = StarredIn(movies, shows)
|
||||
|
||||
uiState.emit(UiState.Normal(data, starredIn))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.toString()
|
||||
uiState.emit(UiState.Error(e.message))
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.jdtech.jellyfin.models.FavoriteSection
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -20,36 +20,37 @@ class SearchResultViewModel
|
|||
constructor(
|
||||
private val jellyfinRepository: JellyfinRepository
|
||||
) : ViewModel() {
|
||||
private val _sections = MutableLiveData<List<FavoriteSection>>()
|
||||
val sections: LiveData<List<FavoriteSection>> = _sections
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
|
||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
||||
sealed class UiState {
|
||||
data class Normal(val sections: List<FavoriteSection>) : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String?) : UiState()
|
||||
}
|
||||
|
||||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
fun loadData(query: String) {
|
||||
_error.value = null
|
||||
_finishedLoading.value = false
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
val items = jellyfinRepository.getSearchItems(query)
|
||||
|
||||
if (items.isEmpty()) {
|
||||
_sections.value = listOf()
|
||||
_finishedLoading.value = true
|
||||
uiState.emit(UiState.Normal(emptyList()))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val tempSections = mutableListOf<FavoriteSection>()
|
||||
val sections = mutableListOf<FavoriteSection>()
|
||||
|
||||
withContext(Dispatchers.Default) {
|
||||
FavoriteSection(
|
||||
UUID.randomUUID(),
|
||||
"Movies",
|
||||
items.filter { it.type == "Movie" }).let {
|
||||
if (it.items.isNotEmpty()) tempSections.add(
|
||||
if (it.items.isNotEmpty()) sections.add(
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@ -57,7 +58,7 @@ constructor(
|
|||
UUID.randomUUID(),
|
||||
"Shows",
|
||||
items.filter { it.type == "Series" }).let {
|
||||
if (it.items.isNotEmpty()) tempSections.add(
|
||||
if (it.items.isNotEmpty()) sections.add(
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@ -65,18 +66,16 @@ constructor(
|
|||
UUID.randomUUID(),
|
||||
"Episodes",
|
||||
items.filter { it.type == "Episode" }).let {
|
||||
if (it.items.isNotEmpty()) tempSections.add(
|
||||
if (it.items.isNotEmpty()) sections.add(
|
||||
it
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_sections.value = tempSections
|
||||
uiState.emit(UiState.Normal(sections))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.toString()
|
||||
uiState.emit(UiState.Error(e.message))
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,48 +1,49 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.*
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dev.jdtech.jellyfin.adapters.EpisodeItem
|
||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SeasonViewModel
|
||||
@Inject
|
||||
constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||
constructor(
|
||||
private val jellyfinRepository: JellyfinRepository
|
||||
) : ViewModel() {
|
||||
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||
|
||||
private val _episodes = MutableLiveData<List<EpisodeItem>>()
|
||||
val episodes: LiveData<List<EpisodeItem>> = _episodes
|
||||
sealed class UiState {
|
||||
data class Normal(val episodes: List<EpisodeItem>) : UiState()
|
||||
object Loading : UiState()
|
||||
data class Error(val message: String?) : UiState()
|
||||
}
|
||||
|
||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
||||
|
||||
private val _error = MutableLiveData<String>()
|
||||
val error: LiveData<String> = _error
|
||||
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||
scope.launch { uiState.collect { collector(it) } }
|
||||
}
|
||||
|
||||
fun loadEpisodes(seriesId: UUID, seasonId: UUID) {
|
||||
_error.value = null
|
||||
_finishedLoading.value = false
|
||||
viewModelScope.launch {
|
||||
uiState.emit(UiState.Loading)
|
||||
try {
|
||||
_episodes.value = getEpisodes(seriesId, seasonId)
|
||||
val episodes = getEpisodes(seriesId, seasonId)
|
||||
uiState.emit(UiState.Normal(episodes))
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
_error.value = e.toString()
|
||||
uiState.emit(UiState.Error(e.message))
|
||||
}
|
||||
_finishedLoading.value = true
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getEpisodes(seriesId: UUID, seasonId: UUID): List<EpisodeItem> {
|
||||
val episodes = jellyfinRepository.getEpisodes(seriesId, seasonId, fields = listOf(ItemFields.OVERVIEW))
|
||||
val episodes =
|
||||
jellyfinRepository.getEpisodes(seriesId, seasonId, fields = listOf(ItemFields.OVERVIEW))
|
||||
return listOf(EpisodeItem.Header) + episodes.map { EpisodeItem.Episode(it) }
|
||||
}
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
package dev.jdtech.jellyfin.viewmodels
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
|
@ -10,6 +9,9 @@ import dev.jdtech.jellyfin.api.JellyfinApi
|
|||
import dev.jdtech.jellyfin.database.Server
|
||||
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
@ -23,12 +25,17 @@ constructor(
|
|||
private val jellyfinApi: JellyfinApi,
|
||||
private val database: ServerDatabaseDao,
|
||||
) : ViewModel() {
|
||||
val servers = database.getAllServers()
|
||||
|
||||
private val _servers = database.getAllServers()
|
||||
val servers: LiveData<List<Server>> = _servers
|
||||
private val navigateToMain = MutableSharedFlow<Boolean>(
|
||||
replay = 0,
|
||||
extraBufferCapacity = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
|
||||
private val _navigateToMain = MutableLiveData<Boolean>()
|
||||
val navigateToMain: LiveData<Boolean> = _navigateToMain
|
||||
fun onNavigateToMain(scope: LifecycleCoroutineScope, collector: (Boolean) -> Unit) {
|
||||
scope.launch { navigateToMain.collect { collector(it) } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete server from database
|
||||
|
@ -54,10 +61,6 @@ constructor(
|
|||
userId = UUID.fromString(server.userId)
|
||||
}
|
||||
|
||||
_navigateToMain.value = true
|
||||
}
|
||||
|
||||
fun doneNavigatingToMain() {
|
||||
_navigateToMain.value = false
|
||||
navigateToMain.tryEmit(true)
|
||||
}
|
||||
}
|
|
@ -1,46 +1,23 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:ignore="MissingDefaultResource"
|
||||
>
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<variable
|
||||
name="item"
|
||||
type="androidx.lifecycle.LiveData<dev.jdtech.jellyfin.tv.ui.MediaDetailViewModel.State>"
|
||||
/>
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel"
|
||||
/>
|
||||
</data>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
>
|
||||
tools:ignore="MissingDefaultResource">
|
||||
|
||||
<include
|
||||
android:id="@+id/error_layout"
|
||||
layout="@layout/error_panel"
|
||||
tools:visibility="gone"
|
||||
/>
|
||||
tools:visibility="gone" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
>
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
>
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/back_button"
|
||||
|
@ -48,12 +25,11 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/transparent_circle_background"
|
||||
android:contentDescription="@string/player_controls_exit"
|
||||
android:focusable="true"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/ic_arrow_left"
|
||||
android:focusable="true"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
|
@ -65,20 +41,18 @@
|
|||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
app:layout_constraintStart_toEndOf="@id/back_button"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Alita: Battle Angel"
|
||||
/>
|
||||
tools:text="Alita: Battle Angel" />
|
||||
|
||||
<TextClock
|
||||
android:id="@+id/clock"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginEnd="24dp"
|
||||
tools:text="12:00"
|
||||
/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="12:00" />
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/poster"
|
||||
|
@ -86,10 +60,8 @@
|
|||
android:layout_height="180dp"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
android:scaleType="centerCrop"
|
||||
app:baseItemImage="@{item.dto}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
/>
|
||||
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/subtitle"
|
||||
|
@ -98,34 +70,30 @@
|
|||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toEndOf="@id/poster"
|
||||
app:layout_constraintTop_toBottomOf="@id/title"
|
||||
android:visibility="gone"
|
||||
tools:text="Subtitle"
|
||||
/>
|
||||
tools:text="Subtitle" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/genres"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
android:text="@{item.genres}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintStart_toEndOf="@id/poster"
|
||||
app:layout_constraintTop_toBottomOf="@id/subtitle"
|
||||
tools:text="Action, Science Fiction, Adventure"
|
||||
/>
|
||||
tools:text="Action, Science Fiction, Adventure" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/year"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||
app:layout_constraintStart_toEndOf="@id/poster"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{item.year}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintStart_toEndOf="@id/poster"
|
||||
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||
tools:text="2019" />
|
||||
|
||||
<TextView
|
||||
|
@ -133,11 +101,10 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||
app:layout_constraintStart_toEndOf="@id/year"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{item.runtimeMinutes}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintStart_toEndOf="@id/year"
|
||||
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||
tools:text="122 min" />
|
||||
|
||||
<TextView
|
||||
|
@ -145,11 +112,10 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||
app:layout_constraintStart_toEndOf="@id/playtime"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{item.officialRating}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintStart_toEndOf="@id/playtime"
|
||||
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||
tools:text="PG-13" />
|
||||
|
||||
<TextView
|
||||
|
@ -157,13 +123,12 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||
app:layout_constraintStart_toEndOf="@id/official_rating"
|
||||
android:drawablePadding="4dp"
|
||||
android:text="@{item.communityRating}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:drawableStart="@drawable/ic_star"
|
||||
android:drawableTint="@color/yellow"
|
||||
app:drawableStartCompat="@drawable/ic_star"
|
||||
app:drawableTint="@color/yellow"
|
||||
app:layout_constraintStart_toEndOf="@id/official_rating"
|
||||
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||
tools:text="7.3" />
|
||||
|
||||
<TextView
|
||||
|
@ -171,28 +136,26 @@
|
|||
android:layout_width="400dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@{item.description}"
|
||||
android:maxLines="5"
|
||||
android:ellipsize="end"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="5"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintStart_toEndOf="@id/poster"
|
||||
app:layout_constraintTop_toBottomOf="@id/year"
|
||||
tools:text="An angel falls. A warrior rises. When Alita awakens with no memory of who she is in a future world she does not recognize, she is taken in by Ido, a compassionate doctor who realizes that somewhere in this abandoned cyborg shell is the heart and soul of a young woman with an extraordinary past."
|
||||
/>
|
||||
tools:text="An angel falls. A warrior rises. When Alita awakens with no memory of who she is in a future world she does not recognize, she is taken in by Ido, a compassionate doctor who realizes that somewhere in this abandoned cyborg shell is the heart and soul of a young woman with an extraordinary past." />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_circular"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
app:layout_constraintStart_toEndOf="@id/poster"
|
||||
app:layout_constraintTop_toBottomOf="@id/description"
|
||||
android:elevation="8dp"
|
||||
android:indeterminateTint="@color/white"
|
||||
android:padding="8dp"
|
||||
android:visibility="invisible" />
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintStart_toEndOf="@id/poster"
|
||||
app:layout_constraintTop_toBottomOf="@id/description" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/play_button"
|
||||
|
@ -200,13 +163,12 @@
|
|||
android:layout_height="48dp"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
android:contentDescription="@string/play_button_description"
|
||||
android:focusable="true"
|
||||
android:paddingHorizontal="24dp"
|
||||
android:paddingVertical="12dp"
|
||||
android:src="@drawable/ic_play"
|
||||
android:focusable="true"
|
||||
app:layout_constraintStart_toEndOf="@id/poster"
|
||||
app:layout_constraintTop_toBottomOf="@id/description"
|
||||
/>
|
||||
app:layout_constraintTop_toBottomOf="@id/description" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/trailer_button"
|
||||
|
@ -214,12 +176,11 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:contentDescription="@string/trailer_button_description"
|
||||
android:focusable="true"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_film"
|
||||
android:focusable="true"
|
||||
app:layout_constraintStart_toEndOf="@id/play_button"
|
||||
app:layout_constraintTop_toBottomOf="@id/description"
|
||||
/>
|
||||
app:layout_constraintTop_toBottomOf="@id/description" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/check_button"
|
||||
|
@ -227,71 +188,63 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:contentDescription="@string/check_button_description"
|
||||
android:padding="12dp"
|
||||
android:focusable="true"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_check"
|
||||
app:layout_constraintStart_toEndOf="@id/trailer_button"
|
||||
app:layout_constraintTop_toBottomOf="@id/description"
|
||||
/>
|
||||
app:layout_constraintTop_toBottomOf="@id/description" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/favorite_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="12dp"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_heart"
|
||||
android:contentDescription="@string/favorite_button_description"
|
||||
android:focusable="true"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_heart"
|
||||
app:layout_constraintStart_toEndOf="@id/check_button"
|
||||
app:layout_constraintTop_toBottomOf="@id/description"
|
||||
/>
|
||||
app:layout_constraintTop_toBottomOf="@id/description" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/season_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/play_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:text="@string/seasons"
|
||||
android:visibility="gone"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="@string/seasons"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline3"
|
||||
/>
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/play_button" />
|
||||
|
||||
<androidx.leanback.widget.ListRowView
|
||||
android:id="@+id/seasons_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/season_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
/>
|
||||
app:layout_constraintTop_toBottomOf="@id/season_title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/cast_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/seasons_row"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:text="@string/cast_amp_crew"
|
||||
android:visibility="gone"
|
||||
android:layout_marginStart="@dimen/horizontal_margin"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/cast_amp_crew"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline3"
|
||||
/>
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/seasons_row" />
|
||||
|
||||
<androidx.leanback.widget.ListRowView
|
||||
android:id="@+id/cast_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/cast_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
/>
|
||||
app:layout_constraintTop_toBottomOf="@id/cast_title" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</layout>
|
||||
</FrameLayout>
|
|
@ -1,24 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<import type="org.jellyfin.sdk.model.api.LocationType" />
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingBottom="24dp">
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loading_indicator"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/holder"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -36,7 +32,6 @@
|
|||
android:layout_height="85dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:baseItemImage="@{viewModel.item}"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/holder"
|
||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Findroid.Image" />
|
||||
|
@ -49,9 +44,10 @@
|
|||
android:layout_marginEnd="8dp"
|
||||
android:background="@drawable/circle_background"
|
||||
android:backgroundTint="?attr/colorError"
|
||||
android:visibility="@{viewModel.item.locationType == LocationType.VIRTUAL ? View.VISIBLE : View.GONE}"
|
||||
app:layout_constraintEnd_toEndOf="@id/episode_image"
|
||||
app:layout_constraintTop_toTopOf="@id/episode_image">
|
||||
app:layout_constraintTop_toTopOf="@id/episode_image"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
|
@ -81,7 +77,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:text="@{String.format(@string/episode_name_extended, viewModel.item.parentIndexNumber, viewModel.item.indexNumber, viewModel.item.name)}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/episode_image"
|
||||
|
@ -104,7 +99,6 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{viewModel.dateString}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
tools:text="4/6/2013" />
|
||||
|
||||
|
@ -113,7 +107,6 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{viewModel.runTime}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
tools:text="26 min" />
|
||||
|
||||
|
@ -123,7 +116,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="4dp"
|
||||
android:gravity="bottom"
|
||||
android:text="@{viewModel.item.communityRating.toString()}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:drawableStartCompat="@drawable/ic_star"
|
||||
app:drawableTint="@color/yellow"
|
||||
|
@ -189,16 +181,32 @@
|
|||
android:padding="12dp"
|
||||
android:src="@drawable/ic_heart" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/download_button"
|
||||
<RelativeLayout
|
||||
android:id="@+id/download_button_wrapper"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginEnd="12dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/download_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/button_accent_background"
|
||||
android:contentDescription="@string/download_button_description"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_download" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_download"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:elevation="8dp"
|
||||
android:indeterminateTint="@color/white"
|
||||
android:padding="8dp"
|
||||
android:visibility="invisible" />
|
||||
</RelativeLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/delete_button"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -243,17 +251,15 @@
|
|||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/overview"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:text="@{viewModel.item.overview}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/player_items_error"
|
||||
tools:text="After one hundred years of peace, humanity is suddenly reminded of the terror of being at the Titans' mercy." />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,15 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.DownloadViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -33,11 +25,11 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_downloads"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:visibility="gone"/>
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/downloads_recycler_view"
|
||||
|
@ -45,7 +37,6 @@
|
|||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="16dp"
|
||||
app:downloadSections="@{viewModel.downloadSections}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -56,6 +47,4 @@
|
|||
tools:itemCount="4"
|
||||
tools:listitem="@layout/download_section" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,28 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.FavoriteViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loading_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:trackCornerRadius="10dp" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<include
|
||||
android:id="@+id/error_layout"
|
||||
|
@ -33,11 +23,11 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_favorites"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:visibility="gone"/>
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/favorites_recycler_view"
|
||||
|
@ -45,7 +35,6 @@
|
|||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="16dp"
|
||||
app:favoriteSections="@{viewModel.favoriteSections}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -56,6 +45,4 @@
|
|||
tools:itemCount="4"
|
||||
tools:listitem="@layout/favorite_section" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,45 +1,30 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
>
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.HomeViewModel"
|
||||
/>
|
||||
</data>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/refresh_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
>
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.HomeFragment"
|
||||
>
|
||||
tools:context=".fragments.HomeFragment">
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loading_indicator"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:visibility="gone"
|
||||
/>
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<include
|
||||
android:id="@+id/error_layout"
|
||||
layout="@layout/error_panel"
|
||||
/>
|
||||
layout="@layout/error_panel" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/views_recycler_view"
|
||||
|
@ -52,12 +37,8 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:views="@{viewModel.views()}"
|
||||
tools:itemCount="4"
|
||||
tools:listitem="@layout/view_item"
|
||||
/>
|
||||
tools:listitem="@layout/view_item" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
</layout>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
|
|
@ -1,16 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.LibraryViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.LibraryFragment">
|
||||
|
@ -26,7 +17,9 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:trackCornerRadius="10dp" />
|
||||
|
||||
<include android:id="@+id/error_layout" layout="@layout/error_panel" />
|
||||
<include
|
||||
android:id="@+id/error_layout"
|
||||
layout="@layout/error_panel" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/items_recycler_view"
|
||||
|
@ -36,7 +29,6 @@
|
|||
android:paddingHorizontal="12dp"
|
||||
android:paddingTop="16dp"
|
||||
android:scrollbars="none"
|
||||
app:items="@{viewModel.items}"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -46,7 +38,5 @@
|
|||
tools:itemCount="6"
|
||||
tools:listitem="@layout/base_item" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
@ -1,16 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.MediaViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true"
|
||||
|
@ -27,7 +18,9 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:trackCornerRadius="10dp" />
|
||||
|
||||
<include android:id="@+id/error_layout" layout="@layout/error_panel" />
|
||||
<include
|
||||
android:id="@+id/error_layout"
|
||||
layout="@layout/error_panel" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/views_recycler_view"
|
||||
|
@ -37,7 +30,6 @@
|
|||
android:paddingHorizontal="12dp"
|
||||
android:paddingTop="16dp"
|
||||
android:scrollbars="none"
|
||||
app:collections="@{viewModel.collections}"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -47,5 +39,4 @@
|
|||
tools:itemCount="4"
|
||||
tools:listitem="@layout/collection_item" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -1,18 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -45,7 +34,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="200dp"
|
||||
android:scaleType="centerCrop"
|
||||
app:itemBackdropImage="@{viewModel.item}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -66,7 +54,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:text="@{viewModel.item.name}"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
app:layout_constraintBottom_toTopOf="@id/original_title"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -77,7 +64,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:text="@{viewModel.item.originalTitle}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
@ -95,7 +81,6 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{viewModel.dateString}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
tools:text="2019" />
|
||||
|
||||
|
@ -104,7 +89,6 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{viewModel.runTime}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
tools:text="122 min" />
|
||||
|
||||
|
@ -113,7 +97,6 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@{viewModel.item.officialRating}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
tools:text="PG-13" />
|
||||
|
||||
|
@ -123,7 +106,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="4dp"
|
||||
android:gravity="bottom"
|
||||
android:text="@{viewModel.item.communityRating.toString()}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:drawableStartCompat="@drawable/ic_star"
|
||||
app:drawableTint="@color/yellow"
|
||||
|
@ -256,8 +238,7 @@
|
|||
android:id="@+id/genres_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="@{viewModel.item.genres.size() < 1 ? View.GONE : View.VISIBLE}">
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/genres_title"
|
||||
|
@ -273,7 +254,6 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="64dp"
|
||||
android:text="@{viewModel.genresString}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -285,8 +265,7 @@
|
|||
android:id="@+id/director_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="@{viewModel.director == null ? View.GONE : View.VISIBLE}">
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/director_title"
|
||||
|
@ -302,7 +281,6 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="64dp"
|
||||
android:text="@{viewModel.director.name}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -314,8 +292,7 @@
|
|||
android:id="@+id/writers_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="@{viewModel.writers.size() < 1 ? View.GONE : View.VISIBLE}">
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/writers_title"
|
||||
|
@ -331,7 +308,6 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="64dp"
|
||||
android:text="@{viewModel.writersString}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -347,16 +323,15 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:text="@{viewModel.item.overview}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
tools:text="An angel falls. A warrior rises. When Alita awakens with no memory of who she is in a future world she does not recognize, she is taken in by Ido, a compassionate doctor who realizes that somewhere in this abandoned cyborg shell is the heart and soul of a young woman with an extraordinary past." />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/next_up_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="@{viewModel.nextUp != null ? View.VISIBLE : View.GONE}">
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
|
@ -382,7 +357,6 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:adjustViewBounds="true"
|
||||
app:baseItemImage="@{viewModel.nextUp}"
|
||||
app:layout_constraintDimensionRatio="H,16:9"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -390,10 +364,10 @@
|
|||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Findroid.Image" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/next_up_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@{String.format(@string/episode_name_extended, viewModel.nextUp.parentIndexNumber, viewModel.nextUp.indexNumber, viewModel.nextUp.name)}"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
@ -404,11 +378,11 @@
|
|||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/seasons_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="@{viewModel.seasons != null ? View.VISIBLE : View.GONE}">
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
|
@ -426,7 +400,6 @@
|
|||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
app:items="@{viewModel.seasons}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/base_item" />
|
||||
|
@ -457,13 +430,10 @@
|
|||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:people="@{viewModel.actors}"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/person_item" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -1,18 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<import type="android.view.View" />
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.PersonDetailViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -120,7 +109,7 @@
|
|||
android:layout_marginBottom="12dp"
|
||||
android:text="@string/movies_label"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||
android:visibility="@{viewModel.starredIn.movies.empty ? View.GONE : View.VISIBLE}" />
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/movies_list"
|
||||
|
@ -130,7 +119,6 @@
|
|||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
app:items="@{viewModel.starredIn.movies}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="4"
|
||||
tools:listitem="@layout/base_item" />
|
||||
|
@ -143,7 +131,7 @@
|
|||
android:layout_marginBottom="12dp"
|
||||
android:text="@string/shows_label"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||
android:visibility="@{viewModel.starredIn.shows.empty ? View.GONE : View.VISIBLE}" />
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/show_list"
|
||||
|
@ -152,7 +140,6 @@
|
|||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="12dp"
|
||||
app:items="@{viewModel.starredIn.shows}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:itemCount="4"
|
||||
tools:listitem="@layout/base_item" />
|
||||
|
@ -163,7 +150,4 @@
|
|||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
||||
</layout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,31 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.SearchResultViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loading_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:trackCornerRadius="10dp" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<include android:id="@+id/error_layout" layout="@layout/error_panel" />
|
||||
<include
|
||||
android:id="@+id/error_layout"
|
||||
layout="@layout/error_panel" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/no_search_results_text"
|
||||
|
@ -44,7 +35,6 @@
|
|||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="16dp"
|
||||
app:favoriteSections="@{viewModel.sections}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -53,6 +43,4 @@
|
|||
tools:itemCount="4"
|
||||
tools:listitem="@layout/favorite_section" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</layout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,31 +1,19 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".fragments.SeasonFragment">
|
||||
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="dev.jdtech.jellyfin.viewmodels.SeasonViewModel" />
|
||||
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/loading_indicator"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:trackCornerRadius="10dp" />
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<include
|
||||
android:id="@+id/error_layout"
|
||||
|
@ -36,7 +24,6 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:clipToPadding="false"
|
||||
app:episodes="@{viewModel.episodes}"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -44,5 +31,4 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:itemCount="4"
|
||||
tools:listitem="@layout/episode_item" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in a new issue