From 90d6f17181bf32e4354f1ab49883db3a4ab92e8d Mon Sep 17 00:00:00 2001 From: LagradOst Date: Thu, 10 Jun 2021 17:15:14 +0200 Subject: [PATCH] chromecast stuff --- .../animeproviders/ShiroProvider.kt | 2 +- .../cloudstream3/ui/result/EpisodeAdapter.kt | 30 ++++- .../cloudstream3/ui/result/ResultFragment.kt | 126 +++++++++++++++--- .../cloudstream3/ui/result/ResultViewModel.kt | 29 +++- .../cloudstream3/utils/CastOptionsProvider.kt | 3 +- app/src/main/res/drawable/go_back_30.xml | 4 +- app/src/main/res/drawable/go_forward_30.xml | 4 +- app/src/main/res/layout/fragment_result.xml | 6 +- 8 files changed, 159 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ShiroProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ShiroProvider.kt index 900b14e0..f4c505fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ShiroProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/animeproviders/ShiroProvider.kt @@ -189,7 +189,7 @@ class ShiroProvider : MainAPI() { data.english, data.japanese, data.name.replace("Dubbed", ""),//data.canonicalTitle ?: data.name.replace("Dubbed", ""), - "$mainUrl/${slug}", + "$mainUrl/anime/${slug}", this.name, getType(data.type ?: ""), "https://cdn.shiro.is/${data.image}", 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 7f6a1dde..c067967d 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 @@ -8,19 +8,23 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import androidx.recyclerview.widget.RecyclerView +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.isCastApiAvailable import kotlinx.android.synthetic.main.result_episode.view.* -const val ACTION_PLAY_EPISODE = 1 -const val ACTION_RELOAD_EPISODE = 2 +const val ACTION_PLAY_EPISODE_IN_PLAYER = 1 +const val ACTION_RELOAD_EPISODE = 4 +const val ACTION_CHROME_CAST_EPISODE = 2 data class EpisodeClickEvent(val action: Int, val data: ResultEpisode) class EpisodeAdapter( private var activity: Activity, var cardList: ArrayList, - val resView: RecyclerView, - val clickCallback: (EpisodeClickEvent) -> Unit, + private val resView: RecyclerView, + private val clickCallback: (EpisodeClickEvent) -> Unit, ) : RecyclerView.Adapter() { @@ -70,11 +74,23 @@ class EpisodeAdapter( ) v.layoutParams = param } - setWidth(episodeViewPrecentage, card.watchProgress) - setWidth(episodeViewPercentageOff, 1 - card.watchProgress) + + val watchProgress = card.getWatchProgress() + setWidth(episodeViewPrecentage, watchProgress) + setWidth(episodeViewPercentageOff, 1 - watchProgress) episodeHolder.setOnClickListener { - clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE, card)) + if (activity.isCastApiAvailable()) { + val castContext = CastContext.getSharedInstance(activity) + println("SSTATE: " + castContext.castState + "<<") + if (castContext.castState == CastState.CONNECTED) { + clickCallback.invoke(EpisodeClickEvent(ACTION_CHROME_CAST_EPISODE, card)) + } else { + clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card)) + } + } else { + clickCallback.invoke(EpisodeClickEvent(ACTION_PLAY_EPISODE_IN_PLAYER, card)) + } } } } 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 b5a5eee2..9e6f1bab 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 @@ -1,12 +1,16 @@ package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE import android.view.ViewGroup +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.widget.NestedScrollView @@ -18,9 +22,16 @@ import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.request.RequestOptions.bitmapTransform +import com.google.android.exoplayer2.ext.cast.CastPlayer +import com.google.android.exoplayer2.util.MimeTypes +import com.google.android.gms.cast.MediaInfo +import com.google.android.gms.cast.MediaMetadata +import com.google.android.gms.cast.MediaQueueItem +import com.google.android.gms.cast.MediaStatus.REPEAT_MODE_REPEAT_SINGLE import com.google.android.gms.cast.framework.CastButtonFactory import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastState +import com.google.android.gms.common.images.WebImage import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.AnimeLoadResponse import com.lagradost.cloudstream3.LoadResponse @@ -35,20 +46,27 @@ import com.lagradost.cloudstream3.ui.player.PlayerFragment import com.lagradost.cloudstream3.utils.ExtractorLink import jp.wasabeef.glide.transformations.BlurTransformation import kotlinx.android.synthetic.main.fragment_result.* +import org.json.JSONObject const val MAX_SYNO_LENGH = 300 data class ResultEpisode( val name: String?, + val poster: String?, val episode: Int, val season: Int?, val data: Any, val apiName: String, val id: Int, val index: Int, - val watchProgress: Float, // 0-1 + val progress: Long, // time in MS + val duration: Long, // duration in MS ) +fun ResultEpisode.getWatchProgress(): Float { + return progress.toFloat() / duration +} + class ResultFragment : Fragment() { companion object { fun newInstance(url: String, slug: String, apiName: String) = @@ -80,6 +98,8 @@ class ResultFragment : Fragment() { super.onDestroy() } + var currentPoster: String? = null + @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -120,7 +140,6 @@ class ResultFragment : Fragment() { activity?.onBackPressed() } - val adapter: RecyclerView.Adapter? = activity?.let { it -> EpisodeAdapter( it, @@ -130,35 +149,82 @@ class ResultFragment : Fragment() { val id = episodeClick.data.id val index = episodeClick.data.index val buildInPlayer = true - if (buildInPlayer) { - (requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction() - .setCustomAnimations(R.anim.enter_anim, - R.anim.exit_anim, - R.anim.pop_enter, - R.anim.pop_exit) - .add(R.id.homeRoot, PlayerFragment.newInstance(PlayerData(index, null, 0))) - .commit() - } else { - when (episodeClick.action) { + when (episodeClick.action) { + ACTION_CHROME_CAST_EPISODE -> { + Toast.makeText(activity, "Loading links", Toast.LENGTH_SHORT).show() - /* - ACTION_PLAY_EPISODE -> { - if (allEpisodes.containsKey(id)) { - playEpisode(allEpisodes[id], index) - } else { - viewModel.loadEpisode(episodeClick.data) { res -> - if (res is Resource.Success) { - playEpisode(allEpisodes[id], index) - } + viewModel.loadEpisode(episodeClick.data, true) { data -> + when (data) { + is Resource.Failure -> { + Toast.makeText(activity, "Failed to load links", Toast.LENGTH_SHORT).show() + } + is Resource.Success -> { + val epData = episodeClick.data + val links = data.value + + val castContext = CastContext.getSharedInstance(requireContext()) + + val mediaItems = links.map { + val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE) + movieMetadata.putString( + MediaMetadata.KEY_TITLE, + + "Episode ${epData.episode}" + + if (epData.name != null) + "- ${epData.name}" + else + "" + ) + movieMetadata.putString(MediaMetadata.KEY_ALBUM_ARTIST, + epData.name ?: "Episode ${epData.episode}") + + val srcPoster = epData.poster ?: currentPoster + if (srcPoster != null) { + movieMetadata.addImage(WebImage(Uri.parse(srcPoster))) + } + + MediaQueueItem.Builder( + MediaInfo.Builder(it.url) + .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + .setContentType(MimeTypes.VIDEO_UNKNOWN) + .setCustomData(JSONObject().put("data", it.name)) + .setMetadata(movieMetadata) + .build() + ) + .build() + }.toTypedArray() + + val castPlayer = CastPlayer(castContext) + castPlayer.loadItems( + mediaItems, + 0, + epData.progress, + REPEAT_MODE_REPEAT_SINGLE + ) } } } - ACTION_RELOAD_EPISODE -> viewModel.loadEpisode(episodeClick.data) { res -> + } + + ACTION_PLAY_EPISODE_IN_PLAYER -> { + if (buildInPlayer) { + (requireActivity() as AppCompatActivity).supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.enter_anim, + R.anim.exit_anim, + R.anim.pop_enter, + R.anim.pop_exit) + .add(R.id.homeRoot, PlayerFragment.newInstance(PlayerData(index, null, 0))) + .commit() + } + } + ACTION_RELOAD_EPISODE -> { + /*viewModel.load(episodeClick.data) { res -> if (res is Resource.Success) { playEpisode(allEpisodes[id], index) } }*/ } + } } } @@ -184,6 +250,22 @@ class ResultFragment : Fragment() { if (d is LoadResponse) { result_bookmark_button.text = "Watching" + currentPoster = d.posterUrl + + result_openinbrower.setOnClickListener { + val i = Intent(Intent.ACTION_VIEW) + i.data = Uri.parse(d.url) + startActivity(i) + } + + result_share.setOnClickListener { + val i = Intent(Intent.ACTION_SEND) + i.type = "text/plain" + i.putExtra(Intent.EXTRA_SUBJECT, d.name) + i.putExtra(Intent.EXTRA_TEXT, d.url) + startActivity(Intent.createChooser(i, d.name)) + } + if (d.year != null) { result_year.visibility = View.VISIBLE result_year.text = d.year.toString() 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 58b0cc32..ae31da29 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 @@ -41,13 +41,15 @@ class ResultViewModel : ViewModel() { for ((index, i) in dataList.withIndex()) { episodes.add(ResultEpisode( null, // TODO ADD NAMES + null, index + 1, //TODO MAKE ABLE TO NOT HAVE SOME EPISODE null, // TODO FIX SEASON i, apiName, (d.url + index).hashCode(), index, - 0f,//(index * 0.1f),//TODO TEST; REMOVE + 0,//(index * 0.1f),//TODO TEST; REMOVE + 0, )) } _episodes.postValue(episodes) @@ -59,25 +61,28 @@ class ResultViewModel : ViewModel() { for ((index, i) in d.episodes.withIndex()) { episodes.add(ResultEpisode( null, // TODO ADD NAMES + null, index + 1, //TODO MAKE ABLE TO NOT HAVE SOME EPISODE null, // TODO FIX SEASON i, apiName, (d.url + index).hashCode(), index, - 0f,//(index * 0.1f),//TODO TEST; REMOVE + 0,//(index * 0.1f),//TODO TEST; REMOVE + 0, )) } _episodes.postValue(episodes) } is MovieLoadResponse -> { _episodes.postValue(arrayListOf(ResultEpisode(null, + null, 0, null, d.movieUrl, d.apiName, (d.url).hashCode(), 0, - 0f))) + 0, 0))) } } } @@ -95,17 +100,26 @@ class ResultViewModel : ViewModel() { private var _apiName: MutableLiveData = MutableLiveData() - fun loadEpisode(episode: ResultEpisode, isCasting : Boolean, callback: (Resource) -> Unit) { + fun loadEpisode( + episode: ResultEpisode, + isCasting: Boolean, + callback: (Resource>) -> Unit, + ) { loadEpisode(episode.id, episode.data, isCasting, callback) } - fun loadEpisode(id: Int, data: Any, isCasting : Boolean, callback: (Resource) -> Unit) = + private fun loadEpisode( + id: Int, + data: Any, + isCasting: Boolean, + callback: (Resource>) -> Unit, + ) = viewModelScope.launch { if (_allEpisodes.value?.contains(id) == true) { _allEpisodes.value?.remove(id) } val links = ArrayList() - val data = safeApiCall { + val localData = safeApiCall { getApiFromName(_apiName.value).loadLinks(data, isCasting) { //TODO IMPLEMENT CASTING for (i in links) { if (i.url == it.url) return@loadLinks @@ -116,8 +130,9 @@ class ResultViewModel : ViewModel() { // _allEpisodes.value?.get(episode.id)?.add(it) } + links } - callback.invoke(data) + callback.invoke(localData) } fun loadIndex(index: Int): ResultEpisode? { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/CastOptionsProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/CastOptionsProvider.kt index 908a8668..9403b74a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/CastOptionsProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/CastOptionsProvider.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.utils import android.content.Context +import com.google.android.gms.cast.CastMediaControlIntent import com.google.android.gms.cast.framework.CastOptions import com.google.android.gms.cast.framework.OptionsProvider import com.google.android.gms.cast.framework.SessionProvider @@ -35,7 +36,7 @@ class CastOptionsProvider : OptionsProvider { .build() return CastOptions.Builder() - .setReceiverApplicationId("A12D4273") + .setReceiverApplicationId( CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID) .setStopReceiverApplicationWhenEndingSession(true) .setCastMediaOptions(mediaOptions) .build() diff --git a/app/src/main/res/drawable/go_back_30.xml b/app/src/main/res/drawable/go_back_30.xml index 8ab536ee..3b78f9fc 100644 --- a/app/src/main/res/drawable/go_back_30.xml +++ b/app/src/main/res/drawable/go_back_30.xml @@ -1,8 +1,8 @@ + android:layout_height="180dp">