From b5f913cc7295b32830041c48e1b7b222244990c1 Mon Sep 17 00:00:00 2001 From: LagradOst Date: Sat, 17 Jul 2021 16:14:25 +0200 Subject: [PATCH] episode options sff --- .../com/lagradost/cloudstream3/UIHelper.kt | 12 + .../cloudstream3/ui/result/EpisodeAdapter.kt | 57 ++-- .../cloudstream3/ui/result/ResultFragment.kt | 264 ++++++++++++------ .../cloudstream3/ui/result/ResultViewModel.kt | 59 ++-- .../lagradost/cloudstream3/utils/DataStore.kt | 3 + app/src/main/res/values/array.xml | 23 ++ app/src/main/res/values/strings.xml | 1 + 7 files changed, 285 insertions(+), 134 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt index a40e80bb..0045892a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt @@ -27,6 +27,7 @@ import androidx.core.content.ContextCompat import androidx.fragment.app.FragmentActivity import androidx.preference.PreferenceManager import com.google.android.gms.cast.framework.CastContext +import com.google.android.gms.cast.framework.CastState import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.lagradost.cloudstream3.ui.result.ResultFragment @@ -151,6 +152,17 @@ object UIHelper { return isCastApiAvailable } + fun Context.isConnectedToChromecast(): Boolean { + if (isCastApiAvailable()) { + val castContext = CastContext.getSharedInstance(this) + if (castContext.castState == CastState.CONNECTED) { + return true + } + } + return false + } + + fun adjustAlpha(@ColorInt color: Int, factor: Float): Int { val alpha = (Color.alpha(color) * factor).roundToInt() val red = Color.red(color) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index f7ad8126..2afbdd2c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -2,6 +2,8 @@ package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint import android.app.Activity +import android.content.Context +import android.content.DialogInterface import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -9,6 +11,7 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.LayoutRes +import androidx.appcompat.app.AlertDialog import androidx.core.widget.ContentLoadingProgressBar import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide @@ -16,15 +19,27 @@ import com.bumptech.glide.load.model.GlideUrl import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.UIHelper.hideSystemUI import com.lagradost.cloudstream3.UIHelper.isCastApiAvailable +import com.lagradost.cloudstream3.utils.getId import kotlinx.android.synthetic.main.result_episode.view.episode_holder import kotlinx.android.synthetic.main.result_episode.view.episode_text import kotlinx.android.synthetic.main.result_episode_large.view.* -const val ACTION_RELOAD_EPISODE = 4 -const val ACTION_CHROME_CAST_EPISODE = 2 -const val ACTION_DOWNLOAD_EPISODE = 3 const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 +const val ACTION_PLAY_EPISODE_IN_EXTERNAL_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 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) @@ -69,8 +84,6 @@ class EpisodeAdapter( itemView: View, private val clickCallback: (EpisodeClickEvent) -> Unit, ) : RecyclerView.ViewHolder(itemView) { - //private val episodeViewPrecentage: View? = itemView.episode_view_procentage - // private val episodeViewPercentageOff: View? = itemView.episode_view_procentage_off private val episodeText: TextView = itemView.episode_text private val episodeRating: TextView? = itemView.episode_rating private val episodeDescript: TextView? = itemView.episode_descript @@ -78,8 +91,6 @@ class EpisodeAdapter( private val episodePoster: ImageView? = itemView.episode_poster private val episodeDownload: ImageView? = itemView.episode_download - // val episodeExtra: ImageView = itemView.episode_extra - // private val episodePlay: ImageView = itemView.episode_play private val episodeHolder = itemView.episode_holder @SuppressLint("SetTextI18n") @@ -87,20 +98,7 @@ class EpisodeAdapter( val name = if (card.name == null) "Episode ${card.episode}" else "${card.episode}. ${card.name}" episodeText.text = name - fun setWidth(v: View, procentage: Float) { - val param = LinearLayout.LayoutParams( - v.layoutParams.width, - v.layoutParams.height, - procentage - ) - v.layoutParams = param - } - val watchProgress = card.getWatchProgress() - /*if (episodeViewPrecentage != null && episodeViewPercentageOff != null) { - setWidth(episodeViewPrecentage, watchProgress) - setWidth(episodeViewPercentageOff, 1 - watchProgress) - }*/ episodeProgress?.progress = (watchProgress * 50).toInt() episodeProgress?.visibility = if (watchProgress > 0.0f) View.VISIBLE else View.GONE @@ -133,21 +131,18 @@ class EpisodeAdapter( episodeHolder.setOnClickListener { episodeHolder.context?.let { ctx -> - if (ctx.isCastApiAvailable()) { - val castContext = CastContext.getSharedInstance(ctx) - - if (castContext.castState == CastState.CONNECTED) { - clickCallback.invoke(EpisodeClickEvent(ACTION_CHROME_CAST_EPISODE, card)) - } else { - // clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card)) - clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card)) - } + if (ctx.isConnectedToChromecast()) { + clickCallback.invoke(EpisodeClickEvent(ACTION_CHROME_CAST_EPISODE, card)) } else { - // clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card)) - clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card)) //TODO REDO TO MAIN + clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card)) } } + } + episodeHolder.setOnLongClickListener { + clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_OPTIONS, card)) + + return@setOnLongClickListener true } episodeDownload?.setOnClickListener { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index e2902dc7..106a1b7f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -35,6 +35,7 @@ import com.lagradost.cloudstream3.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.UIHelper.getStatusBarHeight import com.lagradost.cloudstream3.UIHelper.isCastApiAvailable +import com.lagradost.cloudstream3.UIHelper.isConnectedToChromecast import com.lagradost.cloudstream3.UIHelper.popCurrentPage import com.lagradost.cloudstream3.UIHelper.popupMenuNoIcons import com.lagradost.cloudstream3.UIHelper.popupMenuNoIconsAndNoStringres @@ -43,14 +44,16 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.player.PlayerData import com.lagradost.cloudstream3.ui.player.PlayerFragment +import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.CastHelper.startCast +import com.lagradost.cloudstream3.utils.Coroutines.main +import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.ExtractorLink -import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import jp.wasabeef.glide.transformations.BlurTransformation import kotlinx.android.synthetic.main.fragment_result.* +import kotlinx.coroutines.Job const val MAX_SYNO_LENGH = 300 @@ -134,6 +137,7 @@ class ResultFragment : Fragment() { private var currentLoadingCount = 0 // THIS IS USED TO PREVENT LATE EVENTS, AFTER DISMISS WAS CLICKED private lateinit var viewModel: ResultViewModel private var allEpisodes: HashMap> = HashMap() + private var allEpisodesSubs: HashMap> = HashMap() private var currentHeaderName: String? = null private var currentType: TvType? = null private var currentEpisodes: List? = null @@ -188,6 +192,7 @@ class ResultFragment : Fragment() { } private var currentPoster: String? = null + private var currentId: Int? = null private var currentIsMovie: Boolean? = null var url: String? = null @@ -255,59 +260,126 @@ class ResultFragment : Fragment() { requireActivity().popCurrentPage() } - fun handleAction(episodeClick: EpisodeClickEvent) { + fun handleAction(episodeClick: EpisodeClickEvent): Job = main { //val id = episodeClick.data.id val index = episodeClick.data.index val buildInPlayer = true currentLoadingCount++ + var currentLinks: ArrayList? = null + var currentSubs: ArrayList? = null + + suspend fun requireLinks(isCasting: Boolean = false): Boolean { + val currentLinksTemp = + if (allEpisodes.containsKey(episodeClick.data.id)) allEpisodes[episodeClick.data.id] else null + val currentSubsTemp = + if (allEpisodes.containsKey(episodeClick.data.id)) allEpisodes[episodeClick.data.id] else null + if (currentLinksTemp != null && currentLinksTemp.size > 0) { + currentLinks = currentLinksTemp + return true + } + + val skipLoading = if (apiName != null) { + getApiFromName(apiName).instantLinkLoading + } else false + + var loadingDialog: AlertDialog? = null + val currentLoad = currentLoadingCount + + if (!skipLoading) { + val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustomTransparent) + val customLayout = layoutInflater.inflate(R.layout.dialog_loading, null) + builder.setView(customLayout) + + loadingDialog = builder.create() + + loadingDialog.show() + loadingDialog.setOnDismissListener { + currentLoadingCount++ + } + } + + val data = viewModel.loadEpisode(episodeClick.data, isCasting) + if (currentLoadingCount != currentLoad) return false + loadingDialog?.dismiss() + + when (data) { + is Resource.Success -> { + currentLinks = data.value.links + currentSubs = data.value.subs + return true + } + is Resource.Failure -> { + Toast.makeText(requireContext(), R.string.error_loading_links, Toast.LENGTH_SHORT).show() + } + else -> { + + } + } + return false + } + + val isLoaded = when (episodeClick.action) { + ACTION_PLAY_EPISODE_IN_PLAYER -> true + ACTION_CHROME_CAST_EPISODE -> requireLinks(true) + ACTION_CHROME_CAST_MIRROR -> requireLinks(true) + else -> requireLinks() + } + if (!isLoaded) return@main // CANT LOAD + when (episodeClick.action) { - ACTION_CHROME_CAST_EPISODE -> { - val skipLoading = if (apiName != null) { - getApiFromName(apiName).instantLinkLoading - } else false - + ACTION_SHOW_OPTIONS -> { + val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) var dialog: AlertDialog? = null - val currentLoad = currentLoadingCount + builder.setTitle(episodeClick.data.name) + val options = requireContext().resources.getStringArray(R.array.episode_long_click_options) + val optionsValues = + requireContext().resources.getIntArray(R.array.episode_long_click_options_values) - if (!skipLoading) { - val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustomTransparent) - val customLayout = layoutInflater.inflate(R.layout.dialog_loading, null) - builder.setView(customLayout) + val verifiedOptions = ArrayList() + val verifiedOptionsValues = ArrayList() - dialog = builder.create() + for (i in options.indices) { + val opv = optionsValues[i] + val op = options[i] - dialog.show() - dialog.setOnDismissListener { - currentLoadingCount++ + val isConnected = requireContext().isConnectedToChromecast() + val add = when (opv) { + ACTION_CHROME_CAST_EPISODE -> isConnected + ACTION_CHROME_CAST_MIRROR -> isConnected + else -> true + } + if (add) { + verifiedOptions.add(op) + verifiedOptionsValues.add(opv) } } - // Toast.makeText(activity, "Loading links", Toast.LENGTH_SHORT).show() - - viewModel.loadEpisode(episodeClick.data, true) { data -> - if (currentLoadingCount != currentLoad) return@loadEpisode + builder.setItems( + verifiedOptions.toTypedArray() + ) { _, which -> + handleAction(EpisodeClickEvent(verifiedOptionsValues[which], episodeClick.data)) dialog?.dismiss() - - when (data) { - is Resource.Failure -> { - Toast.makeText(activity, "Failed to load links", Toast.LENGTH_SHORT).show() - } - is Resource.Success -> { - val eps = currentEpisodes ?: return@loadEpisode - context?.startCast( - apiName ?: return@loadEpisode, - currentIsMovie ?: return@loadEpisode, - currentHeaderName, - currentPoster, - episodeClick.data.index, - eps, - sortUrls(data.value.links), - data.value.subs, - startTime = episodeClick.data.getRealPosition(), - ) - } - } } + + dialog = builder.create() + dialog.show() + } + + + ACTION_CHROME_CAST_EPISODE -> { + + val eps = currentEpisodes ?: return@main + context?.startCast( + apiName ?: return@main, + currentIsMovie ?: return@main, + currentHeaderName, + currentPoster, + episodeClick.data.index, + eps, + sortUrls(currentLinks ?: return@main), + currentSubs ?: return@main, + startTime = episodeClick.data.getRealPosition(), + ) } ACTION_PLAY_EPISODE_IN_PLAYER -> { @@ -330,60 +402,80 @@ class ResultFragment : Fragment() { } } ACTION_RELOAD_EPISODE -> { - /*viewModel.load(episodeClick.data) { res -> - if (res is Resource.Success) { - playEpisode(allEpisodes[id], index) - } - }*/ + viewModel.loadEpisode(episodeClick.data, false) } ACTION_DOWNLOAD_EPISODE -> { - val tempUrl = url - if (tempUrl != null) { - viewModel.loadEpisode(episodeClick.data, true) { data -> - if (data is Resource.Success) { - val isMovie = currentIsMovie ?: return@loadEpisode - val titleName = sanitizeFilename(currentHeaderName ?: return@loadEpisode) - val meta = VideoDownloadManager.DownloadEpisodeMetadata( - episodeClick.data.id, - titleName, - apiName ?: return@loadEpisode, - episodeClick.data.poster ?: currentPoster, - episodeClick.data.name, - if (isMovie) null else episodeClick.data.season, - if (isMovie) null else episodeClick.data.episode - ) + val isMovie = currentIsMovie ?: return@main + val titleName = sanitizeFilename(currentHeaderName ?: return@main) - val folder = when (currentType) { - TvType.Anime -> "Anime/$titleName" - TvType.Movie -> "Movies" - TvType.TvSeries -> "TVSeries/$titleName" - TvType.ONA -> "ONA" - else -> null - } + val meta = VideoDownloadManager.DownloadEpisodeMetadata( + episodeClick.data.id, + titleName, + apiName ?: return@main, + episodeClick.data.poster ?: currentPoster, + episodeClick.data.name, + if (isMovie) null else episodeClick.data.season, + if (isMovie) null else episodeClick.data.episode + ) - VideoDownloadManager.downloadEpisode( - requireContext(), - tempUrl, - folder, - meta, - data.value.links - ) - } - } + val folder = when (currentType) { + TvType.Anime -> "Anime/$titleName" + TvType.Movie -> "Movies" + TvType.TvSeries -> "TVSeries/$titleName" + TvType.ONA -> "ONA" + else -> null + } + + context?.let { ctx -> + // SET VISUAL KEYS + ctx.setKey( + DOWNLOAD_HEADER_CACHE, (currentId ?: return@let).toString(), + VideoDownloadHelper.DownloadHeaderCached( + apiName, + url ?: return@let, + currentType ?: return@let, + currentHeaderName ?: return@let, + currentPoster ?: return@let, + currentId ?: return@let + ) + ) + + val epData = episodeClick.data + ctx.setKey( + DOWNLOAD_EPISODE_CACHE, + epData.id.toString(), + VideoDownloadHelper.DownloadEpisodeCached( + epData.name, + epData.poster, + epData.episode, + epData.season, + epData.id, + currentId ?: return@let, + epData.rating, + epData.descript + ) + ) + + // DOWNLOAD VIDEO + VideoDownloadManager.downloadEpisode( + ctx, + url ?: return@main, + folder, + meta, + currentLinks ?: return@main + ) } } } } - val adapter: RecyclerView.Adapter? = activity?.let { it -> + val adapter: RecyclerView.Adapter = EpisodeAdapter( - it, ArrayList(), - result_episodes, ) { episodeClick -> handleAction(episodeClick) } - } + result_episodes.adapter = adapter result_episodes.layoutManager = GridLayoutManager(context, 1) @@ -409,6 +501,12 @@ class ResultFragment : Fragment() { allEpisodes = it } + observe(viewModel.allEpisodesSubs) { + allEpisodesSubs = it + } + + + observe(viewModel.selectedSeason) { season -> result_season_button?.text = fromIndexToSeasonText(season) } @@ -437,6 +535,10 @@ class ResultFragment : Fragment() { (result_episodes.adapter as EpisodeAdapter).notifyDataSetChanged() } + observe(viewModel.id) { + currentId = it + } + observe(viewModel.resultResponse) { data -> when (data) { is Resource.Success -> { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt index e09ce310..a0cdac6e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel.kt @@ -26,7 +26,7 @@ class ResultViewModel : ViewModel() { private val dubStatus: MutableLiveData = MutableLiveData() private val page: MutableLiveData = MutableLiveData() - private val id: MutableLiveData = MutableLiveData() + val id: MutableLiveData = MutableLiveData() val selectedSeason: MutableLiveData = MutableLiveData(-2) val seasonSelections: MutableLiveData> = MutableLiveData() @@ -208,6 +208,41 @@ class ResultViewModel : ViewModel() { loadEpisode(episode.id, episode.data, isCasting, callback) } + suspend fun loadEpisode( + episode: ResultEpisode, + isCasting: Boolean, + ) : Resource { + return loadEpisode(episode.id, episode.data, isCasting) + } + + private suspend fun loadEpisode( + id: Int, + data: String, + isCasting: Boolean, + ): Resource { + if (_allEpisodes.value?.contains(id) == true) { + _allEpisodes.value?.remove(id) + } + val links = ArrayList() + val subs = ArrayList() + return safeApiCall { + getApiFromName(_apiName.value).loadLinks(data, isCasting, { subtitleFile -> + if (!subs.any { it.url == subtitleFile.url }) { + subs.add(subtitleFile) + _allEpisodesSubs.value?.set(id, subs) + _allEpisodesSubs.postValue(_allEpisodesSubs.value) + } + }) { link -> + if (!links.any { it.url == link.url }) { + links.add(link) + _allEpisodes.value?.set(id, links) + _allEpisodes.postValue(_allEpisodes.value) + } + } + EpisodeData(links, subs) + } + } + private fun loadEpisode( id: Int, data: String, @@ -215,27 +250,7 @@ class ResultViewModel : ViewModel() { callback: (Resource) -> Unit, ) = viewModelScope.launch { - if (_allEpisodes.value?.contains(id) == true) { - _allEpisodes.value?.remove(id) - } - val links = ArrayList() - val subs = ArrayList() - val localData = safeApiCall { - getApiFromName(_apiName.value).loadLinks(data, isCasting, { subtitleFile -> - if (!subs.any { it.url == subtitleFile.url }) { - subs.add(subtitleFile) - _allEpisodesSubs.value?.set(id, subs) - _allEpisodesSubs.postValue(_allEpisodesSubs.value) - } - }) { link -> - if (!links.any { it.url == link.url }) { - links.add(link) - _allEpisodes.value?.set(id, links) - _allEpisodes.postValue(_allEpisodes.value) - } - } - EpisodeData(links, subs) - } + val localData = loadEpisode(id, data, isCasting) callback.invoke(localData) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index ca9a4d60..e895a997 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -6,6 +6,9 @@ import com.fasterxml.jackson.databind.DeserializationFeature import com.fasterxml.jackson.databind.json.JsonMapper import com.fasterxml.jackson.module.kotlin.KotlinModule +const val DOWNLOAD_HEADER_CACHE = "download_header_cache" +const val DOWNLOAD_EPISODE_CACHE = "download_episode_cache" + const val PREFERENCES_NAME: String = "rebuild_preference" object DataStore { diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 5bd714cb..3275fd0b 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -13,4 +13,27 @@ @id/cast_button_type_forward_30_seconds + + + Chromecast Episode + Chromecast Mirror + Play In App + Play In External App + Play In Browser + Copy Link + Auto Download + Download Mirror + Reload Links + + + 4 + 5 + 1 + 2 + 3 + 9 + 6 + 7 + 8 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1fc40357..1aa747a1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,4 +39,5 @@ Play Episode Allow to download episodes Download + Error Loading Links \ No newline at end of file