up next rework
* start playback, when up next episode is clicked * add playhead progress indicator to up next episodes
This commit is contained in:
		@ -171,7 +171,7 @@ data class ContinueWatchingItem(
 | 
			
		||||
    @SerialName("fully_watched") val fullyWatched: Boolean = false,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// EpisodePanel is used in ContinueWatchingItem
 | 
			
		||||
// EpisodePanel is used in ContinueWatchingItem and UpNextSeriesItem
 | 
			
		||||
@Serializable
 | 
			
		||||
data class EpisodePanel(
 | 
			
		||||
    @SerialName("id") val id: String,
 | 
			
		||||
@ -187,13 +187,16 @@ data class EpisodePanel(
 | 
			
		||||
@Serializable
 | 
			
		||||
data class EpisodeMetadata(
 | 
			
		||||
    @SerialName("duration_ms") val durationMs: Int,
 | 
			
		||||
    @SerialName("episode_number") val episodeNumber: Int? = null, // default/nullable value since optional
 | 
			
		||||
    @SerialName("season_id") val seasonId: String,
 | 
			
		||||
    @SerialName("season_number") val seasonNumber: Int,
 | 
			
		||||
    @SerialName("season_title") val seasonTitle: String,
 | 
			
		||||
    @SerialName("series_id") val seriesId: String,
 | 
			
		||||
    @SerialName("series_title") val seriesTitle: String,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
val NoneItem = Item("", "", "", "", "", Images(emptyList(), emptyList()))
 | 
			
		||||
val NoneEpisodeMetadata = EpisodeMetadata(0, "", "", "")
 | 
			
		||||
val NoneEpisodeMetadata = EpisodeMetadata(0, 0, "", 0, "", "", "")
 | 
			
		||||
val NoneEpisodePanel = EpisodePanel("", "", "", "", "", NoneEpisodeMetadata, Thumbnail(listOf()), "")
 | 
			
		||||
 | 
			
		||||
val NoneCollection = Collection<Item>(0, emptyList())
 | 
			
		||||
 | 
			
		||||
@ -36,7 +36,9 @@ import com.bumptech.glide.Glide
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
import org.mosad.teapod.R
 | 
			
		||||
import org.mosad.teapod.databinding.FragmentHomeBinding
 | 
			
		||||
import org.mosad.teapod.ui.activity.main.MainActivity
 | 
			
		||||
import org.mosad.teapod.ui.activity.main.viewmodel.HomeViewModel
 | 
			
		||||
import org.mosad.teapod.util.adapter.MediaEpisodeListAdapter
 | 
			
		||||
import org.mosad.teapod.util.adapter.MediaItemListAdapter
 | 
			
		||||
import org.mosad.teapod.util.decoration.MediaItemDecoration
 | 
			
		||||
import org.mosad.teapod.util.setDrawableTop
 | 
			
		||||
@ -57,14 +59,17 @@ class HomeFragment : Fragment() {
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        super.onViewCreated(view, savedInstanceState)
 | 
			
		||||
 | 
			
		||||
        binding.recyclerNewEpisodes.addItemDecoration(MediaItemDecoration(9))
 | 
			
		||||
        binding.recyclerUpNext.addItemDecoration(MediaItemDecoration(9))
 | 
			
		||||
        binding.recyclerWatchlist.addItemDecoration(MediaItemDecoration(9))
 | 
			
		||||
        binding.recyclerNewTitles.addItemDecoration(MediaItemDecoration(9))
 | 
			
		||||
        binding.recyclerTopTen.addItemDecoration(MediaItemDecoration(9))
 | 
			
		||||
 | 
			
		||||
        binding.recyclerNewEpisodes.adapter = MediaItemListAdapter(
 | 
			
		||||
            MediaItemListAdapter.OnClickListener {
 | 
			
		||||
                activity?.showFragment(MediaFragment(it.id))
 | 
			
		||||
        binding.recyclerUpNext.adapter = MediaEpisodeListAdapter(
 | 
			
		||||
            MediaEpisodeListAdapter.OnClickListener {
 | 
			
		||||
                val activity = activity
 | 
			
		||||
                if (activity is MainActivity) {
 | 
			
		||||
                    activity.startPlayer(it.panel.episodeMetadata.seasonId, it.panel.id)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@ -118,8 +123,8 @@ class HomeFragment : Fragment() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun bindUiStateNormal(uiState: HomeViewModel.UiState.Normal) {
 | 
			
		||||
        val adapterUpNext = binding.recyclerNewEpisodes.adapter as MediaItemListAdapter
 | 
			
		||||
        adapterUpNext.submitList(uiState.upNextItems.filter { !it.fullyWatched }.toItemMediaList())
 | 
			
		||||
        val adapterUpNext = binding.recyclerUpNext.adapter as MediaEpisodeListAdapter
 | 
			
		||||
        adapterUpNext.submitList(uiState.upNextItems.filter { !it.fullyWatched })
 | 
			
		||||
 | 
			
		||||
        val adapterWatchlist = binding.recyclerWatchlist.adapter as MediaItemListAdapter
 | 
			
		||||
        adapterWatchlist.submitList(uiState.watchlistItems.toItemMediaList())
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,70 @@
 | 
			
		||||
package org.mosad.teapod.util.adapter
 | 
			
		||||
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.recyclerview.widget.DiffUtil
 | 
			
		||||
import androidx.recyclerview.widget.ListAdapter
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
import com.bumptech.glide.Glide
 | 
			
		||||
import org.mosad.teapod.R
 | 
			
		||||
import org.mosad.teapod.databinding.ItemMediaBinding
 | 
			
		||||
import org.mosad.teapod.parser.crunchyroll.ContinueWatchingItem
 | 
			
		||||
 | 
			
		||||
class MediaEpisodeListAdapter(private val onClickListener: OnClickListener) : ListAdapter<ContinueWatchingItem, MediaEpisodeListAdapter.MediaViewHolder>(DiffCallback) {
 | 
			
		||||
 | 
			
		||||
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
 | 
			
		||||
        return MediaViewHolder(
 | 
			
		||||
            ItemMediaBinding.inflate(
 | 
			
		||||
                LayoutInflater.from(parent.context),
 | 
			
		||||
                parent,
 | 
			
		||||
                false
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
 | 
			
		||||
        val item = getItem(position)
 | 
			
		||||
        holder.binding.root.setOnClickListener {
 | 
			
		||||
            onClickListener.onClick(item)
 | 
			
		||||
        }
 | 
			
		||||
        holder.bind(item)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    inner class MediaViewHolder(val binding: ItemMediaBinding) :
 | 
			
		||||
        RecyclerView.ViewHolder(binding.root) {
 | 
			
		||||
 | 
			
		||||
        fun bind(item: ContinueWatchingItem) {
 | 
			
		||||
            val metadata = item.panel.episodeMetadata
 | 
			
		||||
 | 
			
		||||
            binding.textTitle.text = binding.root.context.getString(R.string.season_episode_title,
 | 
			
		||||
                metadata.seasonNumber, metadata.episodeNumber, metadata.seriesTitle
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            Glide.with(binding.imagePoster)
 | 
			
		||||
                .load(item.panel.images.thumbnail[0][0].source)
 | 
			
		||||
                .into(binding.imagePoster)
 | 
			
		||||
 | 
			
		||||
            // add watched progress
 | 
			
		||||
            val playheadProgress = ((item.playhead.toFloat() / (metadata.durationMs / 1000)) * 100)
 | 
			
		||||
                .toInt()
 | 
			
		||||
            binding.progressPlayhead.setProgressCompat(playheadProgress, false)
 | 
			
		||||
            binding.progressPlayhead.visibility = if (playheadProgress <= 0)
 | 
			
		||||
                View.GONE else View.VISIBLE
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object DiffCallback : DiffUtil.ItemCallback<ContinueWatchingItem>() {
 | 
			
		||||
        override fun areItemsTheSame(oldItem: ContinueWatchingItem, newItem: ContinueWatchingItem): Boolean {
 | 
			
		||||
            return oldItem.panel.id == newItem.panel.id
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        override fun areContentsTheSame(oldItem: ContinueWatchingItem, newItem: ContinueWatchingItem): Boolean {
 | 
			
		||||
            return oldItem == newItem
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    class OnClickListener(val clickListener: (item: ContinueWatchingItem) -> Unit) {
 | 
			
		||||
        fun onClick(item: ContinueWatchingItem) = clickListener(item)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -2,6 +2,7 @@ package org.mosad.teapod.util.adapter
 | 
			
		||||
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import androidx.recyclerview.widget.DiffUtil
 | 
			
		||||
import androidx.recyclerview.widget.ListAdapter
 | 
			
		||||
import androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
@ -34,8 +35,13 @@ class MediaItemListAdapter(private val onClickListener: OnClickListener) : ListA
 | 
			
		||||
 | 
			
		||||
        fun bind(item: ItemMedia) {
 | 
			
		||||
            binding.textTitle.text = item.title
 | 
			
		||||
            // can we use the view instead of context here?
 | 
			
		||||
            Glide.with(binding.root.context).load(item.posterUrl).into(binding.imagePoster)
 | 
			
		||||
 | 
			
		||||
            Glide.with(binding.imagePoster)
 | 
			
		||||
                .load(item.posterUrl)
 | 
			
		||||
                .into(binding.imagePoster)
 | 
			
		||||
 | 
			
		||||
            binding.imageEpisodePlay.isVisible = false
 | 
			
		||||
            binding.progressPlayhead.isVisible = false
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,6 @@ import jp.wasabeef.glide.transformations.RoundedCornersTransformation
 | 
			
		||||
import org.mosad.teapod.R
 | 
			
		||||
import org.mosad.teapod.databinding.ItemEpisodePlayerBinding
 | 
			
		||||
import org.mosad.teapod.parser.crunchyroll.Episodes
 | 
			
		||||
import org.mosad.teapod.util.tmdb.TMDBTVEpisode
 | 
			
		||||
 | 
			
		||||
class PlayerEpisodeItemAdapter(private val episodes: Episodes) : RecyclerView.Adapter<PlayerEpisodeItemAdapter.EpisodeViewHolder>() {
 | 
			
		||||
 | 
			
		||||
@ -37,11 +36,7 @@ class PlayerEpisodeItemAdapter(private val episodes: Episodes) : RecyclerView.Ad
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        holder.binding.textEpisodeTitle2.text = titleText
 | 
			
		||||
        holder.binding.textEpisodeDesc2.text = if (ep.description.isNotEmpty()) {
 | 
			
		||||
            ep.description
 | 
			
		||||
        } else {
 | 
			
		||||
            ""
 | 
			
		||||
        }
 | 
			
		||||
        holder.binding.textEpisodeDesc2.text = ep.description.ifEmpty { "" }
 | 
			
		||||
 | 
			
		||||
        if (ep.images.thumbnail[0][0].source.isNotEmpty()) {
 | 
			
		||||
            Glide.with(context).load(ep.images.thumbnail[0][0].source)
 | 
			
		||||
 | 
			
		||||
@ -115,7 +115,7 @@
 | 
			
		||||
                android:paddingBottom="7dp">
 | 
			
		||||
 | 
			
		||||
                <TextView
 | 
			
		||||
                    android:id="@+id/text_new_episodes"
 | 
			
		||||
                    android:id="@+id/text_up_next"
 | 
			
		||||
                    android:layout_width="match_parent"
 | 
			
		||||
                    android:layout_height="wrap_content"
 | 
			
		||||
                    android:paddingStart="10dp"
 | 
			
		||||
@ -127,7 +127,7 @@
 | 
			
		||||
                    android:textStyle="bold" />
 | 
			
		||||
 | 
			
		||||
                <androidx.recyclerview.widget.RecyclerView
 | 
			
		||||
                    android:id="@+id/recycler_new_episodes"
 | 
			
		||||
                    android:id="@+id/recycler_up_next"
 | 
			
		||||
                    android:layout_width="match_parent"
 | 
			
		||||
                    android:layout_height="match_parent"
 | 
			
		||||
                    android:orientation="horizontal"
 | 
			
		||||
 | 
			
		||||
@ -13,18 +13,43 @@
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        android:layout_height="match_parent">
 | 
			
		||||
 | 
			
		||||
        <ImageView
 | 
			
		||||
            android:id="@+id/image_poster"
 | 
			
		||||
        <FrameLayout
 | 
			
		||||
            android:id="@+id/frame_image_progress"
 | 
			
		||||
            android:layout_width="0dp"
 | 
			
		||||
            android:layout_height="0dp"
 | 
			
		||||
            android:contentDescription="@string/media_poster_desc"
 | 
			
		||||
            android:scaleType="centerCrop"
 | 
			
		||||
            app:layout_constraintBottom_toTopOf="@+id/text_title"
 | 
			
		||||
            app:layout_constraintDimensionRatio="H,16:9"
 | 
			
		||||
            app:layout_constraintEnd_toEndOf="parent"
 | 
			
		||||
            app:layout_constraintStart_toStartOf="parent"
 | 
			
		||||
            app:layout_constraintTop_toTopOf="parent"
 | 
			
		||||
            tools:srcCompat="@color/md_disabled_text_dark_theme" />
 | 
			
		||||
            app:layout_constraintTop_toTopOf="parent">
 | 
			
		||||
 | 
			
		||||
            <ImageView
 | 
			
		||||
                android:id="@+id/image_poster"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="match_parent"
 | 
			
		||||
                android:contentDescription="@string/media_poster_desc"
 | 
			
		||||
                android:scaleType="centerCrop"
 | 
			
		||||
                tools:srcCompat="@color/imagePlaceholder" />
 | 
			
		||||
 | 
			
		||||
            <ImageView
 | 
			
		||||
                android:id="@+id/image_episode_play"
 | 
			
		||||
                android:layout_width="wrap_content"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_gravity="center"
 | 
			
		||||
                android:background="@drawable/bg_circle__black_transparent_24dp"
 | 
			
		||||
                android:contentDescription="@string/button_play"
 | 
			
		||||
                app:srcCompat="@drawable/ic_baseline_play_arrow_24"
 | 
			
		||||
                app:tint="#FFFFFF" />
 | 
			
		||||
 | 
			
		||||
            <com.google.android.material.progressindicator.LinearProgressIndicator
 | 
			
		||||
                android:id="@+id/progress_playhead"
 | 
			
		||||
                android:layout_width="match_parent"
 | 
			
		||||
                android:layout_height="wrap_content"
 | 
			
		||||
                android:layout_gravity="bottom"
 | 
			
		||||
                android:max="100"
 | 
			
		||||
                app:trackColor="#00FFFFFF"
 | 
			
		||||
                app:trackThickness="2dp" />
 | 
			
		||||
        </FrameLayout>
 | 
			
		||||
 | 
			
		||||
        <TextView
 | 
			
		||||
            android:id="@+id/text_title"
 | 
			
		||||
@ -37,7 +62,7 @@
 | 
			
		||||
            android:text="@string/text_title_ex"
 | 
			
		||||
            android:textAlignment="center"
 | 
			
		||||
            android:textSize="15sp"
 | 
			
		||||
            app:layout_constraintTop_toBottomOf="@+id/image_poster" />
 | 
			
		||||
            app:layout_constraintTop_toBottomOf="@+id/frame_image_progress" />
 | 
			
		||||
    </androidx.constraintlayout.widget.ConstraintLayout>
 | 
			
		||||
 | 
			
		||||
</com.google.android.material.card.MaterialCardView>
 | 
			
		||||
@ -13,6 +13,7 @@
 | 
			
		||||
    <string name="new_simulcasts">New simulcasts</string>
 | 
			
		||||
    <string name="new_titles">New titles</string>
 | 
			
		||||
    <string name="top_ten">Top 10</string>
 | 
			
		||||
    <string name="season_episode_title" translatable="false">S%1$d E%2$d - %3$s</string>
 | 
			
		||||
 | 
			
		||||
    <!-- search fragment -->
 | 
			
		||||
    <string name="search_hint">Search for movies and series</string>
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user