package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint import android.content.Context import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.APIHolder.unixTimeMS import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ResultEpisodeBinding import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.secondsToReadable import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR import com.lagradost.cloudstream3.ui.settings.Globals.PHONE import com.lagradost.cloudstream3.ui.settings.Globals.TV import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.VideoDownloadHelper import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 const val ACTION_PLAY_EPISODE_IN_VLC_PLAYER = 2 const val ACTION_PLAY_EPISODE_IN_BROWSER = 3 const val ACTION_CHROME_CAST_EPISODE = 4 const val ACTION_CHROME_CAST_MIRROR = 5 const val ACTION_DOWNLOAD_EPISODE = 6 const val ACTION_DOWNLOAD_MIRROR = 7 const val ACTION_RELOAD_EPISODE = 8 const val ACTION_COPY_LINK = 9 const val ACTION_SHOW_OPTIONS = 10 const val ACTION_CLICK_DEFAULT = 11 const val ACTION_SHOW_TOAST = 12 const val ACTION_SHOW_DESCRIPTION = 15 const val ACTION_DOWNLOAD_EPISODE_SUBTITLE = 13 const val ACTION_DOWNLOAD_EPISODE_SUBTITLE_MIRROR = 14 const val ACTION_PLAY_EPISODE_IN_WEB_VIDEO = 16 const val ACTION_PLAY_EPISODE_IN_MPV = 17 const val ACTION_MARK_AS_WATCHED = 18 const val TV_EP_SIZE_LARGE = 400 const val TV_EP_SIZE_SMALL = 300 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit, ) : RecyclerView.Adapter() { companion object { /** * @return ACTION_PLAY_EPISODE_IN_PLAYER, ACTION_PLAY_EPISODE_IN_BROWSER or ACTION_PLAY_EPISODE_IN_VLC_PLAYER depending on player settings. * See array.xml/player_pref_values **/ fun getPlayerAction(context: Context): Int { val settingsManager = PreferenceManager.getDefaultSharedPreferences(context) return when (settingsManager.getInt(context.getString(R.string.player_pref_key), 1)) { 1 -> ACTION_PLAY_EPISODE_IN_PLAYER 2 -> ACTION_PLAY_EPISODE_IN_VLC_PLAYER 3 -> ACTION_PLAY_EPISODE_IN_BROWSER 4 -> ACTION_PLAY_EPISODE_IN_WEB_VIDEO 5 -> ACTION_PLAY_EPISODE_IN_MPV else -> ACTION_PLAY_EPISODE_IN_PLAYER } } } var cardList: MutableList = mutableListOf() override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { if (holder.itemView.hasFocus()) { holder.itemView.clearFocus() } } fun updateList(newList: List) { val diffResult = DiffUtil.calculateDiff( ResultDiffCallback(this.cardList, newList) ) cardList.clear() cardList.addAll(newList) diffResult.dispatchUpdatesTo(this) } private fun getItem(position: Int): ResultEpisode { return cardList[position] } override fun getItemViewType(position: Int): Int { val item = getItem(position) return if (item.poster.isNullOrBlank() && item.description.isNullOrBlank()) 0 else 1 } // private val layout = R.layout.result_episode_both override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { /*val layout = if (cardList.filter { it.poster != null }.size >= cardList.size / 2) R.layout.result_episode_large else R.layout.result_episode*/ return when (viewType) { 0 -> { EpisodeCardViewHolderSmall( ResultEpisodeBinding.inflate( LayoutInflater.from(parent.context), parent, false ), hasDownloadSupport, clickCallback, downloadClickCallback ) } 1 -> { EpisodeCardViewHolderLarge( ResultEpisodeLargeBinding.inflate( LayoutInflater.from(parent.context), parent, false ), hasDownloadSupport, clickCallback, downloadClickCallback ) } else -> throw NotImplementedError() } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is EpisodeCardViewHolderLarge -> { holder.bind(getItem(position)) } is EpisodeCardViewHolderSmall -> { holder.bind(getItem(position)) } } } override fun getItemCount(): Int { return cardList.size } class EpisodeCardViewHolderLarge constructor( val binding: ResultEpisodeLargeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { var localCard: ResultEpisode? = null @SuppressLint("SetTextI18n") fun bind(card: ResultEpisode) { localCard = card val setWidth = if (isLayout(TV or EMULATOR)) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT binding.episodeLinHolder.layoutParams.width = setWidth binding.episodeHolderLarge.layoutParams.width = setWidth binding.episodeHolder.layoutParams.width = setWidth binding.apply { downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( card.name, card.poster, card.episode, card.season, card.id, card.parentId, card.rating, card.description, System.currentTimeMillis(), ), null ) { when (it.action) { DOWNLOAD_ACTION_DOWNLOAD -> { clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card)) } DOWNLOAD_ACTION_LONG_CLICK -> { clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_MIRROR, card)) } else -> { downloadClickCallback.invoke(it) } } } val name = if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}" episodeFiller.isVisible = card.isFiller == true episodeText.text = name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name episodeText.isSelected = true // is needed for text repeating if (card.videoWatchState == VideoWatchState.Watched) { // This cannot be done in getDisplayPosition() as when you have not watched something // the duration and position is 0 episodeProgress.max = 1 episodeProgress.progress = 1 episodeProgress.isVisible = true } else { val displayPos = card.getDisplayPosition() episodeProgress.max = (card.duration / 1000).toInt() episodeProgress.progress = (displayPos / 1000).toInt() episodeProgress.isVisible = displayPos > 0L } episodePoster.isVisible = episodePoster.setImage(card.poster) == true if (card.rating != null) { episodeRating.text = episodeRating.context?.getString(R.string.rated_format) ?.format(card.rating.toFloat() / 10f) } else { episodeRating.text = "" } episodeRating.isGone = episodeRating.text.isNullOrBlank() episodeDescript.apply { text = card.description.html() isGone = text.isNullOrBlank() var isExpanded = false setOnClickListener { if (isLayout(TV)) { clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card)) } else { isExpanded = !isExpanded maxLines = if (isExpanded) { Integer.MAX_VALUE } else 4 } } } if (card.airDate != null) { val isUpcoming = unixTimeMS < card.airDate if (isUpcoming) { episodePlayIcon.isVisible = false episodeUpcomingIcon.isVisible = !episodePoster.isVisible episodeDate.setText( txt( R.string.episode_upcoming_format, secondsToReadable(card.airDate.minus(unixTimeMS).div(1000).toInt(), "") ) ) } else { episodeUpcomingIcon.isVisible = false val formattedAirDate = SimpleDateFormat.getDateInstance( DateFormat.LONG, Locale.getDefault() ).apply { }.format(Date(card.airDate)) episodeDate.setText(txt(formattedAirDate)) } } else { episodeDate.isVisible = false } if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } episodePoster.setOnLongClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_TOAST, card)) return@setOnLongClickListener true } } } itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } if (isLayout(TV)) { itemView.isFocusable = true itemView.isFocusableInTouchMode = true //itemView.touchscreenBlocksFocus = false } itemView.setOnLongClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) return@setOnLongClickListener true } //binding.resultEpisodeDownload.isVisible = hasDownloadSupport //binding.resultEpisodeProgressDownloaded.isVisible = hasDownloadSupport } } class EpisodeCardViewHolderSmall constructor( val binding: ResultEpisodeBinding, private val hasDownloadSupport: Boolean, private val clickCallback: (EpisodeClickEvent) -> Unit, private val downloadClickCallback: (DownloadClickEvent) -> Unit, ) : RecyclerView.ViewHolder(binding.root) { @SuppressLint("SetTextI18n") fun bind(card: ResultEpisode) { binding.episodeHolder.layoutParams.apply { width = if (isLayout(TV or EMULATOR)) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT } binding.apply { downloadButton.isVisible = hasDownloadSupport downloadButton.setDefaultClickListener( VideoDownloadHelper.DownloadEpisodeCached( card.name, card.poster, card.episode, card.season, card.id, card.parentId, card.rating, card.description, System.currentTimeMillis(), ), null ) { when (it.action) { DOWNLOAD_ACTION_DOWNLOAD -> { clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_EPISODE, card)) } DOWNLOAD_ACTION_LONG_CLICK -> { clickCallback.invoke(EpisodeClickEvent(ACTION_DOWNLOAD_MIRROR, card)) } else -> { downloadClickCallback.invoke(it) } } } val name = if (card.name == null) "${episodeText.context.getString(R.string.episode)} ${card.episode}" else "${card.episode}. ${card.name}" episodeFiller.isVisible = card.isFiller == true episodeText.text = name//if(card.isFiller == true) episodeText.context.getString(R.string.filler).format(name) else name episodeText.isSelected = true // is needed for text repeating if (card.videoWatchState == VideoWatchState.Watched) { // This cannot be done in getDisplayPosition() as when you have not watched something // the duration and position is 0 episodeProgress.max = 1 episodeProgress.progress = 1 episodeProgress.isVisible = true } else { val displayPos = card.getDisplayPosition() episodeProgress.max = (card.duration / 1000).toInt() episodeProgress.progress = (displayPos / 1000).toInt() episodeProgress.isVisible = displayPos > 0L } itemView.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } if (isLayout(TV)) { itemView.isFocusable = true itemView.isFocusableInTouchMode = true //itemView.touchscreenBlocksFocus = false } itemView.setOnLongClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) return@setOnLongClickListener true } //binding.resultEpisodeDownload.isVisible = hasDownloadSupport //binding.resultEpisodeProgressDownloaded.isVisible = hasDownloadSupport } } } } class ResultDiffCallback( private val oldList: List, private val newList: List ) : DiffUtil.Callback() { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = oldList[oldItemPosition].id == newList[newItemPosition].id override fun getOldListSize() = oldList.size override fun getNewListSize() = newList.size override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = oldList[oldItemPosition] == newList[newItemPosition] }