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 androidx.recyclerview.widget.RecyclerView
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
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.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.HomeEpisodeListAdapter
|
||||||
import dev.jdtech.jellyfin.adapters.HomeItem
|
import dev.jdtech.jellyfin.adapters.HomeItem
|
||||||
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
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.api.JellyfinApi
|
||||||
import dev.jdtech.jellyfin.database.Server
|
import dev.jdtech.jellyfin.database.Server
|
||||||
import dev.jdtech.jellyfin.models.DownloadSection
|
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.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.BaseItemPerson
|
import org.jellyfin.sdk.model.api.BaseItemPerson
|
||||||
import org.jellyfin.sdk.model.api.ImageType
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
@ -32,12 +27,6 @@ fun bindServers(recyclerView: RecyclerView, data: List<Server>?) {
|
||||||
adapter.submitList(data)
|
adapter.submitList(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@BindingAdapter("views")
|
|
||||||
fun bindViews(recyclerView: RecyclerView, data: List<HomeItem>?) {
|
|
||||||
val adapter = recyclerView.adapter as ViewListAdapter
|
|
||||||
adapter.submitList(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
@BindingAdapter("items")
|
@BindingAdapter("items")
|
||||||
fun bindItems(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
fun bindItems(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
||||||
val adapter = recyclerView.adapter as ViewItemListAdapter
|
val adapter = recyclerView.adapter as ViewItemListAdapter
|
||||||
|
@ -68,12 +57,6 @@ fun bindItemBackdropById(imageView: ImageView, itemId: UUID) {
|
||||||
imageView.loadImage("/items/$itemId/Images/${ImageType.BACKDROP}")
|
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")
|
@BindingAdapter("people")
|
||||||
fun bindPeople(recyclerView: RecyclerView, data: List<BaseItemPerson>?) {
|
fun bindPeople(recyclerView: RecyclerView, data: List<BaseItemPerson>?) {
|
||||||
val adapter = recyclerView.adapter as PersonListAdapter
|
val adapter = recyclerView.adapter as PersonListAdapter
|
||||||
|
@ -87,12 +70,6 @@ fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) {
|
||||||
.posterDescription(person.name)
|
.posterDescription(person.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@BindingAdapter("episodes")
|
|
||||||
fun bindEpisodes(recyclerView: RecyclerView, data: List<EpisodeItem>?) {
|
|
||||||
val adapter = recyclerView.adapter as EpisodeListAdapter
|
|
||||||
adapter.submitList(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
@BindingAdapter("homeEpisodes")
|
@BindingAdapter("homeEpisodes")
|
||||||
fun bindHomeEpisodes(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
fun bindHomeEpisodes(recyclerView: RecyclerView, data: List<BaseItemDto>?) {
|
||||||
val adapter = recyclerView.adapter as HomeEpisodeListAdapter
|
val adapter = recyclerView.adapter as HomeEpisodeListAdapter
|
||||||
|
@ -136,18 +113,6 @@ fun bindSeasonPoster(imageView: ImageView, seasonId: UUID) {
|
||||||
imageView.loadImage("/items/${seasonId}/Images/${ImageType.PRIMARY}")
|
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 {
|
private fun ImageView.loadImage(url: String, errorPlaceHolderId: Int? = null): View {
|
||||||
val api = JellyfinApi.getInstance(context.applicationContext)
|
val api = JellyfinApi.getInstance(context.applicationContext)
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.navigation.fragment.NavHostFragment
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.databinding.ActivityMainTvBinding
|
import dev.jdtech.jellyfin.databinding.ActivityMainTvBinding
|
||||||
import dev.jdtech.jellyfin.tv.ui.HomeFragmentDirections
|
import dev.jdtech.jellyfin.tv.ui.HomeFragmentDirections
|
||||||
|
import dev.jdtech.jellyfin.utils.loadDownloadLocation
|
||||||
import dev.jdtech.jellyfin.viewmodels.MainViewModel
|
import dev.jdtech.jellyfin.viewmodels.MainViewModel
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -24,6 +25,8 @@ internal class MainActivityTv : FragmentActivity() {
|
||||||
supportFragmentManager.findFragmentById(R.id.tv_nav_host) as NavHostFragment
|
supportFragmentManager.findFragmentById(R.id.tv_nav_host) as NavHostFragment
|
||||||
val navController = navHostFragment.navController
|
val navController = navHostFragment.navController
|
||||||
|
|
||||||
|
loadDownloadLocation(applicationContext)
|
||||||
|
|
||||||
viewModel.navigateToAddServer.observe(this, {
|
viewModel.navigateToAddServer.observe(this, {
|
||||||
if (it) {
|
if (it) {
|
||||||
navController.navigate(HomeFragmentDirections.actionHomeFragmentToAddServerFragment())
|
navController.navigate(HomeFragmentDirections.actionHomeFragmentToAddServerFragment())
|
||||||
|
|
|
@ -4,21 +4,23 @@ import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
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.findNavController
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.adapters.DownloadEpisodeListAdapter
|
import dev.jdtech.jellyfin.adapters.*
|
||||||
import dev.jdtech.jellyfin.adapters.DownloadViewItemListAdapter
|
|
||||||
import dev.jdtech.jellyfin.adapters.DownloadsListAdapter
|
|
||||||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
|
||||||
import dev.jdtech.jellyfin.databinding.FragmentDownloadBinding
|
import dev.jdtech.jellyfin.databinding.FragmentDownloadBinding
|
||||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
import dev.jdtech.jellyfin.viewmodels.DownloadViewModel
|
import dev.jdtech.jellyfin.viewmodels.DownloadViewModel
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -27,14 +29,14 @@ class DownloadFragment : Fragment() {
|
||||||
private lateinit var binding: FragmentDownloadBinding
|
private lateinit var binding: FragmentDownloadBinding
|
||||||
private val viewModel: DownloadViewModel by viewModels()
|
private val viewModel: DownloadViewModel by viewModels()
|
||||||
|
|
||||||
|
private lateinit var errorDialog: ErrorDialogFragment
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
binding = FragmentDownloadBinding.inflate(inflater, container, false)
|
binding = FragmentDownloadBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
binding.viewModel = viewModel
|
|
||||||
binding.downloadsRecyclerView.adapter = DownloadsListAdapter(
|
binding.downloadsRecyclerView.adapter = DownloadsListAdapter(
|
||||||
DownloadViewItemListAdapter.OnClickListener { item ->
|
DownloadViewItemListAdapter.OnClickListener { item ->
|
||||||
navigateToMediaInfoFragment(item)
|
navigateToMediaInfoFragment(item)
|
||||||
|
@ -42,40 +44,56 @@ class DownloadFragment : Fragment() {
|
||||||
navigateToEpisodeBottomSheetFragment(item)
|
navigateToEpisodeBottomSheetFragment(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
viewModel.finishedLoading.observe(viewLifecycleOwner, { isFinished ->
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
binding.loadingIndicator.visibility = if (isFinished) View.GONE else View.VISIBLE
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
})
|
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
|
Timber.d("$uiState")
|
||||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
when (uiState) {
|
||||||
if (error != null) {
|
is DownloadViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
checkIfLoginRequired(error)
|
is DownloadViewModel.UiState.Loading -> bindUiStateLoading()
|
||||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
is DownloadViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||||
binding.downloadsRecyclerView.visibility = View.GONE
|
}
|
||||||
} else {
|
}
|
||||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
|
||||||
binding.downloadsRecyclerView.visibility = View.VISIBLE
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||||
viewModel.loadData()
|
viewModel.loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
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
|
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) {
|
private fun navigateToMediaInfoFragment(item: PlayerItem) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
DownloadFragmentDirections.actionDownloadFragmentToMediaInfoFragment(
|
DownloadFragmentDirections.actionDownloadFragmentToMediaInfoFragment(
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package dev.jdtech.jellyfin.fragments
|
package dev.jdtech.jellyfin.fragments
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -9,18 +8,22 @@ import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
|
import dev.jdtech.jellyfin.bindBaseItemImage
|
||||||
import dev.jdtech.jellyfin.databinding.EpisodeBottomSheetBinding
|
import dev.jdtech.jellyfin.databinding.EpisodeBottomSheetBinding
|
||||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
import dev.jdtech.jellyfin.utils.requestDownload
|
|
||||||
import dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel
|
import dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.jellyfin.sdk.model.api.LocationType
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@ -39,13 +42,10 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
||||||
): View {
|
): View {
|
||||||
binding = EpisodeBottomSheetBinding.inflate(inflater, container, false)
|
binding = EpisodeBottomSheetBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
binding.viewModel = viewModel
|
|
||||||
|
|
||||||
binding.playButton.setOnClickListener {
|
binding.playButton.setOnClickListener {
|
||||||
binding.playButton.setImageResource(android.R.color.transparent)
|
binding.playButton.setImageResource(android.R.color.transparent)
|
||||||
binding.progressCircular.visibility = View.VISIBLE
|
binding.progressCircular.isVisible = true
|
||||||
viewModel.item.value?.let {
|
viewModel.item?.let {
|
||||||
if (!args.isOffline) {
|
if (!args.isOffline) {
|
||||||
playerViewModel.loadPlayerItems(it)
|
playerViewModel.loadPlayerItems(it)
|
||||||
} else {
|
} 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 ->
|
playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems ->
|
||||||
when (playerItems) {
|
when (playerItems) {
|
||||||
is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(playerItems)
|
is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(playerItems)
|
||||||
|
@ -61,79 +74,46 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.item.observe(viewLifecycleOwner, { episode ->
|
if(!args.isOffline) {
|
||||||
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){
|
|
||||||
val episodeId: UUID = args.episodeId
|
val episodeId: UUID = args.episodeId
|
||||||
|
|
||||||
binding.checkButton.setOnClickListener {
|
binding.checkButton.setOnClickListener {
|
||||||
when (viewModel.played.value) {
|
when (viewModel.played) {
|
||||||
true -> viewModel.markAsUnplayed(episodeId)
|
true -> {
|
||||||
false -> viewModel.markAsPlayed(episodeId)
|
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 {
|
binding.favoriteButton.setOnClickListener {
|
||||||
when (viewModel.favorite.value) {
|
when (viewModel.favorite) {
|
||||||
true -> viewModel.unmarkAsFavorite(episodeId)
|
true -> {
|
||||||
false -> viewModel.markAsFavorite(episodeId)
|
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.setOnClickListener {
|
||||||
|
binding.downloadButton.isEnabled = false
|
||||||
viewModel.loadDownloadRequestItem(episodeId)
|
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)
|
viewModel.loadEpisode(episodeId)
|
||||||
}else {
|
} else {
|
||||||
val playerItem = args.playerItem!!
|
val playerItem = args.playerItem!!
|
||||||
viewModel.loadEpisode(playerItem)
|
viewModel.loadEpisode(playerItem)
|
||||||
|
|
||||||
|
@ -143,14 +123,67 @@ class EpisodeBottomSheetFragment : BottomSheetDialogFragment() {
|
||||||
findNavController().navigate(R.id.downloadFragment)
|
findNavController().navigate(R.id.downloadFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.checkButton.visibility = View.GONE
|
binding.checkButton.isVisible = false
|
||||||
binding.favoriteButton.visibility = View.GONE
|
binding.favoriteButton.isVisible = false
|
||||||
binding.downloadButton.visibility = View.GONE
|
binding.downloadButtonWrapper.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
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) {
|
private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) {
|
||||||
navigateToPlayerActivity(items.items.toTypedArray())
|
navigateToPlayerActivity(items.items.toTypedArray())
|
||||||
binding.playButton.setImageDrawable(
|
binding.playButton.setImageDrawable(
|
||||||
|
|
|
@ -5,7 +5,11 @@ import androidx.fragment.app.Fragment
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.viewModels
|
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.findNavController
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
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.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
import dev.jdtech.jellyfin.viewmodels.FavoriteViewModel
|
import dev.jdtech.jellyfin.viewmodels.FavoriteViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class FavoriteFragment : Fragment() {
|
class FavoriteFragment : Fragment() {
|
||||||
|
@ -24,14 +30,14 @@ class FavoriteFragment : Fragment() {
|
||||||
private lateinit var binding: FragmentFavoriteBinding
|
private lateinit var binding: FragmentFavoriteBinding
|
||||||
private val viewModel: FavoriteViewModel by viewModels()
|
private val viewModel: FavoriteViewModel by viewModels()
|
||||||
|
|
||||||
|
private lateinit var errorDialog: ErrorDialogFragment
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
binding = FragmentFavoriteBinding.inflate(inflater, container, false)
|
binding = FragmentFavoriteBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
binding.viewModel = viewModel
|
|
||||||
binding.favoritesRecyclerView.adapter = FavoritesListAdapter(
|
binding.favoritesRecyclerView.adapter = FavoritesListAdapter(
|
||||||
ViewItemListAdapter.OnClickListener { item ->
|
ViewItemListAdapter.OnClickListener { item ->
|
||||||
navigateToMediaInfoFragment(item)
|
navigateToMediaInfoFragment(item)
|
||||||
|
@ -39,40 +45,56 @@ class FavoriteFragment : Fragment() {
|
||||||
navigateToEpisodeBottomSheetFragment(item)
|
navigateToEpisodeBottomSheetFragment(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
viewModel.finishedLoading.observe(viewLifecycleOwner, { isFinished ->
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
binding.loadingIndicator.visibility = if (isFinished) View.GONE else View.VISIBLE
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
})
|
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
|
Timber.d("$uiState")
|
||||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
when (uiState) {
|
||||||
if (error != null) {
|
is FavoriteViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
checkIfLoginRequired(error)
|
is FavoriteViewModel.UiState.Loading -> bindUiStateLoading()
|
||||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
is FavoriteViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||||
binding.favoritesRecyclerView.visibility = View.GONE
|
}
|
||||||
} else {
|
}
|
||||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
|
||||||
binding.favoritesRecyclerView.visibility = View.VISIBLE
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||||
viewModel.loadData()
|
viewModel.loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
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
|
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) {
|
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
FavoriteFragmentDirections.actionFavoriteFragmentToMediaInfoFragment(
|
FavoriteFragmentDirections.actionFavoriteFragmentToMediaInfoFragment(
|
||||||
|
|
|
@ -12,7 +12,9 @@ import android.widget.Toast.LENGTH_LONG
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
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.checkIfLoginRequired
|
||||||
import dev.jdtech.jellyfin.utils.contentType
|
import dev.jdtech.jellyfin.utils.contentType
|
||||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel
|
import dev.jdtech.jellyfin.viewmodels.HomeViewModel
|
||||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel.Loading
|
import kotlinx.coroutines.launch
|
||||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel.LoadingError
|
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
|
@ -38,6 +40,8 @@ class HomeFragment : Fragment() {
|
||||||
private lateinit var binding: FragmentHomeBinding
|
private lateinit var binding: FragmentHomeBinding
|
||||||
private val viewModel: HomeViewModel by viewModels()
|
private val viewModel: HomeViewModel by viewModels()
|
||||||
|
|
||||||
|
private lateinit var errorDialog: ErrorDialogFragment
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
@ -66,9 +70,6 @@ class HomeFragment : Fragment() {
|
||||||
): View {
|
): View {
|
||||||
binding = FragmentHomeBinding.inflate(inflater, container, false)
|
binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
binding.viewModel = viewModel
|
|
||||||
|
|
||||||
setupView()
|
setupView()
|
||||||
bindState()
|
bindState()
|
||||||
|
|
||||||
|
@ -84,6 +85,7 @@ class HomeFragment : Fragment() {
|
||||||
private fun setupView() {
|
private fun setupView() {
|
||||||
binding.refreshLayout.setOnRefreshListener {
|
binding.refreshLayout.setOnRefreshListener {
|
||||||
viewModel.refreshData()
|
viewModel.refreshData()
|
||||||
|
// binding.refreshLayout.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.viewsRecyclerView.adapter = ViewListAdapter(
|
binding.viewsRecyclerView.adapter = ViewListAdapter(
|
||||||
|
@ -99,50 +101,56 @@ class HomeFragment : Fragment() {
|
||||||
.show()
|
.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 {
|
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||||
viewModel.refreshData()
|
viewModel.refreshData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||||
|
errorDialog.show(parentFragmentManager, "errordialog")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun bindLoading(state: Loading) {
|
private fun bindState() {
|
||||||
binding.errorLayout.errorPanel.isVisible = false
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
binding.viewsRecyclerView.isVisible = true
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
binding.loadingIndicator.visibility = when {
|
Timber.d("$uiState")
|
||||||
state.inProgress && binding.refreshLayout.isRefreshing -> View.GONE
|
when (uiState) {
|
||||||
state.inProgress -> View.VISIBLE
|
is HomeViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
else -> {
|
is HomeViewModel.UiState.Loading -> bindUiStateLoading()
|
||||||
binding.refreshLayout.isRefreshing = false
|
is HomeViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||||
View.GONE
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
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) {
|
private fun navigateToLibraryFragment(view: dev.jdtech.jellyfin.models.View) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
HomeFragmentDirections.actionNavigationHomeToLibraryFragment(
|
HomeFragmentDirections.actionNavigationHomeToLibraryFragment(
|
||||||
|
|
|
@ -3,8 +3,12 @@ package dev.jdtech.jellyfin.fragments
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
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.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.dialogs.SortDialogFragment
|
||||||
import dev.jdtech.jellyfin.utils.SortBy
|
import dev.jdtech.jellyfin.utils.SortBy
|
||||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.SortOrder
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
import java.lang.IllegalArgumentException
|
import java.lang.IllegalArgumentException
|
||||||
|
@ -26,9 +31,10 @@ class LibraryFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var binding: FragmentLibraryBinding
|
private lateinit var binding: FragmentLibraryBinding
|
||||||
private val viewModel: LibraryViewModel by viewModels()
|
private val viewModel: LibraryViewModel by viewModels()
|
||||||
|
|
||||||
private val args: LibraryFragmentArgs by navArgs()
|
private val args: LibraryFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
private lateinit var errorDialog: ErrorDialogFragment
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var sp: SharedPreferences
|
lateinit var sp: SharedPreferences
|
||||||
|
|
||||||
|
@ -67,56 +73,71 @@ class LibraryFragment : Fragment() {
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
binding = FragmentLibraryBinding.inflate(inflater, container, false)
|
binding = FragmentLibraryBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
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 {
|
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||||
viewModel.loadItems(args.libraryId, args.libraryType)
|
viewModel.loadItems(args.libraryId, args.libraryType)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
errorDialog.show(
|
||||||
parentFragmentManager,
|
parentFragmentManager,
|
||||||
"errordialog"
|
"errordialog"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.finishedLoading.observe(viewLifecycleOwner, {
|
|
||||||
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
|
|
||||||
})
|
|
||||||
|
|
||||||
binding.itemsRecyclerView.adapter =
|
binding.itemsRecyclerView.adapter =
|
||||||
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item ->
|
ViewItemListAdapter(ViewItemListAdapter.OnClickListener { item ->
|
||||||
navigateToMediaInfoFragment(item)
|
navigateToMediaInfoFragment(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Sorting options
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
val sortBy = SortBy.fromString(sp.getString("sortBy", SortBy.defaultValue.name)!!)
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
val sortOrder = try {
|
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
SortOrder.valueOf(sp.getString("sortOrder", SortOrder.ASCENDING.name)!!)
|
when (uiState) {
|
||||||
} catch (e: IllegalArgumentException) {
|
is LibraryViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
SortOrder.ASCENDING
|
is LibraryViewModel.UiState.Loading -> bindUiStateLoading()
|
||||||
}
|
is LibraryViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
viewModel.loadItems(args.libraryId, args.libraryType, sortBy = sortBy, sortOrder = sortOrder)
|
// Sorting options
|
||||||
|
val sortBy = SortBy.fromString(sp.getString("sortBy", SortBy.defaultValue.name)!!)
|
||||||
|
val sortOrder = try {
|
||||||
|
SortOrder.valueOf(sp.getString("sortOrder", SortOrder.ASCENDING.name)!!)
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
SortOrder.ASCENDING
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
|
||||||
|
|
|
@ -3,8 +3,12 @@ package dev.jdtech.jellyfin.fragments
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.*
|
import android.view.*
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
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.findNavController
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
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.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
import dev.jdtech.jellyfin.viewmodels.MediaViewModel
|
import dev.jdtech.jellyfin.viewmodels.MediaViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MediaFragment : Fragment() {
|
class MediaFragment : Fragment() {
|
||||||
|
@ -23,6 +29,8 @@ class MediaFragment : Fragment() {
|
||||||
|
|
||||||
private var originalSoftInputMode: Int? = null
|
private var originalSoftInputMode: Int? = null
|
||||||
|
|
||||||
|
private lateinit var errorDialog: ErrorDialogFragment
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setHasOptionsMenu(true)
|
setHasOptionsMenu(true)
|
||||||
|
@ -56,37 +64,30 @@ class MediaFragment : Fragment() {
|
||||||
): View {
|
): View {
|
||||||
binding = FragmentMediaBinding.inflate(inflater, container, false)
|
binding = FragmentMediaBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
binding.viewModel = viewModel
|
|
||||||
binding.viewsRecyclerView.adapter =
|
binding.viewsRecyclerView.adapter =
|
||||||
CollectionListAdapter(CollectionListAdapter.OnClickListener { library ->
|
CollectionListAdapter(CollectionListAdapter.OnClickListener { library ->
|
||||||
navigateToLibraryFragment(library)
|
navigateToLibraryFragment(library)
|
||||||
})
|
})
|
||||||
|
|
||||||
viewModel.finishedLoading.observe(viewLifecycleOwner, {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
})
|
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
|
Timber.d("$uiState")
|
||||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
when (uiState) {
|
||||||
if (error != null) {
|
is MediaViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
checkIfLoginRequired(error)
|
is MediaViewModel.UiState.Loading -> bindUiStateLoading()
|
||||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
is MediaViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||||
binding.viewsRecyclerView.visibility = View.GONE
|
}
|
||||||
} else {
|
}
|
||||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
|
||||||
binding.viewsRecyclerView.visibility = View.VISIBLE
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||||
viewModel.loadData()
|
viewModel.loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
errorDialog.show(parentFragmentManager, "errordialog")
|
||||||
parentFragmentManager,
|
|
||||||
"errordialog"
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
|
@ -105,6 +106,29 @@ class MediaFragment : Fragment() {
|
||||||
originalSoftInputMode?.let { activity?.window?.setSoftInputMode(it) }
|
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) {
|
private fun navigateToLibraryFragment(library: BaseItemDto) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
MediaFragmentDirections.actionNavigationMediaToLibraryFragment(
|
MediaFragmentDirections.actionNavigationMediaToLibraryFragment(
|
||||||
|
|
|
@ -8,23 +8,28 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
||||||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
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.databinding.FragmentMediaInfoBinding
|
||||||
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
import dev.jdtech.jellyfin.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
|
import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
import dev.jdtech.jellyfin.utils.requestDownload
|
|
||||||
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
import dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.serializer.toUUID
|
import org.jellyfin.sdk.model.serializer.toUUID
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
@ -36,35 +41,39 @@ class MediaInfoFragment : Fragment() {
|
||||||
private lateinit var binding: FragmentMediaInfoBinding
|
private lateinit var binding: FragmentMediaInfoBinding
|
||||||
private val viewModel: MediaInfoViewModel by viewModels()
|
private val viewModel: MediaInfoViewModel by viewModels()
|
||||||
private val playerViewModel: PlayerViewModel by viewModels()
|
private val playerViewModel: PlayerViewModel by viewModels()
|
||||||
|
|
||||||
private val args: MediaInfoFragmentArgs by navArgs()
|
private val args: MediaInfoFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
lateinit var errorDialog: ErrorDialogFragment
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
|
binding = FragmentMediaInfoBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
binding.viewModel = viewModel
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
if (error != null) {
|
Timber.d("$uiState")
|
||||||
checkIfLoginRequired(error)
|
when (uiState) {
|
||||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
is MediaInfoViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
binding.mediaInfoScrollview.visibility = View.GONE
|
is MediaInfoViewModel.UiState.Loading -> bindUiStateLoading()
|
||||||
} else {
|
is MediaInfoViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
}
|
||||||
binding.mediaInfoScrollview.visibility = View.VISIBLE
|
}
|
||||||
|
if (!args.isOffline) {
|
||||||
|
viewModel.loadData(args.itemId, args.itemType)
|
||||||
|
} else {
|
||||||
|
viewModel.loadData(args.playerItem!!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
if(args.itemType != "Movie") {
|
if(args.itemType != "Movie") {
|
||||||
binding.downloadButton.visibility = View.GONE
|
binding.downloadButton.visibility = View.GONE
|
||||||
|
@ -74,36 +83,6 @@ class MediaInfoFragment : Fragment() {
|
||||||
viewModel.loadData(args.itemId, args.itemType)
|
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 ->
|
playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems ->
|
||||||
when (playerItems) {
|
when (playerItems) {
|
||||||
is PlayerViewModel.PlayerItemError -> bindPlayerItemsError(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 {
|
binding.trailerButton.setOnClickListener {
|
||||||
if (viewModel.item.value?.remoteTrailers.isNullOrEmpty()) return@setOnClickListener
|
if (viewModel.item?.remoteTrailers.isNullOrEmpty()) return@setOnClickListener
|
||||||
val intent = Intent(
|
val intent = Intent(
|
||||||
Intent.ACTION_VIEW,
|
Intent.ACTION_VIEW,
|
||||||
Uri.parse(viewModel.item.value?.remoteTrailers?.get(0)?.url)
|
Uri.parse(viewModel.item?.remoteTrailers?.get(0)?.url)
|
||||||
)
|
)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.nextUp.setOnClickListener {
|
binding.nextUp.setOnClickListener {
|
||||||
navigateToEpisodeBottomSheetFragment(viewModel.nextUp.value!!)
|
navigateToEpisodeBottomSheetFragment(viewModel.nextUp!!)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.seasonsRecyclerView.adapter =
|
binding.seasonsRecyclerView.adapter =
|
||||||
|
@ -166,9 +118,8 @@ class MediaInfoFragment : Fragment() {
|
||||||
|
|
||||||
binding.playButton.setOnClickListener {
|
binding.playButton.setOnClickListener {
|
||||||
binding.playButton.setImageResource(android.R.color.transparent)
|
binding.playButton.setImageResource(android.R.color.transparent)
|
||||||
binding.progressCircular.visibility = View.VISIBLE
|
binding.progressCircular.isVisible = true
|
||||||
|
viewModel.item?.let { item ->
|
||||||
viewModel.item.value?.let { item ->
|
|
||||||
if (!args.isOffline) {
|
if (!args.isOffline) {
|
||||||
playerViewModel.loadPlayerItems(item) {
|
playerViewModel.loadPlayerItems(item) {
|
||||||
VideoVersionDialogFragment(item, playerViewModel).show(
|
VideoVersionDialogFragment(item, playerViewModel).show(
|
||||||
|
@ -188,16 +139,28 @@ class MediaInfoFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.checkButton.setOnClickListener {
|
binding.checkButton.setOnClickListener {
|
||||||
when (viewModel.played.value) {
|
when (viewModel.played) {
|
||||||
true -> viewModel.markAsUnplayed(args.itemId)
|
true -> {
|
||||||
false -> viewModel.markAsPlayed(args.itemId)
|
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 {
|
binding.favoriteButton.setOnClickListener {
|
||||||
when (viewModel.favorite.value) {
|
when (viewModel.favorite) {
|
||||||
true -> viewModel.unmarkAsFavorite(args.itemId)
|
true -> {
|
||||||
false -> viewModel.markAsFavorite(args.itemId)
|
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)
|
viewModel.loadDownloadRequestItem(args.itemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.deleteButton.visibility = View.GONE
|
binding.deleteButton.isVisible = false
|
||||||
|
|
||||||
viewModel.loadData(args.itemId, args.itemType)
|
|
||||||
} else {
|
} else {
|
||||||
binding.favoriteButton.visibility = View.GONE
|
binding.favoriteButton.isVisible = false
|
||||||
binding.checkButton.visibility = View.GONE
|
binding.checkButton.isVisible = false
|
||||||
binding.downloadButton.visibility = View.GONE
|
binding.downloadButton.isVisible = false
|
||||||
|
|
||||||
binding.deleteButton.setOnClickListener {
|
binding.deleteButton.setOnClickListener {
|
||||||
viewModel.deleteItem()
|
viewModel.deleteItem()
|
||||||
findNavController().navigate(R.id.downloadFragment)
|
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) {
|
private fun bindPlayerItems(items: PlayerViewModel.PlayerItems) {
|
||||||
navigateToPlayerActivity(items.items.toTypedArray())
|
navigateToPlayerActivity(items.items.toTypedArray())
|
||||||
binding.playButton.setImageDrawable(
|
binding.playButton.setImageDrawable(
|
||||||
|
|
|
@ -9,6 +9,9 @@ import androidx.core.view.isVisible
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
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.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
import dev.jdtech.jellyfin.viewmodels.PersonDetailViewModel
|
import dev.jdtech.jellyfin.viewmodels.PersonDetailViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
internal class PersonDetailFragment : Fragment() {
|
internal class PersonDetailFragment : Fragment() {
|
||||||
|
@ -29,15 +34,14 @@ internal class PersonDetailFragment : Fragment() {
|
||||||
|
|
||||||
private val args: PersonDetailFragmentArgs by navArgs()
|
private val args: PersonDetailFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
private lateinit var errorDialog: ErrorDialogFragment
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
binding = FragmentPersonDetailBinding.inflate(inflater, container, false)
|
binding = FragmentPersonDetailBinding.inflate(inflater, container, false)
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
binding.viewModel = viewModel
|
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,42 +51,65 @@ internal class PersonDetailFragment : Fragment() {
|
||||||
binding.moviesList.adapter = adapter()
|
binding.moviesList.adapter = adapter()
|
||||||
binding.showList.adapter = adapter()
|
binding.showList.adapter = adapter()
|
||||||
|
|
||||||
viewModel.data.observe(viewLifecycleOwner) { data ->
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
binding.name.text = data.name
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
binding.overview.text = data.overview
|
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
|
Timber.d("$uiState")
|
||||||
setupOverviewExpansion()
|
when (uiState) {
|
||||||
|
is PersonDetailViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
bindItemImage(binding.personImage, data.dto)
|
is PersonDetailViewModel.UiState.Loading -> bindUiStateLoading()
|
||||||
}
|
is PersonDetailViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||||
|
}
|
||||||
viewModel.finishedLoading.observe(viewLifecycleOwner, {
|
}
|
||||||
binding.loadingIndicator.visibility = if (it) View.GONE else View.VISIBLE
|
viewModel.loadData(args.personId)
|
||||||
})
|
|
||||||
|
|
||||||
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 {
|
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||||
viewModel.loadData(args.personId)
|
viewModel.loadData(args.personId)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
||||||
ErrorDialogFragment(viewModel.error.value ?: getString(R.string.unknown_error)).show(
|
errorDialog.show(parentFragmentManager, "errordialog")
|
||||||
parentFragmentManager,
|
}
|
||||||
"errordialog"
|
}
|
||||||
)
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.loadData(args.personId)
|
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(
|
private fun adapter() = ViewItemListAdapter(
|
||||||
|
@ -103,7 +130,6 @@ internal class PersonDetailFragment : Fragment() {
|
||||||
binding.overviewGradient.isVisible = false
|
binding.overviewGradient.isVisible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,11 @@ import androidx.fragment.app.Fragment
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.viewModels
|
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.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
import dev.jdtech.jellyfin.viewmodels.SearchResultViewModel
|
import dev.jdtech.jellyfin.viewmodels.SearchResultViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SearchResultFragment : Fragment() {
|
class SearchResultFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var binding: FragmentSearchResultBinding
|
private lateinit var binding: FragmentSearchResultBinding
|
||||||
private val viewModel: SearchResultViewModel by viewModels()
|
private val viewModel: SearchResultViewModel by viewModels()
|
||||||
|
|
||||||
private val args: SearchResultFragmentArgs by navArgs()
|
private val args: SearchResultFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
private lateinit var errorDialog: ErrorDialogFragment
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
binding = FragmentSearchResultBinding.inflate(inflater, container, false)
|
binding = FragmentSearchResultBinding.inflate(inflater, container, false)
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
binding.viewModel = viewModel
|
|
||||||
binding.searchResultsRecyclerView.adapter = FavoritesListAdapter(
|
binding.searchResultsRecyclerView.adapter = FavoritesListAdapter(
|
||||||
ViewItemListAdapter.OnClickListener { item ->
|
ViewItemListAdapter.OnClickListener { item ->
|
||||||
navigateToMediaInfoFragment(item)
|
navigateToMediaInfoFragment(item)
|
||||||
|
@ -42,42 +47,58 @@ class SearchResultFragment : Fragment() {
|
||||||
navigateToEpisodeBottomSheetFragment(item)
|
navigateToEpisodeBottomSheetFragment(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
viewModel.finishedLoading.observe(viewLifecycleOwner, { isFinished ->
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
binding.loadingIndicator.visibility = if (isFinished) View.GONE else View.VISIBLE
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
})
|
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
|
Timber.d("$uiState")
|
||||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
when (uiState) {
|
||||||
if (error != null) {
|
is SearchResultViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
checkIfLoginRequired(error)
|
is SearchResultViewModel.UiState.Loading -> bindUiStateLoading()
|
||||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
is SearchResultViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||||
binding.searchResultsRecyclerView.visibility = View.GONE
|
}
|
||||||
} else {
|
}
|
||||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
viewModel.loadData(args.query)
|
||||||
binding.searchResultsRecyclerView.visibility = View.VISIBLE
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||||
viewModel.loadData(args.query)
|
viewModel.loadData(args.query)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
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
|
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) {
|
private fun navigateToMediaInfoFragment(item: BaseItemDto) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
FavoriteFragmentDirections.actionFavoriteFragmentToMediaInfoFragment(
|
FavoriteFragmentDirections.actionFavoriteFragmentToMediaInfoFragment(
|
||||||
|
|
|
@ -5,7 +5,11 @@ import androidx.fragment.app.Fragment
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.viewModels
|
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.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
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.dialogs.ErrorDialogFragment
|
||||||
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
import dev.jdtech.jellyfin.utils.checkIfLoginRequired
|
||||||
import dev.jdtech.jellyfin.viewmodels.SeasonViewModel
|
import dev.jdtech.jellyfin.viewmodels.SeasonViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class SeasonFragment : Fragment() {
|
class SeasonFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var binding: FragmentSeasonBinding
|
private lateinit var binding: FragmentSeasonBinding
|
||||||
private val viewModel: SeasonViewModel by viewModels()
|
private val viewModel: SeasonViewModel by viewModels()
|
||||||
|
|
||||||
private val args: SeasonFragmentArgs by navArgs()
|
private val args: SeasonFragmentArgs by navArgs()
|
||||||
|
|
||||||
|
private lateinit var errorDialog: ErrorDialogFragment
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
binding = FragmentSeasonBinding.inflate(inflater, container, false)
|
binding = FragmentSeasonBinding.inflate(inflater, container, false)
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
binding.viewModel = viewModel
|
|
||||||
|
|
||||||
viewModel.error.observe(viewLifecycleOwner, { error ->
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
if (error != null) {
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
checkIfLoginRequired(error)
|
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
binding.errorLayout.errorPanel.visibility = View.VISIBLE
|
Timber.d("$uiState")
|
||||||
binding.episodesRecyclerView.visibility = View.GONE
|
when (uiState) {
|
||||||
} else {
|
is SeasonViewModel.UiState.Normal -> bindUiStateNormal(uiState)
|
||||||
binding.errorLayout.errorPanel.visibility = View.GONE
|
is SeasonViewModel.UiState.Loading -> bindUiStateLoading()
|
||||||
binding.episodesRecyclerView.visibility = View.VISIBLE
|
is SeasonViewModel.UiState.Error -> bindUiStateError(uiState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.loadEpisodes(args.seriesId, args.seasonId)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorRetryButton.setOnClickListener {
|
binding.errorLayout.errorRetryButton.setOnClickListener {
|
||||||
viewModel.loadEpisodes(args.seriesId, args.seasonId)
|
viewModel.loadEpisodes(args.seriesId, args.seasonId)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.errorLayout.errorDetailsButton.setOnClickListener {
|
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 =
|
binding.episodesRecyclerView.adapter =
|
||||||
EpisodeListAdapter(EpisodeListAdapter.OnClickListener { episode ->
|
EpisodeListAdapter(EpisodeListAdapter.OnClickListener { episode ->
|
||||||
navigateToEpisodeBottomSheetFragment(episode)
|
navigateToEpisodeBottomSheetFragment(episode)
|
||||||
}, args.seriesId, args.seriesName, args.seasonId, args.seasonName)
|
}, 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) {
|
private fun navigateToEpisodeBottomSheetFragment(episode: BaseItemDto) {
|
||||||
|
|
|
@ -6,12 +6,16 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.fragment.app.viewModels
|
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.findNavController
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.databinding.FragmentServerSelectBinding
|
import dev.jdtech.jellyfin.databinding.FragmentServerSelectBinding
|
||||||
import dev.jdtech.jellyfin.dialogs.DeleteServerDialogFragment
|
import dev.jdtech.jellyfin.dialogs.DeleteServerDialogFragment
|
||||||
import dev.jdtech.jellyfin.adapters.ServerGridAdapter
|
import dev.jdtech.jellyfin.adapters.ServerGridAdapter
|
||||||
import dev.jdtech.jellyfin.viewmodels.ServerSelectViewModel
|
import dev.jdtech.jellyfin.viewmodels.ServerSelectViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class ServerSelectFragment : Fragment() {
|
class ServerSelectFragment : Fragment() {
|
||||||
|
@ -44,11 +48,15 @@ class ServerSelectFragment : Fragment() {
|
||||||
navigateToAddServerFragment()
|
navigateToAddServerFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.navigateToMain.observe(viewLifecycleOwner, {
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
if (it) {
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
navigateToMainActivity()
|
viewModel.onNavigateToMain(viewLifecycleOwner.lifecycleScope) {
|
||||||
|
if (it) {
|
||||||
|
navigateToMainActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
@ -61,6 +69,5 @@ class ServerSelectFragment : Fragment() {
|
||||||
|
|
||||||
private fun navigateToMainActivity() {
|
private fun navigateToMainActivity() {
|
||||||
findNavController().navigate(ServerSelectFragmentDirections.actionServerSelectFragmentToHomeFragment())
|
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.LiveTv
|
||||||
import dev.jdtech.jellyfin.models.CollectionType.Music
|
import dev.jdtech.jellyfin.models.CollectionType.Music
|
||||||
import dev.jdtech.jellyfin.models.CollectionType.Playlists
|
import dev.jdtech.jellyfin.models.CollectionType.Playlists
|
||||||
|
import dev.jdtech.jellyfin.models.CollectionType.BoxSets
|
||||||
|
|
||||||
enum class CollectionType (val type: String) {
|
enum class CollectionType (val type: String) {
|
||||||
HomeVideos("homevideos"),
|
HomeVideos("homevideos"),
|
||||||
Music("music"),
|
Music("music"),
|
||||||
Playlists("playlists"),
|
Playlists("playlists"),
|
||||||
Books("books"),
|
Books("books"),
|
||||||
LiveTv("livetv")
|
LiveTv("livetv"),
|
||||||
|
BoxSets("boxsets")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unsupportedCollections() = listOf(
|
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.HeaderItem
|
||||||
import androidx.leanback.widget.ListRow
|
import androidx.leanback.widget.ListRow
|
||||||
import androidx.leanback.widget.ListRowPresenter
|
import androidx.leanback.widget.ListRowPresenter
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.adapters.HomeItem
|
import dev.jdtech.jellyfin.adapters.HomeItem
|
||||||
import dev.jdtech.jellyfin.viewmodels.HomeViewModel
|
import dev.jdtech.jellyfin.viewmodels.HomeViewModel
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
internal class HomeFragment : BrowseSupportFragment() {
|
internal class HomeFragment : BrowseSupportFragment() {
|
||||||
|
@ -48,7 +53,22 @@ internal class HomeFragment : BrowseSupportFragment() {
|
||||||
setOnClickListener { navigateToSettingsFragment() }
|
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()
|
rowsAdapter.clear()
|
||||||
homeItems.map { section -> rowsAdapter.add(section.toListRow()) }
|
homeItems.map { section -> rowsAdapter.add(section.toListRow()) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,22 +11,24 @@ import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.fragment.navArgs
|
import androidx.navigation.fragment.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
import dev.jdtech.jellyfin.adapters.PersonListAdapter
|
||||||
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
import dev.jdtech.jellyfin.adapters.ViewItemListAdapter
|
||||||
|
import dev.jdtech.jellyfin.bindBaseItemImage
|
||||||
import dev.jdtech.jellyfin.databinding.MediaDetailFragmentBinding
|
import dev.jdtech.jellyfin.databinding.MediaDetailFragmentBinding
|
||||||
import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
|
import dev.jdtech.jellyfin.dialogs.VideoVersionDialogFragment
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
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.MediaInfoViewModel
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel.PlayerItemError
|
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel.PlayerItemError
|
||||||
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel.PlayerItems
|
import dev.jdtech.jellyfin.viewmodels.PlayerViewModel.PlayerItems
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
|
@ -35,7 +37,6 @@ internal class MediaDetailFragment : Fragment() {
|
||||||
private lateinit var binding: MediaDetailFragmentBinding
|
private lateinit var binding: MediaDetailFragmentBinding
|
||||||
|
|
||||||
private val viewModel: MediaInfoViewModel by viewModels()
|
private val viewModel: MediaInfoViewModel by viewModels()
|
||||||
private val detailViewModel: MediaDetailViewModel by viewModels()
|
|
||||||
private val playerViewModel: PlayerViewModel by viewModels()
|
private val playerViewModel: PlayerViewModel by viewModels()
|
||||||
|
|
||||||
private val args: MediaDetailFragmentArgs by navArgs()
|
private val args: MediaDetailFragmentArgs by navArgs()
|
||||||
|
@ -52,28 +53,29 @@ internal class MediaDetailFragment : Fragment() {
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View {
|
||||||
binding = MediaDetailFragmentBinding.inflate(inflater)
|
binding = MediaDetailFragmentBinding.inflate(inflater)
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
|
||||||
return binding.root
|
return binding.root
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
|
||||||
binding.viewModel = viewModel
|
viewLifecycleOwner.lifecycleScope.launch {
|
||||||
binding.item = detailViewModel.transformData(viewModel.item, resources) {
|
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
bindActions(it)
|
viewModel.onUiState(viewLifecycleOwner.lifecycleScope) { uiState ->
|
||||||
bindState(it)
|
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(
|
val seasonsAdapter = ViewItemListAdapter(
|
||||||
fixedWidth = true,
|
fixedWidth = true,
|
||||||
onClickListener = ViewItemListAdapter.OnClickListener {})
|
onClickListener = ViewItemListAdapter.OnClickListener {})
|
||||||
|
|
||||||
viewModel.seasons.observe(viewLifecycleOwner) {
|
|
||||||
seasonsAdapter.submitList(it)
|
|
||||||
binding.seasonTitle.isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.seasonsRow.gridView.adapter = seasonsAdapter
|
binding.seasonsRow.gridView.adapter = seasonsAdapter
|
||||||
binding.seasonsRow.gridView.verticalSpacing = 25
|
binding.seasonsRow.gridView.verticalSpacing = 25
|
||||||
|
|
||||||
|
@ -81,33 +83,110 @@ internal class MediaDetailFragment : Fragment() {
|
||||||
Toast.makeText(requireContext(), "Not yet implemented", Toast.LENGTH_SHORT).show()
|
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.adapter = castAdapter
|
||||||
binding.castRow.gridView.verticalSpacing = 25
|
binding.castRow.gridView.verticalSpacing = 25
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindState(state: MediaDetailViewModel.State) {
|
playerViewModel.onPlaybackRequested(lifecycleScope) { playerItems ->
|
||||||
playerViewModel.onPlaybackRequested(lifecycleScope) { state ->
|
when (playerItems) {
|
||||||
when (state) {
|
is PlayerItemError -> bindPlayerItemsError(playerItems)
|
||||||
is PlayerItemError -> bindPlayerItemsError(state)
|
is PlayerItems -> bindPlayerItems(playerItems)
|
||||||
is PlayerItems -> bindPlayerItems(state)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (state.media) {
|
binding.playButton.setOnClickListener {
|
||||||
is Movie -> binding.title.text = state.media.title
|
binding.playButton.setImageResource(android.R.color.transparent)
|
||||||
is TvShow -> with(binding.subtitle) {
|
binding.progressCircular.isVisible = true
|
||||||
binding.title.text = state.media.episode
|
viewModel.item?.let { item ->
|
||||||
text = state.media.show
|
playerViewModel.loadPlayerItems(item) {
|
||||||
isVisible = true
|
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) {
|
private fun bindPlayerItems(items: PlayerItems) {
|
||||||
navigateToPlayerActivity(items.items.toTypedArray())
|
navigateToPlayerActivity(items.items.toTypedArray())
|
||||||
binding.playButton.setImageDrawable(
|
binding.playButton.setImageDrawable(
|
||||||
|
@ -132,59 +211,6 @@ internal class MediaDetailFragment : Fragment() {
|
||||||
binding.progressCircular.visibility = View.INVISIBLE
|
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(
|
private fun navigateToPlayerActivity(
|
||||||
playerItems: Array<PlayerItem>,
|
playerItems: Array<PlayerItem>,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package dev.jdtech.jellyfin.tv.ui
|
package dev.jdtech.jellyfin.tv.ui
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.Transformations
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.R
|
import dev.jdtech.jellyfin.R
|
||||||
|
@ -14,37 +12,35 @@ import javax.inject.Inject
|
||||||
internal class MediaDetailViewModel @Inject internal constructor() : ViewModel() {
|
internal class MediaDetailViewModel @Inject internal constructor() : ViewModel() {
|
||||||
|
|
||||||
fun transformData(
|
fun transformData(
|
||||||
data: LiveData<BaseItemDto>,
|
data: BaseItemDto,
|
||||||
resources: Resources,
|
resources: Resources,
|
||||||
transformed: (State) -> Unit
|
transformed: (State) -> Unit
|
||||||
): LiveData<State> {
|
): State {
|
||||||
return Transformations.map(data) { baseItemDto ->
|
return State(
|
||||||
State(
|
dto = data,
|
||||||
dto = baseItemDto,
|
description = data.overview.orEmpty(),
|
||||||
description = baseItemDto.overview.orEmpty(),
|
year = data.productionYear.toString(),
|
||||||
year = baseItemDto.productionYear.toString(),
|
officialRating = data.officialRating.orEmpty(),
|
||||||
officialRating = baseItemDto.officialRating.orEmpty(),
|
communityRating = data.communityRating.toString(),
|
||||||
communityRating = baseItemDto.communityRating.toString(),
|
|
||||||
runtimeMinutes = String.format(
|
runtimeMinutes = String.format(
|
||||||
resources.getString(R.string.runtime_minutes),
|
resources.getString(R.string.runtime_minutes),
|
||||||
baseItemDto.runTimeTicks?.div(600_000_000)
|
data.runTimeTicks?.div(600_000_000)
|
||||||
),
|
),
|
||||||
genres = baseItemDto.genres?.joinToString(" / ").orEmpty(),
|
genres = data.genres?.joinToString(" / ").orEmpty(),
|
||||||
trailerUrl = baseItemDto.remoteTrailers?.firstOrNull()?.url,
|
trailerUrl = data.remoteTrailers?.firstOrNull()?.url,
|
||||||
isPlayed = baseItemDto.userData?.played == true,
|
isPlayed = data.userData?.played == true,
|
||||||
isFavorite = baseItemDto.userData?.isFavorite == true,
|
isFavorite = data.userData?.isFavorite == true,
|
||||||
media = if (baseItemDto.type == MOVIE.type) {
|
media = if (data.type == MOVIE.type) {
|
||||||
State.Movie(
|
State.Movie(
|
||||||
title = baseItemDto.name.orEmpty()
|
title = data.name.orEmpty()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
State.TvShow(
|
State.TvShow(
|
||||||
episode = baseItemDto.episodeTitle ?: baseItemDto.name.orEmpty(),
|
episode = data.episodeTitle ?: data.name.orEmpty(),
|
||||||
show = baseItemDto.seriesName.orEmpty()
|
show = data.seriesName.orEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
).also(transformed)
|
).also(transformed)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data class State(
|
data class State(
|
||||||
|
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import androidx.core.content.getSystemService
|
import androidx.core.content.getSystemService
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import dev.jdtech.jellyfin.models.DownloadMetadata
|
import dev.jdtech.jellyfin.models.DownloadMetadata
|
||||||
import dev.jdtech.jellyfin.models.DownloadRequestItem
|
import dev.jdtech.jellyfin.models.DownloadRequestItem
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
|
@ -18,7 +17,7 @@ import java.util.UUID
|
||||||
|
|
||||||
var defaultStorage: File? = null
|
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)
|
val downloadRequest = DownloadManager.Request(uri)
|
||||||
.setTitle(downloadRequestItem.metadata.name)
|
.setTitle(downloadRequestItem.metadata.name)
|
||||||
.setDescription("Downloading")
|
.setDescription("Downloading")
|
||||||
|
@ -32,7 +31,7 @@ fun requestDownload(uri: Uri, downloadRequestItem: DownloadRequestItem, context:
|
||||||
)
|
)
|
||||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
if (!File(defaultStorage, downloadRequestItem.itemId.toString()).exists())
|
if (!File(defaultStorage, downloadRequestItem.itemId.toString()).exists())
|
||||||
downloadFile(downloadRequest, context.requireContext())
|
downloadFile(downloadRequest, context)
|
||||||
createMetadataFile(
|
createMetadataFile(
|
||||||
downloadRequestItem.metadata,
|
downloadRequestItem.metadata,
|
||||||
downloadRequestItem.itemId)
|
downloadRequestItem.itemId)
|
||||||
|
|
|
@ -1,68 +1,63 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import androidx.lifecycle.*
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dev.jdtech.jellyfin.models.DownloadSection
|
import dev.jdtech.jellyfin.models.DownloadSection
|
||||||
import dev.jdtech.jellyfin.utils.loadDownloadedEpisodes
|
import dev.jdtech.jellyfin.utils.loadDownloadedEpisodes
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class DownloadViewModel : ViewModel() {
|
class DownloadViewModel : ViewModel() {
|
||||||
private val _downloadSections = MutableLiveData<List<DownloadSection>>()
|
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||||
val downloadSections: LiveData<List<DownloadSection>> = _downloadSections
|
|
||||||
|
|
||||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
sealed class UiState {
|
||||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
data class Normal(val downloadSections: List<DownloadSection>) : UiState()
|
||||||
|
object Loading : UiState()
|
||||||
|
data class Error(val message: String?) : UiState()
|
||||||
|
}
|
||||||
|
|
||||||
private val _error = MutableLiveData<String>()
|
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||||
val error: LiveData<String> = _error
|
scope.launch { uiState.collect { collector(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ResourceType")
|
|
||||||
fun loadData() {
|
fun loadData() {
|
||||||
_error.value = null
|
|
||||||
_finishedLoading.value = false
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
val items = loadDownloadedEpisodes()
|
val items = loadDownloadedEpisodes()
|
||||||
if (items.isEmpty()) {
|
if (items.isEmpty()) {
|
||||||
_downloadSections.value = listOf()
|
uiState.emit(UiState.Normal(emptyList()))
|
||||||
_finishedLoading.value = true
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val tempDownloadSections = mutableListOf<DownloadSection>()
|
val downloadSections = mutableListOf<DownloadSection>()
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
DownloadSection(
|
|
||||||
UUID.randomUUID(),
|
|
||||||
"Episodes",
|
|
||||||
items.filter { it.metadata?.type == "Episode"}).let {
|
|
||||||
if (it.items.isNotEmpty()) tempDownloadSections.add(
|
|
||||||
it
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DownloadSection(
|
DownloadSection(
|
||||||
UUID.randomUUID(),
|
UUID.randomUUID(),
|
||||||
"Movies",
|
"Episodes",
|
||||||
items.filter { it.metadata?.type == "Movie" }).let {
|
items.filter { it.metadata?.type == "Episode" }).let {
|
||||||
if (it.items.isNotEmpty()) tempDownloadSections.add(
|
if (it.items.isNotEmpty()) downloadSections.add(
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_downloadSections.value = tempDownloadSections
|
DownloadSection(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
"Movies",
|
||||||
|
items.filter { it.metadata?.type == "Movie" }).let {
|
||||||
|
if (it.items.isNotEmpty()) downloadSections.add(
|
||||||
|
it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uiState.emit(UiState.Normal(downloadSections))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
uiState.emit(UiState.Error(e.message))
|
||||||
_error.value = e.toString()
|
|
||||||
}
|
}
|
||||||
_finishedLoading.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,23 +1,18 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.*
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.models.DownloadMetadata
|
|
||||||
import dev.jdtech.jellyfin.models.DownloadRequestItem
|
import dev.jdtech.jellyfin.models.DownloadRequestItem
|
||||||
import dev.jdtech.jellyfin.models.PlayerItem
|
import dev.jdtech.jellyfin.models.PlayerItem
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import dev.jdtech.jellyfin.utils.baseItemDtoToDownloadMetadata
|
import dev.jdtech.jellyfin.utils.*
|
||||||
import dev.jdtech.jellyfin.utils.deleteDownloadedEpisode
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
|
import kotlinx.coroutines.flow.collect
|
||||||
import dev.jdtech.jellyfin.utils.itemIsDownloaded
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
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 timber.log.Timber
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.time.ZoneOffset
|
import java.time.ZoneOffset
|
||||||
|
@ -29,91 +24,126 @@ import javax.inject.Inject
|
||||||
class EpisodeBottomSheetViewModel
|
class EpisodeBottomSheetViewModel
|
||||||
@Inject
|
@Inject
|
||||||
constructor(
|
constructor(
|
||||||
|
private val application: Application,
|
||||||
private val jellyfinRepository: JellyfinRepository
|
private val jellyfinRepository: JellyfinRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||||
|
|
||||||
private val _item = MutableLiveData<BaseItemDto>()
|
sealed class UiState {
|
||||||
val item: LiveData<BaseItemDto> = _item
|
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>()
|
object Loading : UiState()
|
||||||
val runTime: LiveData<String> = _runTime
|
data class Error(val message: String?) : UiState()
|
||||||
|
}
|
||||||
|
|
||||||
private val _dateString = MutableLiveData<String>()
|
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||||
val dateString: LiveData<String> = _dateString
|
scope.launch { uiState.collect { collector(it) } }
|
||||||
|
}
|
||||||
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
|
|
||||||
|
|
||||||
|
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()
|
var playerItems: MutableList<PlayerItem> = mutableListOf()
|
||||||
|
|
||||||
lateinit var downloadRequestItem: DownloadRequestItem
|
lateinit var downloadRequestItem: DownloadRequestItem
|
||||||
|
|
||||||
fun loadEpisode(episodeId: UUID) {
|
fun loadEpisode(episodeId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
_downloaded.value = itemIsDownloaded(episodeId)
|
val tempItem = jellyfinRepository.getItem(episodeId)
|
||||||
val item = jellyfinRepository.getItem(episodeId)
|
item = tempItem
|
||||||
_item.value = item
|
runTime = "${tempItem.runTimeTicks?.div(600000000)} min"
|
||||||
_runTime.value = "${item.runTimeTicks?.div(600000000)} min"
|
dateString = getDateString(tempItem)
|
||||||
_dateString.value = getDateString(item)
|
played = tempItem.userData?.played == true
|
||||||
_played.value = item.userData?.played
|
favorite = tempItem.userData?.isFavorite == true
|
||||||
_favorite.value = item.userData?.isFavorite
|
downloaded = itemIsDownloaded(episodeId)
|
||||||
|
uiState.emit(
|
||||||
|
UiState.Normal(
|
||||||
|
tempItem,
|
||||||
|
runTime,
|
||||||
|
dateString,
|
||||||
|
played,
|
||||||
|
favorite,
|
||||||
|
downloaded,
|
||||||
|
downloadEpisode
|
||||||
|
)
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
uiState.emit(UiState.Error(e.message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadEpisode(playerItem : PlayerItem){
|
fun loadEpisode(playerItem: PlayerItem) {
|
||||||
playerItems.add(playerItem)
|
viewModelScope.launch {
|
||||||
_item.value = downloadMetadataToBaseItemDto(playerItem.metadata!!)
|
uiState.emit(UiState.Loading)
|
||||||
|
playerItems.add(playerItem)
|
||||||
|
item = downloadMetadataToBaseItemDto(playerItem.metadata!!)
|
||||||
|
uiState.emit(
|
||||||
|
UiState.Normal(
|
||||||
|
item!!,
|
||||||
|
runTime,
|
||||||
|
dateString,
|
||||||
|
played,
|
||||||
|
favorite,
|
||||||
|
downloaded,
|
||||||
|
downloadEpisode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markAsPlayed(itemId: UUID) {
|
fun markAsPlayed(itemId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
jellyfinRepository.markAsPlayed(itemId)
|
jellyfinRepository.markAsPlayed(itemId)
|
||||||
}
|
}
|
||||||
_played.value = true
|
played = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markAsUnplayed(itemId: UUID) {
|
fun markAsUnplayed(itemId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
jellyfinRepository.markAsUnplayed(itemId)
|
jellyfinRepository.markAsUnplayed(itemId)
|
||||||
}
|
}
|
||||||
_played.value = false
|
played = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markAsFavorite(itemId: UUID) {
|
fun markAsFavorite(itemId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
jellyfinRepository.markAsFavorite(itemId)
|
jellyfinRepository.markAsFavorite(itemId)
|
||||||
}
|
}
|
||||||
_favorite.value = true
|
favorite = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unmarkAsFavorite(itemId: UUID) {
|
fun unmarkAsFavorite(itemId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
jellyfinRepository.unmarkAsFavorite(itemId)
|
jellyfinRepository.unmarkAsFavorite(itemId)
|
||||||
}
|
}
|
||||||
_favorite.value = false
|
favorite = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadDownloadRequestItem(itemId: UUID) {
|
fun loadDownloadRequestItem(itemId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
loadEpisode(itemId)
|
//loadEpisode(itemId)
|
||||||
val episode = _item.value
|
val episode = item
|
||||||
val uri = jellyfinRepository.getStreamUrl(itemId, episode?.mediaSources?.get(0)?.id!!)
|
val uri = jellyfinRepository.getStreamUrl(itemId, episode?.mediaSources?.get(0)?.id!!)
|
||||||
|
Timber.d(uri)
|
||||||
val metadata = baseItemDtoToDownloadMetadata(episode)
|
val metadata = baseItemDtoToDownloadMetadata(episode)
|
||||||
downloadRequestItem = DownloadRequestItem(uri, itemId, metadata)
|
downloadRequestItem = DownloadRequestItem(uri, itemId, metadata)
|
||||||
_downloadEpisode.value = true
|
downloadEpisode = true
|
||||||
|
requestDownload(Uri.parse(downloadRequestItem.uri), downloadRequestItem, application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,7 +163,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doneDownloadEpisode() {
|
fun doneDownloadEpisode() {
|
||||||
_downloadEpisode.value = false
|
downloadEpisode = false
|
||||||
_downloaded.value = true
|
downloaded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,16 +1,16 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.models.FavoriteSection
|
import dev.jdtech.jellyfin.models.FavoriteSection
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -20,40 +20,41 @@ class FavoriteViewModel
|
||||||
constructor(
|
constructor(
|
||||||
private val jellyfinRepository: JellyfinRepository
|
private val jellyfinRepository: JellyfinRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _favoriteSections = MutableLiveData<List<FavoriteSection>>()
|
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||||
val favoriteSections: LiveData<List<FavoriteSection>> = _favoriteSections
|
|
||||||
|
|
||||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
sealed class UiState {
|
||||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
data class Normal(val favoriteSections: List<FavoriteSection>) : UiState()
|
||||||
|
object Loading : UiState()
|
||||||
|
data class Error(val message: String?) : UiState()
|
||||||
|
}
|
||||||
|
|
||||||
private val _error = MutableLiveData<String>()
|
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||||
val error: LiveData<String> = _error
|
scope.launch { uiState.collect { collector(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadData() {
|
fun loadData() {
|
||||||
_error.value = null
|
|
||||||
_finishedLoading.value = false
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
val items = jellyfinRepository.getFavoriteItems()
|
val items = jellyfinRepository.getFavoriteItems()
|
||||||
|
|
||||||
if (items.isEmpty()) {
|
if (items.isEmpty()) {
|
||||||
_favoriteSections.value = listOf()
|
uiState.emit(UiState.Normal(emptyList()))
|
||||||
_finishedLoading.value = true
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val tempFavoriteSections = mutableListOf<FavoriteSection>()
|
val favoriteSections = mutableListOf<FavoriteSection>()
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
FavoriteSection(
|
FavoriteSection(
|
||||||
UUID.randomUUID(),
|
UUID.randomUUID(),
|
||||||
"Movies",
|
"Movies",
|
||||||
items.filter { it.type == "Movie" }).let {
|
items.filter { it.type == "Movie" }).let {
|
||||||
if (it.items.isNotEmpty()) tempFavoriteSections.add(
|
if (it.items.isNotEmpty()) favoriteSections.add(
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -61,7 +62,7 @@ constructor(
|
||||||
UUID.randomUUID(),
|
UUID.randomUUID(),
|
||||||
"Shows",
|
"Shows",
|
||||||
items.filter { it.type == "Series" }).let {
|
items.filter { it.type == "Series" }).let {
|
||||||
if (it.items.isNotEmpty()) tempFavoriteSections.add(
|
if (it.items.isNotEmpty()) favoriteSections.add(
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -69,18 +70,16 @@ constructor(
|
||||||
UUID.randomUUID(),
|
UUID.randomUUID(),
|
||||||
"Episodes",
|
"Episodes",
|
||||||
items.filter { it.type == "Episode" }).let {
|
items.filter { it.type == "Episode" }).let {
|
||||||
if (it.items.isNotEmpty()) tempFavoriteSections.add(
|
if (it.items.isNotEmpty()) favoriteSections.add(
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_favoriteSections.value = tempFavoriteSections
|
uiState.emit(UiState.Normal(favoriteSections))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
uiState.emit(UiState.Error(e.message))
|
||||||
_error.value = e.toString()
|
|
||||||
}
|
}
|
||||||
_finishedLoading.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,8 +2,6 @@ package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.LifecycleCoroutineScope
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.syncPlaybackProgress
|
||||||
import dev.jdtech.jellyfin.utils.toView
|
import dev.jdtech.jellyfin.utils.toView
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject internal constructor(
|
class HomeViewModel @Inject internal constructor(
|
||||||
application: Application,
|
private val application: Application,
|
||||||
private val repository: JellyfinRepository
|
private val repository: JellyfinRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||||
|
|
||||||
private val views = MutableLiveData<List<HomeItem>>()
|
sealed class UiState {
|
||||||
private val state = MutableSharedFlow<State>(
|
data class Normal(val homeItems: List<HomeItem>) : UiState()
|
||||||
replay = 0,
|
object Loading : UiState()
|
||||||
extraBufferCapacity = 1,
|
data class Error(val message: String?) : UiState()
|
||||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
}
|
||||||
)
|
|
||||||
|
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||||
|
scope.launch { uiState.collect { collector(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadData(updateCapabilities = true)
|
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)
|
fun refreshData() = loadData(updateCapabilities = false)
|
||||||
|
|
||||||
private fun loadData(updateCapabilities: Boolean) {
|
private fun loadData(updateCapabilities: Boolean) {
|
||||||
state.tryEmit(Loading(inProgress = true))
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
if (updateCapabilities) repository.postCapabilities()
|
if (updateCapabilities) repository.postCapabilities()
|
||||||
|
|
||||||
val updated = loadDynamicItems() + loadViews()
|
val updated = loadDynamicItems() + loadViews()
|
||||||
views.postValue(updated)
|
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
syncPlaybackProgress(repository)
|
syncPlaybackProgress(repository)
|
||||||
}
|
}
|
||||||
state.tryEmit(Loading(inProgress = false))
|
uiState.emit(UiState.Normal(updated))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
uiState.emit(UiState.Error(e.message))
|
||||||
state.tryEmit(LoadingError(e.toString()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,11 +68,11 @@ class HomeViewModel @Inject internal constructor(
|
||||||
|
|
||||||
val items = mutableListOf<HomeSection>()
|
val items = mutableListOf<HomeSection>()
|
||||||
if (resumeItems.isNotEmpty()) {
|
if (resumeItems.isNotEmpty()) {
|
||||||
items.add(HomeSection(continueWatchingString, resumeItems))
|
items.add(HomeSection(application.resources.getString(R.string.continue_watching), resumeItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextUpItems.isNotEmpty()) {
|
if (nextUpItems.isNotEmpty()) {
|
||||||
items.add(HomeSection(nextUpString, nextUpItems))
|
items.add(HomeSection(application.resources.getString(R.string.next_up), nextUpItems))
|
||||||
}
|
}
|
||||||
|
|
||||||
items.map { Section(it) }
|
items.map { Section(it) }
|
||||||
|
@ -102,11 +87,6 @@ class HomeViewModel @Inject internal constructor(
|
||||||
.map { (view, latest) -> view.toView().apply { items = latest } }
|
.map { (view, latest) -> view.toView().apply { items = latest } }
|
||||||
.map { ViewItem(it) }
|
.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 dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import dev.jdtech.jellyfin.utils.SortBy
|
import dev.jdtech.jellyfin.utils.SortBy
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import org.jellyfin.sdk.model.api.SortOrder
|
import org.jellyfin.sdk.model.api.SortOrder
|
||||||
|
@ -14,16 +16,20 @@ import javax.inject.Inject
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LibraryViewModel
|
class LibraryViewModel
|
||||||
@Inject
|
@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>>()
|
sealed class UiState {
|
||||||
val items: LiveData<List<BaseItemDto>> = _items
|
data class Normal(val items: List<BaseItemDto>) : UiState()
|
||||||
|
object Loading : UiState()
|
||||||
|
data class Error(val message: String?) : UiState()
|
||||||
|
}
|
||||||
|
|
||||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
scope.launch { uiState.collect { collector(it) } }
|
||||||
|
}
|
||||||
private val _error = MutableLiveData<String>()
|
|
||||||
val error: LiveData<String> = _error
|
|
||||||
|
|
||||||
fun loadItems(
|
fun loadItems(
|
||||||
parentId: UUID,
|
parentId: UUID,
|
||||||
|
@ -31,8 +37,6 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||||
sortBy: SortBy = SortBy.defaultValue,
|
sortBy: SortBy = SortBy.defaultValue,
|
||||||
sortOrder: SortOrder = SortOrder.ASCENDING
|
sortOrder: SortOrder = SortOrder.ASCENDING
|
||||||
) {
|
) {
|
||||||
_error.value = null
|
|
||||||
_finishedLoading.value = false
|
|
||||||
Timber.d("$libraryType")
|
Timber.d("$libraryType")
|
||||||
val itemType = when (libraryType) {
|
val itemType = when (libraryType) {
|
||||||
"movies" -> "Movie"
|
"movies" -> "Movie"
|
||||||
|
@ -40,19 +44,19 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
_items.value = jellyfinRepository.getItems(
|
val items = jellyfinRepository.getItems(
|
||||||
parentId,
|
parentId,
|
||||||
includeTypes = if (itemType != null) listOf(itemType) else null,
|
includeTypes = if (itemType != null) listOf(itemType) else null,
|
||||||
recursive = true,
|
recursive = true,
|
||||||
sortBy = sortBy,
|
sortBy = sortBy,
|
||||||
sortOrder = sortOrder
|
sortOrder = sortOrder
|
||||||
)
|
)
|
||||||
|
uiState.emit(UiState.Normal(items))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
uiState.emit(UiState.Error(e.message))
|
||||||
_error.value = e.toString()
|
|
||||||
}
|
}
|
||||||
_finishedLoading.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.deleteDownloadedEpisode
|
||||||
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
|
import dev.jdtech.jellyfin.utils.downloadMetadataToBaseItemDto
|
||||||
import dev.jdtech.jellyfin.utils.itemIsDownloaded
|
import dev.jdtech.jellyfin.utils.itemIsDownloaded
|
||||||
|
import dev.jdtech.jellyfin.utils.requestDownload
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
|
@ -25,92 +29,134 @@ import javax.inject.Inject
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MediaInfoViewModel
|
class MediaInfoViewModel
|
||||||
@Inject
|
@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>()
|
sealed class UiState {
|
||||||
val item: LiveData<BaseItemDto> = _item
|
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>>()
|
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||||
val actors: LiveData<List<BaseItemPerson>> = _actors
|
scope.launch { uiState.collect { collector(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
private val _director = MutableLiveData<BaseItemPerson>()
|
var item: BaseItemDto? = null
|
||||||
val director: LiveData<BaseItemPerson> = _director
|
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>>()
|
private lateinit var downloadRequestItem: DownloadRequestItem
|
||||||
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
|
|
||||||
|
|
||||||
lateinit var playerItem: PlayerItem
|
lateinit var playerItem: PlayerItem
|
||||||
|
|
||||||
fun loadData(itemId: UUID, itemType: String) {
|
fun loadData(itemId: UUID, itemType: String) {
|
||||||
_error.value = null
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
_downloaded.value = itemIsDownloaded(itemId)
|
val tempItem = jellyfinRepository.getItem(itemId)
|
||||||
_item.value = jellyfinRepository.getItem(itemId)
|
item = tempItem
|
||||||
_actors.value = getActors(_item.value!!)
|
actors = getActors(tempItem)
|
||||||
_director.value = getDirector(_item.value!!)
|
director = getDirector(tempItem)
|
||||||
_writers.value = getWriters(_item.value!!)
|
writers = getWriters(tempItem)
|
||||||
_writersString.value =
|
writersString = writers.joinToString(separator = ", ") { it.name.toString() }
|
||||||
_writers.value?.joinToString(separator = ", ") { it.name.toString() }
|
genresString = tempItem.genres?.joinToString(separator = ", ") ?: ""
|
||||||
_genresString.value = _item.value?.genres?.joinToString(separator = ", ")
|
runTime = "${tempItem.runTimeTicks?.div(600000000)} min"
|
||||||
_runTime.value = "${_item.value?.runTimeTicks?.div(600000000)} min"
|
dateString = getDateString(tempItem)
|
||||||
_dateString.value = getDateString(_item.value!!)
|
played = tempItem.userData?.played ?: false
|
||||||
_played.value = _item.value?.userData?.played
|
favorite = tempItem.userData?.isFavorite ?: false
|
||||||
_favorite.value = _item.value?.userData?.isFavorite
|
downloaded = itemIsDownloaded(itemId)
|
||||||
if (itemType == "Series" || itemType == "Episode") {
|
if (itemType == "Series") {
|
||||||
_nextUp.value = getNextUp(itemId)
|
nextUp = getNextUp(itemId)
|
||||||
_seasons.value = jellyfinRepository.getSeasons(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) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.d(e)
|
||||||
_error.value = e.toString()
|
Timber.d(itemId.toString())
|
||||||
|
uiState.emit(UiState.Error(e.message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadData(playerItem: PlayerItem) {
|
fun loadData(pItem: PlayerItem) {
|
||||||
this.playerItem = playerItem
|
viewModelScope.launch {
|
||||||
_item.value = downloadMetadataToBaseItemDto(playerItem.metadata!!)
|
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>? {
|
private suspend fun getActors(item: BaseItemDto): List<BaseItemPerson> {
|
||||||
val actors: List<BaseItemPerson>?
|
val actors: List<BaseItemPerson>
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
actors = item.people?.filter { it.type == "Actor" }
|
actors = item.people?.filter { it.type == "Actor" } ?: emptyList()
|
||||||
}
|
}
|
||||||
return actors
|
return actors
|
||||||
}
|
}
|
||||||
|
@ -123,10 +169,10 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||||
return director
|
return director
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getWriters(item: BaseItemDto): List<BaseItemPerson>? {
|
private suspend fun getWriters(item: BaseItemDto): List<BaseItemPerson> {
|
||||||
val writers: List<BaseItemPerson>?
|
val writers: List<BaseItemPerson>
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
writers = item.people?.filter { it.type == "Writer" }
|
writers = item.people?.filter { it.type == "Writer" } ?: emptyList()
|
||||||
}
|
}
|
||||||
return writers
|
return writers
|
||||||
}
|
}
|
||||||
|
@ -144,28 +190,28 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
jellyfinRepository.markAsPlayed(itemId)
|
jellyfinRepository.markAsPlayed(itemId)
|
||||||
}
|
}
|
||||||
_played.value = true
|
played = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markAsUnplayed(itemId: UUID) {
|
fun markAsUnplayed(itemId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
jellyfinRepository.markAsUnplayed(itemId)
|
jellyfinRepository.markAsUnplayed(itemId)
|
||||||
}
|
}
|
||||||
_played.value = false
|
played = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun markAsFavorite(itemId: UUID) {
|
fun markAsFavorite(itemId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
jellyfinRepository.markAsFavorite(itemId)
|
jellyfinRepository.markAsFavorite(itemId)
|
||||||
}
|
}
|
||||||
_favorite.value = true
|
favorite = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unmarkAsFavorite(itemId: UUID) {
|
fun unmarkAsFavorite(itemId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
jellyfinRepository.unmarkAsFavorite(itemId)
|
jellyfinRepository.unmarkAsFavorite(itemId)
|
||||||
}
|
}
|
||||||
_favorite.value = false
|
favorite = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDateString(item: BaseItemDto): String {
|
private fun getDateString(item: BaseItemDto): String {
|
||||||
|
@ -191,21 +237,17 @@ constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() {
|
||||||
|
|
||||||
fun loadDownloadRequestItem(itemId: UUID) {
|
fun loadDownloadRequestItem(itemId: UUID) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val downloadItem = _item.value
|
val downloadItem = item
|
||||||
val uri =
|
val uri =
|
||||||
jellyfinRepository.getStreamUrl(itemId, downloadItem?.mediaSources?.get(0)?.id!!)
|
jellyfinRepository.getStreamUrl(itemId, downloadItem?.mediaSources?.get(0)?.id!!)
|
||||||
val metadata = baseItemDtoToDownloadMetadata(downloadItem)
|
val metadata = baseItemDtoToDownloadMetadata(downloadItem)
|
||||||
downloadRequestItem = DownloadRequestItem(uri, itemId, metadata)
|
downloadRequestItem = DownloadRequestItem(uri, itemId, metadata)
|
||||||
_downloadMedia.value = true
|
downloadMedia = true
|
||||||
|
requestDownload(Uri.parse(downloadRequestItem.uri), downloadRequestItem, application)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteItem() {
|
fun deleteItem() {
|
||||||
deleteDownloadedEpisode(playerItem.mediaSourceUri)
|
deleteDownloadedEpisode(playerItem.mediaSourceUri)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun doneDownloadMedia() {
|
|
||||||
_downloadMedia.value = false
|
|
||||||
_downloaded.value = true
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -2,10 +2,12 @@ package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import dev.jdtech.jellyfin.models.unsupportedCollections
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
|
@ -15,38 +17,35 @@ constructor(
|
||||||
private val jellyfinRepository: JellyfinRepository
|
private val jellyfinRepository: JellyfinRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _collections = MutableLiveData<List<BaseItemDto>>()
|
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||||
val collections: LiveData<List<BaseItemDto>> = _collections
|
|
||||||
|
|
||||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
sealed class UiState {
|
||||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
data class Normal(val collections: List<BaseItemDto>) : UiState()
|
||||||
|
object Loading : UiState()
|
||||||
|
data class Error(val message: String?) : UiState()
|
||||||
|
}
|
||||||
|
|
||||||
private val _error = MutableLiveData<String>()
|
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||||
val error: LiveData<String> = _error
|
scope.launch { uiState.collect { collector(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadData()
|
loadData()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadData() {
|
fun loadData() {
|
||||||
_finishedLoading.value = false
|
|
||||||
_error.value = null
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
val items = jellyfinRepository.getItems()
|
val items = jellyfinRepository.getItems()
|
||||||
_collections.value =
|
val collections =
|
||||||
items.filter {
|
items.filter { collection -> unsupportedCollections().none { it.type == collection.collectionType } }
|
||||||
it.collectionType != "homevideos" &&
|
uiState.emit(UiState.Normal(collections))
|
||||||
it.collectionType != "music" &&
|
|
||||||
it.collectionType != "playlists" &&
|
|
||||||
it.collectionType != "boxsets" &&
|
|
||||||
it.collectionType != "books"
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
uiState.emit(
|
||||||
_error.value = e.toString()
|
UiState.Error(e.message)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
_finishedLoading.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.models.ContentType.TVSHOW
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import dev.jdtech.jellyfin.utils.contentType
|
import dev.jdtech.jellyfin.utils.contentType
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||||
import timber.log.Timber
|
|
||||||
import java.lang.Exception
|
import java.lang.Exception
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -21,29 +21,30 @@ internal class PersonDetailViewModel @Inject internal constructor(
|
||||||
private val jellyfinRepository: JellyfinRepository
|
private val jellyfinRepository: JellyfinRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val data = MutableLiveData<PersonOverview>()
|
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||||
val starredIn = MutableLiveData<StarredIn>()
|
|
||||||
|
|
||||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
sealed class UiState {
|
||||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
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>()
|
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||||
val error: LiveData<String> = _error
|
scope.launch { uiState.collect { collector(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
fun loadData(personId: UUID) {
|
fun loadData(personId: UUID) {
|
||||||
_error.value = null
|
|
||||||
_finishedLoading.value = false
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
val personDetail = jellyfinRepository.getItem(personId)
|
val personDetail = jellyfinRepository.getItem(personId)
|
||||||
|
|
||||||
data.postValue(
|
val data = PersonOverview(
|
||||||
PersonOverview(
|
name = personDetail.name.orEmpty(),
|
||||||
name = personDetail.name.orEmpty(),
|
overview = personDetail.overview.orEmpty(),
|
||||||
overview = personDetail.overview.orEmpty(),
|
dto = personDetail
|
||||||
dto = personDetail
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val items = jellyfinRepository.getPersonItems(
|
val items = jellyfinRepository.getPersonItems(
|
||||||
personIds = listOf(personId),
|
personIds = listOf(personId),
|
||||||
includeTypes = listOf(MOVIE, TVSHOW),
|
includeTypes = listOf(MOVIE, TVSHOW),
|
||||||
|
@ -53,13 +54,12 @@ internal class PersonDetailViewModel @Inject internal constructor(
|
||||||
val movies = items.filter { it.contentType() == MOVIE }
|
val movies = items.filter { it.contentType() == MOVIE }
|
||||||
val shows = items.filter { it.contentType() == TVSHOW }
|
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) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
uiState.emit(UiState.Error(e.message))
|
||||||
_error.value = e.toString()
|
|
||||||
}
|
}
|
||||||
_finishedLoading.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.models.FavoriteSection
|
import dev.jdtech.jellyfin.models.FavoriteSection
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -20,36 +20,37 @@ class SearchResultViewModel
|
||||||
constructor(
|
constructor(
|
||||||
private val jellyfinRepository: JellyfinRepository
|
private val jellyfinRepository: JellyfinRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
private val _sections = MutableLiveData<List<FavoriteSection>>()
|
private val uiState = MutableStateFlow<UiState>(UiState.Loading)
|
||||||
val sections: LiveData<List<FavoriteSection>> = _sections
|
|
||||||
|
|
||||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
sealed class UiState {
|
||||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
data class Normal(val sections: List<FavoriteSection>) : UiState()
|
||||||
|
object Loading : UiState()
|
||||||
|
data class Error(val message: String?) : UiState()
|
||||||
|
}
|
||||||
|
|
||||||
private val _error = MutableLiveData<String>()
|
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||||
val error: LiveData<String> = _error
|
scope.launch { uiState.collect { collector(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
fun loadData(query: String) {
|
fun loadData(query: String) {
|
||||||
_error.value = null
|
|
||||||
_finishedLoading.value = false
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
val items = jellyfinRepository.getSearchItems(query)
|
val items = jellyfinRepository.getSearchItems(query)
|
||||||
|
|
||||||
if (items.isEmpty()) {
|
if (items.isEmpty()) {
|
||||||
_sections.value = listOf()
|
uiState.emit(UiState.Normal(emptyList()))
|
||||||
_finishedLoading.value = true
|
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val tempSections = mutableListOf<FavoriteSection>()
|
val sections = mutableListOf<FavoriteSection>()
|
||||||
|
|
||||||
withContext(Dispatchers.Default) {
|
withContext(Dispatchers.Default) {
|
||||||
FavoriteSection(
|
FavoriteSection(
|
||||||
UUID.randomUUID(),
|
UUID.randomUUID(),
|
||||||
"Movies",
|
"Movies",
|
||||||
items.filter { it.type == "Movie" }).let {
|
items.filter { it.type == "Movie" }).let {
|
||||||
if (it.items.isNotEmpty()) tempSections.add(
|
if (it.items.isNotEmpty()) sections.add(
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -57,7 +58,7 @@ constructor(
|
||||||
UUID.randomUUID(),
|
UUID.randomUUID(),
|
||||||
"Shows",
|
"Shows",
|
||||||
items.filter { it.type == "Series" }).let {
|
items.filter { it.type == "Series" }).let {
|
||||||
if (it.items.isNotEmpty()) tempSections.add(
|
if (it.items.isNotEmpty()) sections.add(
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -65,18 +66,16 @@ constructor(
|
||||||
UUID.randomUUID(),
|
UUID.randomUUID(),
|
||||||
"Episodes",
|
"Episodes",
|
||||||
items.filter { it.type == "Episode" }).let {
|
items.filter { it.type == "Episode" }).let {
|
||||||
if (it.items.isNotEmpty()) tempSections.add(
|
if (it.items.isNotEmpty()) sections.add(
|
||||||
it
|
it
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_sections.value = tempSections
|
uiState.emit(UiState.Normal(sections))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
uiState.emit(UiState.Error(e.message))
|
||||||
_error.value = e.toString()
|
|
||||||
}
|
}
|
||||||
_finishedLoading.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,48 +1,49 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.*
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dev.jdtech.jellyfin.adapters.EpisodeItem
|
import dev.jdtech.jellyfin.adapters.EpisodeItem
|
||||||
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
import dev.jdtech.jellyfin.repository.JellyfinRepository
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.jellyfin.sdk.model.api.ItemFields
|
import org.jellyfin.sdk.model.api.ItemFields
|
||||||
import timber.log.Timber
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SeasonViewModel
|
class SeasonViewModel
|
||||||
@Inject
|
@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>>()
|
sealed class UiState {
|
||||||
val episodes: LiveData<List<EpisodeItem>> = _episodes
|
data class Normal(val episodes: List<EpisodeItem>) : UiState()
|
||||||
|
object Loading : UiState()
|
||||||
|
data class Error(val message: String?) : UiState()
|
||||||
|
}
|
||||||
|
|
||||||
private val _finishedLoading = MutableLiveData<Boolean>()
|
fun onUiState(scope: LifecycleCoroutineScope, collector: (UiState) -> Unit) {
|
||||||
val finishedLoading: LiveData<Boolean> = _finishedLoading
|
scope.launch { uiState.collect { collector(it) } }
|
||||||
|
}
|
||||||
private val _error = MutableLiveData<String>()
|
|
||||||
val error: LiveData<String> = _error
|
|
||||||
|
|
||||||
fun loadEpisodes(seriesId: UUID, seasonId: UUID) {
|
fun loadEpisodes(seriesId: UUID, seasonId: UUID) {
|
||||||
_error.value = null
|
|
||||||
_finishedLoading.value = false
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
|
uiState.emit(UiState.Loading)
|
||||||
try {
|
try {
|
||||||
_episodes.value = getEpisodes(seriesId, seasonId)
|
val episodes = getEpisodes(seriesId, seasonId)
|
||||||
|
uiState.emit(UiState.Normal(episodes))
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
uiState.emit(UiState.Error(e.message))
|
||||||
_error.value = e.toString()
|
|
||||||
}
|
}
|
||||||
_finishedLoading.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getEpisodes(seriesId: UUID, seasonId: UUID): List<EpisodeItem> {
|
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) }
|
return listOf(EpisodeItem.Header) + episodes.map { EpisodeItem.Episode(it) }
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
package dev.jdtech.jellyfin.viewmodels
|
package dev.jdtech.jellyfin.viewmodels
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LifecycleCoroutineScope
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
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.Server
|
||||||
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
import dev.jdtech.jellyfin.database.ServerDatabaseDao
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -23,12 +25,17 @@ constructor(
|
||||||
private val jellyfinApi: JellyfinApi,
|
private val jellyfinApi: JellyfinApi,
|
||||||
private val database: ServerDatabaseDao,
|
private val database: ServerDatabaseDao,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
val servers = database.getAllServers()
|
||||||
|
|
||||||
private val _servers = database.getAllServers()
|
private val navigateToMain = MutableSharedFlow<Boolean>(
|
||||||
val servers: LiveData<List<Server>> = _servers
|
replay = 0,
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
|
||||||
private val _navigateToMain = MutableLiveData<Boolean>()
|
fun onNavigateToMain(scope: LifecycleCoroutineScope, collector: (Boolean) -> Unit) {
|
||||||
val navigateToMain: LiveData<Boolean> = _navigateToMain
|
scope.launch { navigateToMain.collect { collector(it) } }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete server from database
|
* Delete server from database
|
||||||
|
@ -54,10 +61,6 @@ constructor(
|
||||||
userId = UUID.fromString(server.userId)
|
userId = UUID.fromString(server.userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
_navigateToMain.value = true
|
navigateToMain.tryEmit(true)
|
||||||
}
|
|
||||||
|
|
||||||
fun doneNavigatingToMain() {
|
|
||||||
_navigateToMain.value = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,297 +1,250 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
tools:ignore="MissingDefaultResource"
|
android:layout_width="match_parent"
|
||||||
>
|
android:layout_height="match_parent"
|
||||||
|
tools:ignore="MissingDefaultResource">
|
||||||
|
|
||||||
<data>
|
<include
|
||||||
|
android:id="@+id/error_layout"
|
||||||
|
layout="@layout/error_panel"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
<import type="android.view.View" />
|
<ScrollView
|
||||||
|
|
||||||
<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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content">
|
||||||
>
|
|
||||||
|
|
||||||
<include
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/error_layout"
|
|
||||||
layout="@layout/error_panel"
|
|
||||||
tools:visibility="gone"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ScrollView
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<ImageButton
|
||||||
|
android:id="@+id/back_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
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"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/back_button"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Alita: Battle Angel" />
|
||||||
|
|
||||||
|
<TextClock
|
||||||
|
android:id="@+id/clock"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="18sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="12:00" />
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/poster"
|
||||||
|
android:layout_width="320dp"
|
||||||
|
android:layout_height="180dp"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/title" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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"
|
||||||
|
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:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/poster"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/subtitle"
|
||||||
|
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"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/poster"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||||
|
tools:text="2019" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/playtime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/year"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||||
|
tools:text="122 min" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/official_rating"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/playtime"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/genres"
|
||||||
|
tools:text="PG-13" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/community_rating"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
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
|
||||||
|
android:id="@+id/description"
|
||||||
|
android:layout_width="400dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
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." />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_circular"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:indeterminateTint="@color/white"
|
||||||
|
android:padding="8dp"
|
||||||
|
android:visibility="invisible"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/poster"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/description" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/play_button"
|
||||||
|
android:layout_width="72dp"
|
||||||
|
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"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/poster"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/description" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/trailer_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
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"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/play_button"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/description" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/check_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:contentDescription="@string/check_button_description"
|
||||||
|
android:focusable="true"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:src="@drawable/ic_check"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/trailer_button"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/description" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/favorite_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/season_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
>
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/season_title" />
|
||||||
|
|
||||||
<ImageButton
|
<TextView
|
||||||
android:id="@+id/back_button"
|
android:id="@+id/cast_title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/transparent_circle_background"
|
android:layout_marginStart="@dimen/horizontal_margin"
|
||||||
android:contentDescription="@string/player_controls_exit"
|
android:layout_marginTop="16dp"
|
||||||
android:padding="16dp"
|
android:text="@string/cast_amp_crew"
|
||||||
android:src="@drawable/ic_arrow_left"
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline3"
|
||||||
android:focusable="true"
|
android:visibility="gone"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toBottomOf="@id/seasons_row" />
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
<androidx.leanback.widget.ListRowView
|
||||||
android:id="@+id/title"
|
android:id="@+id/cast_row"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="24dp"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:layout_marginTop="8dp"
|
app:layout_constraintTop_toBottomOf="@id/cast_title" />
|
||||||
android:paddingBottom="16dp"
|
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/back_button"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="Alita: Battle Angel"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextClock
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:id="@+id/clock"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:layout_marginEnd="24dp"
|
|
||||||
tools:text="12:00"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
</ScrollView>
|
||||||
android:id="@+id/poster"
|
|
||||||
android:layout_width="320dp"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextView
|
</FrameLayout>
|
||||||
android:id="@+id/subtitle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
|
||||||
android:layout_marginStart="@dimen/horizontal_margin"
|
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/poster"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/title"
|
|
||||||
android:visibility="gone"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
tools:text="2019" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playtime"
|
|
||||||
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"
|
|
||||||
tools:text="122 min" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/official_rating"
|
|
||||||
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"
|
|
||||||
tools:text="PG-13" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/community_rating"
|
|
||||||
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"
|
|
||||||
tools:text="7.3" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/description"
|
|
||||||
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: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."
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/play_button"
|
|
||||||
android:layout_width="72dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_marginStart="@dimen/horizontal_margin"
|
|
||||||
android:contentDescription="@string/play_button_description"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/trailer_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:contentDescription="@string/trailer_button_description"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:src="@drawable/ic_film"
|
|
||||||
android:focusable="true"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/play_button"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/description"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/check_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:contentDescription="@string/check_button_description"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:focusable="true"
|
|
||||||
android:src="@drawable/ic_check"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/trailer_button"
|
|
||||||
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"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/check_button"
|
|
||||||
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:textAppearance="@style/TextAppearance.MaterialComponents.Headline3"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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:textAppearance="@style/TextAppearance.MaterialComponents.Headline3"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
</layout>
|
|
|
@ -1,259 +1,265 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingBottom="24dp">
|
||||||
|
|
||||||
<data>
|
<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" />
|
||||||
|
|
||||||
<import type="android.view.View" />
|
<ImageView
|
||||||
|
android:id="@+id/holder"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:src="@drawable/ic_minus_fat"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="?attr/colorOnSurface" />
|
||||||
|
|
||||||
<import type="org.jellyfin.sdk.model.api.LocationType" />
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/episode_image"
|
||||||
|
android:layout_width="142dp"
|
||||||
|
android:layout_height="85dp"
|
||||||
|
android:layout_marginStart="24dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/holder"
|
||||||
|
app:shapeAppearance="@style/ShapeAppearanceOverlay.Findroid.Image" />
|
||||||
|
|
||||||
<variable
|
<FrameLayout
|
||||||
name="viewModel"
|
android:id="@+id/missing_icon"
|
||||||
type="dev.jdtech.jellyfin.viewmodels.EpisodeBottomSheetViewModel" />
|
android:layout_width="24dp"
|
||||||
</data>
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_width="match_parent"
|
android:background="@drawable/circle_background"
|
||||||
android:layout_height="match_parent"
|
android:backgroundTint="?attr/colorError"
|
||||||
android:paddingBottom="24dp">
|
app:layout_constraintEnd_toEndOf="@id/episode_image"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/episode_image"
|
||||||
<ImageView
|
android:visibility="gone"
|
||||||
android:id="@+id/holder"
|
tools:visibility="visible">
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
android:src="@drawable/ic_minus_fat"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:tint="?attr/colorOnSurface" />
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/episode_image"
|
|
||||||
android:layout_width="142dp"
|
|
||||||
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" />
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/missing_icon"
|
|
||||||
android:layout_width="24dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
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">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:text="M"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
tools:ignore="HardcodedText" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/progress_bar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="4dp"
|
|
||||||
android:layout_marginHorizontal="8dp"
|
|
||||||
android:layout_marginBottom="8dp"
|
|
||||||
android:background="@drawable/button_setup_background"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/episode_image"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/episode_image"
|
|
||||||
tools:layout_width="50dp"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/episode_name"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="0dp"
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="M"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/progress_bar"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="4dp"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:background="@drawable/button_setup_background"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/episode_image"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/episode_image"
|
||||||
|
tools:layout_width="50dp"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/episode_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/episode_image"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/episode_image"
|
||||||
|
tools:text="1. To You, in 2000 Years: The Fall of Shiganshina, Part 1" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/episode_metadata"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/episode_image"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/episode_image">
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/year"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginEnd="24dp"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
android:text="@{String.format(@string/episode_name_extended, viewModel.item.parentIndexNumber, viewModel.item.indexNumber, viewModel.item.name)}"
|
tools:text="4/6/2013" />
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/episode_image"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/episode_image"
|
|
||||||
tools:text="1. To You, in 2000 Years: The Fall of Shiganshina, Part 1" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<TextView
|
||||||
android:id="@+id/episode_metadata"
|
android:id="@+id/playtime"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginEnd="24dp"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
tools:text="26 min" />
|
||||||
app:layout_constraintStart_toStartOf="@id/episode_image"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/episode_image">
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
<TextView
|
android:id="@+id/community_rating"
|
||||||
android:id="@+id/year"
|
android:layout_width="wrap_content"
|
||||||
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" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/playtime"
|
|
||||||
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" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/community_rating"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
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"
|
|
||||||
tools:text="8.8" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/buttons"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:drawablePadding="4dp"
|
||||||
android:layout_marginTop="12dp"
|
android:gravity="bottom"
|
||||||
android:layout_marginBottom="24dp"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:drawableStartCompat="@drawable/ic_star"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:drawableTint="@color/yellow"
|
||||||
app:layout_constraintTop_toBottomOf="@id/episode_metadata">
|
tools:text="8.8" />
|
||||||
|
|
||||||
<RelativeLayout
|
</LinearLayout>
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="12dp">
|
|
||||||
|
|
||||||
<ImageButton
|
<LinearLayout
|
||||||
android:id="@+id/play_button"
|
android:id="@+id/buttons"
|
||||||
android:layout_width="72dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="48dp"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/button_setup_background"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:contentDescription="@string/play_button_description"
|
android:layout_marginTop="12dp"
|
||||||
android:foreground="@drawable/ripple_background"
|
android:layout_marginBottom="24dp"
|
||||||
android:paddingHorizontal="24dp"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:paddingVertical="12dp"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:src="@drawable/ic_play" />
|
app:layout_constraintTop_toBottomOf="@id/episode_metadata">
|
||||||
|
|
||||||
<ProgressBar
|
<RelativeLayout
|
||||||
android:id="@+id/progress_circular"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="48dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="48dp"
|
android:layout_marginEnd="12dp">
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:elevation="8dp"
|
|
||||||
android:indeterminateTint="@color/white"
|
|
||||||
android:padding="8dp"
|
|
||||||
android:visibility="invisible" />
|
|
||||||
</RelativeLayout>
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/check_button"
|
android:id="@+id/play_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="72dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="48dp"
|
||||||
android:layout_marginEnd="12dp"
|
android:background="@drawable/button_setup_background"
|
||||||
android:background="@drawable/button_accent_background"
|
android:contentDescription="@string/play_button_description"
|
||||||
android:contentDescription="@string/check_button_description"
|
android:foreground="@drawable/ripple_background"
|
||||||
android:padding="12dp"
|
android:paddingHorizontal="24dp"
|
||||||
android:src="@drawable/ic_check" />
|
android:paddingVertical="12dp"
|
||||||
|
android:src="@drawable/ic_play" />
|
||||||
|
|
||||||
<ImageButton
|
<ProgressBar
|
||||||
android:id="@+id/favorite_button"
|
android:id="@+id/progress_circular"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="48dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="48dp"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_centerHorizontal="true"
|
||||||
android:background="@drawable/button_accent_background"
|
android:elevation="8dp"
|
||||||
android:contentDescription="@string/favorite_button_description"
|
android:indeterminateTint="@color/white"
|
||||||
android:padding="12dp"
|
android:padding="8dp"
|
||||||
android:src="@drawable/ic_heart" />
|
android:visibility="invisible" />
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/check_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/button_accent_background"
|
||||||
|
android:contentDescription="@string/check_button_description"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:src="@drawable/ic_check" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/favorite_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/button_accent_background"
|
||||||
|
android:contentDescription="@string/favorite_button_description"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:src="@drawable/ic_heart" />
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:id="@+id/download_button_wrapper"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp">
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/download_button"
|
android:id="@+id/download_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="48dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="48dp"
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:background="@drawable/button_accent_background"
|
android:background="@drawable/button_accent_background"
|
||||||
android:contentDescription="@string/download_button_description"
|
android:contentDescription="@string/download_button_description"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:src="@drawable/ic_download" />
|
android:src="@drawable/ic_download" />
|
||||||
|
|
||||||
<ImageButton
|
<ProgressBar
|
||||||
android:id="@+id/delete_button"
|
android:id="@+id/progress_download"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="48dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="48dp"
|
||||||
android:layout_marginEnd="12dp"
|
android:layout_centerHorizontal="true"
|
||||||
android:background="@drawable/button_accent_background"
|
android:elevation="8dp"
|
||||||
android:contentDescription="@string/delete_button_description"
|
android:indeterminateTint="@color/white"
|
||||||
android:padding="12dp"
|
android:padding="8dp"
|
||||||
android:src="@drawable/ic_trash" />
|
android:visibility="invisible" />
|
||||||
</LinearLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<ImageButton
|
||||||
android:id="@+id/player_items_error"
|
android:id="@+id/delete_button"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginEnd="12dp"
|
||||||
android:layout_marginTop="12dp"
|
android:background="@drawable/button_accent_background"
|
||||||
android:layout_marginBottom="12dp"
|
android:contentDescription="@string/delete_button_description"
|
||||||
android:orientation="horizontal"
|
android:padding="12dp"
|
||||||
android:visibility="gone"
|
android:src="@drawable/ic_trash" />
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
</LinearLayout>
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/buttons"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/player_items_error_text"
|
android:id="@+id/player_items_error"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:text="@string/error_preparing_player_items"
|
android:layout_marginTop="12dp"
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
android:layout_marginBottom="12dp"
|
||||||
android:textColor="?attr/colorError" />
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone"
|
||||||
<TextView
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:id="@+id/player_items_error_details"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:layout_width="wrap_content"
|
app:layout_constraintTop_toBottomOf="@id/buttons"
|
||||||
android:layout_height="wrap_content"
|
tools:visibility="visible">
|
||||||
android:text="@string/view_details_underlined"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
android:textColor="?attr/colorError" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="0dp"
|
android:id="@+id/player_items_error_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginEnd="8dp"
|
||||||
android:layout_marginTop="12dp"
|
android:text="@string/error_preparing_player_items"
|
||||||
android:text="@{viewModel.item.overview}"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:textColor="?attr/colorError" />
|
||||||
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>
|
<TextView
|
||||||
|
android:id="@+id/player_items_error_details"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/view_details_underlined"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:textColor="?attr/colorError" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</layout>
|
<TextView
|
||||||
|
android:id="@+id/overview"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
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>
|
|
@ -1,61 +1,50 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<data>
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
<variable
|
android:id="@+id/loading_indicator"
|
||||||
name="viewModel"
|
android:layout_width="wrap_content"
|
||||||
type="dev.jdtech.jellyfin.viewmodels.DownloadViewModel" />
|
android:layout_height="wrap_content"
|
||||||
</data>
|
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" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<include
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/error_layout"
|
||||||
android:layout_height="match_parent">
|
layout="@layout/error_panel" />
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
<TextView
|
||||||
android:id="@+id/loading_indicator"
|
android:id="@+id/no_downloads_text"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:indeterminate="true"
|
android:text="@string/no_downloads"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:trackCornerRadius="10dp" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<include
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/error_layout"
|
android:id="@+id/downloads_recycler_view"
|
||||||
layout="@layout/error_panel" />
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0.0"
|
||||||
|
tools:itemCount="4"
|
||||||
|
tools:listitem="@layout/download_section" />
|
||||||
|
|
||||||
<TextView
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:id="@+id/no_downloads_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/no_downloads"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
android:visibility="gone"/>
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/downloads_recycler_view"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
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"
|
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="0.0"
|
|
||||||
tools:itemCount="4"
|
|
||||||
tools:listitem="@layout/download_section" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</layout>
|
|
|
@ -1,61 +1,48 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<data>
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
<variable
|
android:id="@+id/loading_indicator"
|
||||||
name="viewModel"
|
android:layout_width="0dp"
|
||||||
type="dev.jdtech.jellyfin.viewmodels.FavoriteViewModel" />
|
android:layout_height="wrap_content"
|
||||||
</data>
|
android:indeterminate="true"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<include
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/error_layout"
|
||||||
android:layout_height="match_parent">
|
layout="@layout/error_panel" />
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
<TextView
|
||||||
android:id="@+id/loading_indicator"
|
android:id="@+id/no_favorites_text"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:indeterminate="true"
|
android:text="@string/no_favorites"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:visibility="gone"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:trackCornerRadius="10dp" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<include
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/error_layout"
|
android:id="@+id/favorites_recycler_view"
|
||||||
layout="@layout/error_panel" />
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0.0"
|
||||||
|
tools:itemCount="4"
|
||||||
|
tools:listitem="@layout/favorite_section" />
|
||||||
|
|
||||||
<TextView
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:id="@+id/no_favorites_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/no_favorites"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
android:visibility="gone"/>
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/favorites_recycler_view"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
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"
|
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:layout_constraintVertical_bias="0.0"
|
|
||||||
tools:itemCount="4"
|
|
||||||
tools:listitem="@layout/favorite_section" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</layout>
|
|
|
@ -1,63 +1,44 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
>
|
android:id="@+id/refresh_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<data>
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|
||||||
<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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
>
|
tools:context=".fragments.HomeFragment">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/loading_indicator"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="0dp"
|
||||||
tools:context=".fragments.HomeFragment"
|
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" />
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
<include
|
||||||
android:id="@+id/loading_indicator"
|
android:id="@+id/error_layout"
|
||||||
android:layout_width="0dp"
|
layout="@layout/error_panel" />
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:indeterminate="true"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
android:visibility="gone"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<include
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/error_layout"
|
android:id="@+id/views_recycler_view"
|
||||||
layout="@layout/error_panel"
|
android:layout_width="0dp"
|
||||||
/>
|
android:layout_height="0dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:itemCount="4"
|
||||||
|
tools:listitem="@layout/view_item" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||||
android:id="@+id/views_recycler_view"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:paddingTop="16dp"
|
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
|
||||||
|
|
||||||
</layout>
|
|
||||||
|
|
|
@ -1,52 +1,42 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".fragments.LibraryFragment">
|
||||||
|
|
||||||
<data>
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
|
android:id="@+id/loading_indicator"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
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" />
|
||||||
|
|
||||||
<variable
|
<include
|
||||||
name="viewModel"
|
android:id="@+id/error_layout"
|
||||||
type="dev.jdtech.jellyfin.viewmodels.LibraryViewModel" />
|
layout="@layout/error_panel" />
|
||||||
</data>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/items_recycler_view"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="0dp"
|
||||||
tools:context=".fragments.LibraryFragment">
|
android:layout_height="0dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:scrollbars="none"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:spanCount="@integer/library_columns"
|
||||||
|
tools:itemCount="6"
|
||||||
|
tools:listitem="@layout/base_item" />
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:id="@+id/loading_indicator"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
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" />
|
|
||||||
|
|
||||||
<include android:id="@+id/error_layout" layout="@layout/error_panel" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/items_recycler_view"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
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"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:spanCount="@integer/library_columns"
|
|
||||||
tools:itemCount="6"
|
|
||||||
tools:listitem="@layout/base_item" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</layout>
|
|
||||||
|
|
||||||
|
|
|
@ -1,51 +1,42 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
tools:context=".fragments.MediaFragment">
|
||||||
|
|
||||||
<data>
|
<com.google.android.material.progressindicator.CircularProgressIndicator
|
||||||
|
android:id="@+id/loading_indicator"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
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" />
|
||||||
|
|
||||||
<variable
|
<include
|
||||||
name="viewModel"
|
android:id="@+id/error_layout"
|
||||||
type="dev.jdtech.jellyfin.viewmodels.MediaViewModel" />
|
layout="@layout/error_panel" />
|
||||||
</data>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/views_recycler_view"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="0dp"
|
||||||
android:animateLayoutChanges="true"
|
android:layout_height="0dp"
|
||||||
tools:context=".fragments.MediaFragment">
|
android:clipToPadding="false"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:scrollbars="none"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:spanCount="@integer/collection_columns"
|
||||||
|
tools:itemCount="4"
|
||||||
|
tools:listitem="@layout/collection_item" />
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:id="@+id/loading_indicator"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
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" />
|
|
||||||
|
|
||||||
<include android:id="@+id/error_layout" layout="@layout/error_panel" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/views_recycler_view"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
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"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:spanCount="@integer/collection_columns"
|
|
||||||
tools:itemCount="4"
|
|
||||||
tools:listitem="@layout/collection_item" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</layout>
|
|
||||||
|
|
|
@ -1,469 +1,439 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<data>
|
<include
|
||||||
|
android:id="@+id/error_layout"
|
||||||
|
layout="@layout/error_panel" />
|
||||||
|
|
||||||
<import type="android.view.View" />
|
<ScrollView
|
||||||
|
android:id="@+id/media_info_scrollview"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<variable
|
<LinearLayout
|
||||||
name="viewModel"
|
android:layout_width="match_parent"
|
||||||
type="dev.jdtech.jellyfin.viewmodels.MediaInfoViewModel" />
|
android:layout_height="wrap_content"
|
||||||
</data>
|
android:orientation="vertical"
|
||||||
|
tools:context=".fragments.MediaInfoFragment">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="200dp"
|
||||||
|
android:layout_marginBottom="8dp">
|
||||||
|
|
||||||
<include
|
<ImageView
|
||||||
android:id="@+id/error_layout"
|
android:id="@+id/item_banner"
|
||||||
layout="@layout/error_panel" />
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="200dp"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<ScrollView
|
<FrameLayout
|
||||||
android:id="@+id/media_info_scrollview"
|
android:layout_width="0dp"
|
||||||
android:layout_width="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_height="0dp"
|
android:background="@drawable/header_gradient"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/original_title"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
tools:text="Alita: Battle Angel" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/original_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical"
|
android:layout_marginHorizontal="24dp"
|
||||||
tools:context=".fragments.MediaInfoFragment">
|
android:layout_marginBottom="16dp">
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/year"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
tools:text="2019" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/playtime"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
tools:text="122 min" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/official_rating"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
tools:text="PG-13" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/community_rating"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
android:gravity="bottom"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
app:drawableStartCompat="@drawable/ic_star"
|
||||||
|
app:drawableTint="@color/yellow"
|
||||||
|
tools:text="7.3" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/play_button"
|
||||||
|
android:layout_width="72dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:background="@drawable/button_setup_background"
|
||||||
|
android:contentDescription="@string/play_button_description"
|
||||||
|
android:foreground="@drawable/ripple_background"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:paddingVertical="12dp"
|
||||||
|
android:src="@drawable/ic_play" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progress_circular"
|
||||||
|
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/trailer_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/button_accent_background"
|
||||||
|
android:contentDescription="@string/trailer_button_description"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:src="@drawable/ic_film" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/check_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/button_accent_background"
|
||||||
|
android:contentDescription="@string/check_button_description"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:src="@drawable/ic_check" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/favorite_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/button_accent_background"
|
||||||
|
android:contentDescription="@string/download_button_description"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:src="@drawable/ic_heart" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/download_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/button_accent_background"
|
||||||
|
android:contentDescription="@string/download_button_description"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:src="@drawable/ic_download" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/delete_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:background="@drawable/button_accent_background"
|
||||||
|
android:contentDescription="@string/delete_button_description"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:src="@drawable/ic_trash" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/player_items_error"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="-12dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/player_items_error_text"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:text="@string/error_preparing_player_items"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:textColor="?attr/colorError" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/player_items_error_details"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/view_details_underlined"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:textColor="?attr/colorError" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/genres_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="200dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="8dp">
|
android:layout_marginBottom="12dp">
|
||||||
|
|
||||||
<ImageView
|
<TextView
|
||||||
android:id="@+id/item_banner"
|
android:id="@+id/genres_title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="200dp"
|
android:layout_height="wrap_content"
|
||||||
android:scaleType="centerCrop"
|
android:text="@string/genres"
|
||||||
app:itemBackdropImage="@{viewModel.item}"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:ignore="ContentDescription" />
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:background="@drawable/header_gradient"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/name"
|
android:id="@+id/genres"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginStart="64dp"
|
||||||
android:text="@{viewModel.item.name}"
|
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/original_title"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
tools:text="Alita: Battle Angel" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/original_title"
|
|
||||||
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"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Action, Science Fiction, Adventure" />
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/director_layout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginBottom="12dp">
|
||||||
android:layout_marginBottom="16dp">
|
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/year"
|
android:id="@+id/director_title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:text="@string/director"
|
||||||
android:text="@{viewModel.dateString}"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
tools:text="2019" />
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/playtime"
|
android:id="@+id/director"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginStart="64dp"
|
||||||
android:text="@{viewModel.runTime}"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
tools:text="122 min" />
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Robert Rodriguez" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/writers_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="12dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/official_rating"
|
android:id="@+id/writers_title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="8dp"
|
android:text="@string/writers"
|
||||||
android:text="@{viewModel.item.officialRating}"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
tools:text="PG-13" />
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/community_rating"
|
android:id="@+id/writers"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:drawablePadding="4dp"
|
android:layout_marginStart="64dp"
|
||||||
android:gravity="bottom"
|
|
||||||
android:text="@{viewModel.item.communityRating.toString()}"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
app:drawableStartCompat="@drawable/ic_star"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:drawableTint="@color/yellow"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
tools:text="7.3" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
</LinearLayout>
|
tools:text="James Cameron, Laeta Kalogridis, Yukito Kishiro" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
|
||||||
android:layout_marginBottom="24dp">
|
|
||||||
|
|
||||||
<RelativeLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="12dp">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/play_button"
|
|
||||||
android:layout_width="72dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:background="@drawable/button_setup_background"
|
|
||||||
android:contentDescription="@string/play_button_description"
|
|
||||||
android:foreground="@drawable/ripple_background"
|
|
||||||
android:paddingHorizontal="24dp"
|
|
||||||
android:paddingVertical="12dp"
|
|
||||||
android:src="@drawable/ic_play" />
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/progress_circular"
|
|
||||||
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/trailer_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:background="@drawable/button_accent_background"
|
|
||||||
android:contentDescription="@string/trailer_button_description"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:src="@drawable/ic_film" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/check_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:background="@drawable/button_accent_background"
|
|
||||||
android:contentDescription="@string/check_button_description"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:src="@drawable/ic_check" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/favorite_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:background="@drawable/button_accent_background"
|
|
||||||
android:contentDescription="@string/download_button_description"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:src="@drawable/ic_heart" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/download_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:background="@drawable/button_accent_background"
|
|
||||||
android:contentDescription="@string/download_button_description"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:src="@drawable/ic_download" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/delete_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="12dp"
|
|
||||||
android:background="@drawable/button_accent_background"
|
|
||||||
android:contentDescription="@string/delete_button_description"
|
|
||||||
android:padding="12dp"
|
|
||||||
android:src="@drawable/ic_trash" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/player_items_error"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
|
||||||
android:layout_marginTop="-12dp"
|
|
||||||
android:layout_marginBottom="12dp"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:visibility="gone"
|
|
||||||
tools:visibility="visible">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/player_items_error_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="8dp"
|
|
||||||
android:text="@string/error_preparing_player_items"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
android:textColor="?attr/colorError" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/player_items_error_details"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/view_details_underlined"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
android:textColor="?attr/colorError" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/info"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
|
||||||
android:layout_marginBottom="12dp"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
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}">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/genres_title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/genres"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/genres"
|
|
||||||
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"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="Action, Science Fiction, Adventure" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
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}">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/director_title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/director"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/director"
|
|
||||||
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"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="Robert Rodriguez" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
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}">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/writers_title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/writers"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/writers"
|
|
||||||
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"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="James Cameron, Laeta Kalogridis, Yukito Kishiro" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/description"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
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: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}">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
|
||||||
android:layout_marginBottom="12dp"
|
|
||||||
android:text="@string/next_up"
|
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
|
||||||
android:textSize="18sp" />
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
|
||||||
android:id="@+id/next_up"
|
|
||||||
android:layout_width="@dimen/nextup_media_width"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusable="true"
|
|
||||||
android:foreground="@drawable/ripple_background"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
|
||||||
android:id="@+id/next_up_image"
|
|
||||||
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"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Findroid.Image" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
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"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/next_up_image"
|
|
||||||
tools:text="The Girl Flautist" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
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}">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
|
||||||
android:layout_marginBottom="12dp"
|
|
||||||
android:text="@string/seasons"
|
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
|
||||||
android:textSize="18sp" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/seasons_recycler_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
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" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/actors"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
|
||||||
android:layout_marginBottom="12dp"
|
|
||||||
android:text="@string/cast_amp_crew"
|
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
|
||||||
android:textSize="18sp" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/people_recycler_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
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>
|
</LinearLayout>
|
||||||
</ScrollView>
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</layout>
|
<TextView
|
||||||
|
android:id="@+id/description"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
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">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:text="@string/next_up"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/next_up"
|
||||||
|
android:layout_width="@dimen/nextup_media_width"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:foreground="@drawable/ripple_background"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
|
android:id="@+id/next_up_image"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
app:layout_constraintDimensionRatio="H,16:9"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
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:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/next_up_image"
|
||||||
|
tools:text="The Girl Flautist" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/seasons_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:text="@string/seasons"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/seasons_recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:itemCount="3"
|
||||||
|
tools:listitem="@layout/base_item" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/actors"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginBottom="12dp"
|
||||||
|
android:text="@string/cast_amp_crew"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||||
|
android:textSize="18sp" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/people_recycler_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:itemCount="3"
|
||||||
|
tools:listitem="@layout/person_item" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
|
@ -1,169 +1,153 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<data>
|
<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" />
|
||||||
|
|
||||||
<import type="android.view.View" />
|
<include
|
||||||
|
android:id="@+id/error_layout"
|
||||||
|
layout="@layout/error_panel" />
|
||||||
|
|
||||||
<variable
|
<ScrollView
|
||||||
name="viewModel"
|
android:id="@+id/fragment_content"
|
||||||
type="dev.jdtech.jellyfin.viewmodels.PersonDetailViewModel" />
|
android:layout_width="0dp"
|
||||||
</data>
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
|
||||||
android:id="@+id/loading_indicator"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:indeterminate="true"
|
android:orientation="vertical">
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<include
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/error_layout"
|
android:layout_width="match_parent"
|
||||||
layout="@layout/error_panel" />
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="24dp"
|
||||||
|
android:layout_marginBottom="24dp">
|
||||||
|
|
||||||
<ScrollView
|
<com.google.android.material.imageview.ShapeableImageView
|
||||||
android:id="@+id/fragment_content"
|
android:id="@+id/person_image"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="210dp"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:layout_marginStart="24dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
android:scaleType="centerCrop"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintDimensionRatio="H,3:2"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Findroid.Image" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/person_image"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Actor/Actress name" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/overview"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_marginHorizontal="24dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/read_all"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/person_image"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/name" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/overview_gradient"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:background="@drawable/header_gradient"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/overview"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/overview"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/overview" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/read_all"
|
||||||
|
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:text="@string/view_all"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/movie_label"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="24dp"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:layout_marginBottom="24dp">
|
android:layout_marginBottom="12dp"
|
||||||
|
android:text="@string/movies_label"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<com.google.android.material.imageview.ShapeableImageView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/person_image"
|
android:id="@+id/movies_list"
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="210dp"
|
|
||||||
android:layout_marginStart="24dp"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
app:layout_constraintDimensionRatio="H,3:2"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Findroid.Image" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/name"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline5"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/person_image"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:text="Actor/Actress name" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/overview"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/read_all"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toEndOf="@id/person_image"
|
|
||||||
app:layout_constraintTop_toBottomOf="@+id/name" />
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/overview_gradient"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="24dp"
|
|
||||||
android:background="@drawable/header_gradient"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/overview"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/overview"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/overview" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/read_all"
|
|
||||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginEnd="24dp"
|
|
||||||
android:text="@string/view_all"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="24dp"
|
android:layout_marginBottom="24dp"
|
||||||
android:orientation="vertical">
|
android:clipToPadding="false"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingHorizontal="12dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
|
tools:itemCount="4"
|
||||||
|
tools:listitem="@layout/base_item" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/movie_label"
|
android:id="@+id/show_label"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="24dp"
|
android:layout_marginHorizontal="24dp"
|
||||||
android:layout_marginBottom="12dp"
|
android:layout_marginBottom="12dp"
|
||||||
android:text="@string/movies_label"
|
android:text="@string/shows_label"
|
||||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
|
||||||
android:visibility="@{viewModel.starredIn.movies.empty ? View.GONE : View.VISIBLE}" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/movies_list"
|
android:id="@+id/show_list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="24dp"
|
android:clipToPadding="false"
|
||||||
android:clipToPadding="false"
|
android:orientation="horizontal"
|
||||||
android:orientation="horizontal"
|
android:paddingHorizontal="12dp"
|
||||||
android:paddingHorizontal="12dp"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
app:items="@{viewModel.starredIn.movies}"
|
tools:itemCount="4"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
tools:listitem="@layout/base_item" />
|
||||||
tools:itemCount="4"
|
|
||||||
tools:listitem="@layout/base_item" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/show_label"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginHorizontal="24dp"
|
|
||||||
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}" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/show_list"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
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" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</ScrollView>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</ScrollView>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</layout>
|
|
|
@ -1,58 +1,46 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<data>
|
<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" />
|
||||||
|
|
||||||
<variable
|
<include
|
||||||
name="viewModel"
|
android:id="@+id/error_layout"
|
||||||
type="dev.jdtech.jellyfin.viewmodels.SearchResultViewModel" />
|
layout="@layout/error_panel" />
|
||||||
</data>
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<TextView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/no_search_results_text"
|
||||||
android:layout_height="match_parent">
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/no_search_results"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/loading_indicator"
|
android:id="@+id/search_results_recycler_view"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="0dp"
|
||||||
android:indeterminate="true"
|
android:clipToPadding="false"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:paddingTop="16dp"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:trackCornerRadius="10dp" />
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:itemCount="4"
|
||||||
|
tools:listitem="@layout/favorite_section" />
|
||||||
|
|
||||||
<include android:id="@+id/error_layout" layout="@layout/error_panel" />
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/no_search_results_text"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/no_search_results"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/search_results_recycler_view"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
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"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:itemCount="4"
|
|
||||||
tools:listitem="@layout/favorite_section" />
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
|
|
||||||
</layout>
|
|
|
@ -1,48 +1,34 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
tools:context=".fragments.SeasonFragment">
|
tools:context=".fragments.SeasonFragment">
|
||||||
|
|
||||||
<data>
|
<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" />
|
||||||
|
|
||||||
<variable
|
<include
|
||||||
name="viewModel"
|
android:id="@+id/error_layout"
|
||||||
type="dev.jdtech.jellyfin.viewmodels.SeasonViewModel" />
|
layout="@layout/error_panel" />
|
||||||
|
|
||||||
</data>
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/episodes_recycler_view"
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
android:layout_width="0dp"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="0dp"
|
||||||
android:layout_height="match_parent">
|
android:clipToPadding="false"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
<com.google.android.material.progressindicator.CircularProgressIndicator
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
android:id="@+id/loading_indicator"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
android:layout_width="wrap_content"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:layout_height="wrap_content"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
android:indeterminate="true"
|
tools:itemCount="4"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
tools:listitem="@layout/episode_item" />
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
app:trackCornerRadius="10dp" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/error_layout"
|
|
||||||
layout="@layout/error_panel" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
|
||||||
android:id="@+id/episodes_recycler_view"
|
|
||||||
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"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
|
||||||
tools:itemCount="4"
|
|
||||||
tools:listitem="@layout/episode_item" />
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
||||||
</layout>
|
|
Loading…
Reference in a new issue