From 4bbf40bc22e365825e38af803a94448358b2d623 Mon Sep 17 00:00:00 2001 From: Jarne Demeulemeester Date: Tue, 6 Jul 2021 18:19:37 +0200 Subject: [PATCH] Add header to EpisodeListAdapter --- .../dev/jdtech/jellyfin/BindingAdapters.kt | 2 +- .../jellyfin/adapters/EpisodeListAdapter.kt | 107 ++++++++++--- .../jellyfin/fragments/SeasonFragment.kt | 6 +- .../jellyfin/viewmodels/SeasonViewModel.kt | 18 ++- app/src/main/res/layout/episode_item.xml | 1 + app/src/main/res/layout/fragment_season.xml | 133 +--------------- app/src/main/res/layout/season_header.xml | 144 ++++++++++++++++++ 7 files changed, 249 insertions(+), 162 deletions(-) create mode 100644 app/src/main/res/layout/season_header.xml diff --git a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt index ab78bade..e487d0fc 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/BindingAdapters.kt @@ -113,7 +113,7 @@ fun bindPersonImage(imageView: ImageView, person: BaseItemPerson) { } @BindingAdapter("episodes") -fun bindEpisodes(recyclerView: RecyclerView, data: List?) { +fun bindEpisodes(recyclerView: RecyclerView, data: List?) { val adapter = recyclerView.adapter as EpisodeListAdapter adapter.submitList(data) } diff --git a/app/src/main/java/dev/jdtech/jellyfin/adapters/EpisodeListAdapter.kt b/app/src/main/java/dev/jdtech/jellyfin/adapters/EpisodeListAdapter.kt index df19f837..f25e50a8 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/adapters/EpisodeListAdapter.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/adapters/EpisodeListAdapter.kt @@ -8,10 +8,37 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import dev.jdtech.jellyfin.databinding.EpisodeItemBinding +import dev.jdtech.jellyfin.databinding.SeasonHeaderBinding import org.jellyfin.sdk.model.api.BaseItemDto +import java.util.* -class EpisodeListAdapter(private val onClickListener: OnClickListener) : - ListAdapter(DiffCallback) { +private const val ITEM_VIEW_TYPE_HEADER = 0 +private const val ITEM_VIEW_TYPE_EPISODE = 1 + +class EpisodeListAdapter( + private val onClickListener: OnClickListener, + private val seriesId: UUID, + private val seriesName: String?, + private val seasonId: UUID, + private val seasonName: String? +) : + ListAdapter(DiffCallback) { + + class HeaderViewHolder(private var binding: SeasonHeaderBinding) : + RecyclerView.ViewHolder(binding.root) { + fun bind( + seriesId: UUID, + seriesName: String?, + seasonId: UUID, + seasonName: String? + ) { + binding.seriesId = seriesId + binding.seasonId = seasonId + binding.seasonName.text = seasonName + binding.seriesName.text = seriesName + binding.executePendingBindings() + } + } class EpisodeViewHolder(private var binding: EpisodeItemBinding) : RecyclerView.ViewHolder(binding.root) { @@ -20,7 +47,9 @@ class EpisodeListAdapter(private val onClickListener: OnClickListener) : if (episode.userData?.playedPercentage != null) { binding.progressBar.layoutParams.width = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, - (episode.userData?.playedPercentage?.times(.84))!!.toFloat(), binding.progressBar.context.resources.displayMetrics).toInt() + (episode.userData?.playedPercentage?.times(.84))!!.toFloat(), + binding.progressBar.context.resources.displayMetrics + ).toInt() binding.progressBar.visibility = View.VISIBLE } else { binding.progressBar.visibility = View.GONE @@ -29,35 +58,75 @@ class EpisodeListAdapter(private val onClickListener: OnClickListener) : } } - companion object DiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { + companion object DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: EpisodeItem, newItem: EpisodeItem): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: BaseItemDto, newItem: BaseItemDto): Boolean { + override fun areContentsTheSame(oldItem: EpisodeItem, newItem: EpisodeItem): Boolean { return oldItem == newItem } } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeViewHolder { - return EpisodeViewHolder( - EpisodeItemBinding.inflate( - LayoutInflater.from(parent.context), - parent, - false - ) - ) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + ITEM_VIEW_TYPE_HEADER -> { + HeaderViewHolder( + SeasonHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + ITEM_VIEW_TYPE_EPISODE -> { + EpisodeViewHolder( + EpisodeItemBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + ) + } + else -> throw ClassCastException("Unknown viewType $viewType") + } } - override fun onBindViewHolder(holder: EpisodeViewHolder, position: Int) { - val item = getItem(position) - holder.itemView.setOnClickListener { - onClickListener.onClick(item) + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder.itemViewType) { + ITEM_VIEW_TYPE_HEADER -> { + (holder as HeaderViewHolder).bind(seriesId, seriesName, seasonId, seasonName) + } + ITEM_VIEW_TYPE_EPISODE -> { + val item = getItem(position) as EpisodeItem.Episode + holder.itemView.setOnClickListener { + onClickListener.onClick(item.episode) + } + (holder as EpisodeViewHolder).bind(item.episode) + } + } + } + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is EpisodeItem.Header -> ITEM_VIEW_TYPE_HEADER + is EpisodeItem.Episode -> ITEM_VIEW_TYPE_EPISODE } - holder.bind(item) } class OnClickListener(val clickListener: (item: BaseItemDto) -> Unit) { fun onClick(item: BaseItemDto) = clickListener(item) } +} + +sealed class EpisodeItem { + abstract val id: UUID + + object Header : EpisodeItem() { + override val id: UUID = UUID.randomUUID() + } + + data class Episode(val episode: BaseItemDto) : EpisodeItem() { + override val id = episode.id + } } \ No newline at end of file diff --git a/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt b/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt index 28e831f3..c924d84e 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/fragments/SeasonFragment.kt @@ -37,11 +37,7 @@ class SeasonFragment : Fragment() { binding.episodesRecyclerView.adapter = EpisodeListAdapter(EpisodeListAdapter.OnClickListener { episode -> navigateToEpisodeBottomSheetFragment(episode) - }) - binding.seriesName.text = args.seriesName - binding.seasonName.text = args.seasonName - binding.seriesId = args.seriesId - binding.seasonId = args.seasonId + }, args.seriesId, args.seriesName, args.seasonId, args.seasonName) viewModel.loadEpisodes(args.seriesId, args.seasonId) } diff --git a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt index ebbc50c6..ea6f1227 100644 --- a/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt +++ b/app/src/main/java/dev/jdtech/jellyfin/viewmodels/SeasonViewModel.kt @@ -1,10 +1,13 @@ package dev.jdtech.jellyfin.viewmodels -import androidx.lifecycle.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import dev.jdtech.jellyfin.adapters.EpisodeItem import dev.jdtech.jellyfin.repository.JellyfinRepository import kotlinx.coroutines.launch -import org.jellyfin.sdk.model.api.BaseItemDto import org.jellyfin.sdk.model.api.ItemFields import java.util.* import javax.inject.Inject @@ -14,12 +17,17 @@ class SeasonViewModel @Inject constructor(private val jellyfinRepository: JellyfinRepository) : ViewModel() { - private val _episodes = MutableLiveData>() - val episodes: LiveData> = _episodes + private val _episodes = MutableLiveData>() + val episodes: LiveData> = _episodes fun loadEpisodes(seriesId: UUID, seasonId: UUID) { viewModelScope.launch { - _episodes.value = jellyfinRepository.getEpisodes(seriesId, seasonId, fields = listOf(ItemFields.OVERVIEW)) + _episodes.value = getEpisodes(seriesId, seasonId) } } + + private suspend fun getEpisodes(seriesId: UUID, seasonId: UUID): List { + val episodes = jellyfinRepository.getEpisodes(seriesId, seasonId, fields = listOf(ItemFields.OVERVIEW)) + return listOf(EpisodeItem.Header) + episodes.map { EpisodeItem.Episode(it) } + } } \ No newline at end of file diff --git a/app/src/main/res/layout/episode_item.xml b/app/src/main/res/layout/episode_item.xml index cad6e3c7..0e86c769 100644 --- a/app/src/main/res/layout/episode_item.xml +++ b/app/src/main/res/layout/episode_item.xml @@ -15,6 +15,7 @@ diff --git a/app/src/main/res/layout/fragment_season.xml b/app/src/main/res/layout/fragment_season.xml index aa9761cf..33bc7154 100644 --- a/app/src/main/res/layout/fragment_season.xml +++ b/app/src/main/res/layout/fragment_season.xml @@ -10,154 +10,23 @@ name="viewModel" type="dev.jdtech.jellyfin.viewmodels.SeasonViewModel" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/season_header.xml b/app/src/main/res/layout/season_header.xml new file mode 100644 index 00000000..861060d9 --- /dev/null +++ b/app/src/main/res/layout/season_header.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +