diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index c1d282e4..c4f60756 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3 import android.app.PictureInPictureParams import android.content.pm.PackageManager +import android.net.Uri import android.os.Build import android.os.Bundle import com.google.android.material.bottomnavigation.BottomNavigationView @@ -12,10 +13,20 @@ import androidx.lifecycle.ViewModelStoreOwner import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController +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 +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.lagradost.cloudstream3.UIHelper.checkWrite import com.lagradost.cloudstream3.UIHelper.hasPIPPermission import com.lagradost.cloudstream3.UIHelper.requestRW import com.lagradost.cloudstream3.UIHelper.shouldShowPIPMode +import kotlinx.android.synthetic.main.fragment_result.* class MainActivity : AppCompatActivity() { /*, ViewModelStoreOwner { @@ -99,5 +110,32 @@ class MainActivity : AppCompatActivity() { requestRW() if (checkWrite()) return } + + CastButtonFactory.setUpMediaRouteButton(this, media_route_button) + /* + val castContext = CastContext.getSharedInstance(applicationContext) + fun buildMediaQueueItem(video: String): MediaQueueItem { + // val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_PHOTO) + //movieMetadata.putString(MediaMetadata.KEY_TITLE, "CloudStream") + val mediaInfo = MediaInfo.Builder(Uri.parse(video).toString()) + .setStreamType(MediaInfo.STREAM_TYPE_NONE) + .setContentType(MimeTypes.IMAGE_JPEG) + // .setMetadata(movieMetadata).build() + .build() + return MediaQueueItem.Builder(mediaInfo).build() + }*/ + /* + castContext.addCastStateListener { state -> + if (state == CastState.CONNECTED) { + println("TESTING") + val isCasting = castContext?.sessionManager?.currentCastSession?.remoteMediaClient?.currentItem != null + if(!isCasting) { + val castPlayer = CastPlayer(castContext) + println("LOAD ITEM") + + castPlayer.loadItem(buildMediaQueueItem("https://cdn.discordapp.com/attachments/551382684560261121/730169809408622702/ChromecastLogo6.png"),0) + } + } + }*/ } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt index 96520582..bbef5961 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/UIHelper.kt @@ -6,6 +6,7 @@ import android.app.AppOpsManager import android.content.Context import android.content.pm.PackageManager import android.content.res.Resources +import android.graphics.Color import android.media.AudioAttributes import android.media.AudioFocusRequest import android.media.AudioManager @@ -13,6 +14,7 @@ import android.os.Build import android.view.View import android.view.WindowManager import android.view.inputmethod.InputMethodManager +import androidx.annotation.ColorInt import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -21,9 +23,10 @@ import androidx.preference.PreferenceManager import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability -import com.lagradost.cloudstream3.UIHelper.getGridFormat import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.utils.Event +import kotlin.math.roundToInt + object UIHelper { val Int.toPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt() @@ -143,6 +146,21 @@ object UIHelper { return isCastApiAvailable } + fun adjustAlpha(@ColorInt color: Int, factor: Float): Int { + val alpha = (Color.alpha(color) * factor).roundToInt() + val red = Color.red(color) + val green = Color.green(color) + val blue = Color.blue(color) + return Color.argb(alpha, red, green, blue) + } + + fun Context.colorFromAttribute(attribute: Int): Int { + val attributes = obtainStyledAttributes(intArrayOf(attribute)) + val color = attributes.getColor(0, 0) + attributes.recycle() + return color + } + fun getFocusRequest(): AudioFocusRequest? { if (_AudioFocusRequest != null) return _AudioFocusRequest return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index cc69cdff..d6042a6d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -6,14 +6,22 @@ import android.view.View.INVISIBLE import android.view.View.VISIBLE import android.widget.ImageView import androidx.appcompat.app.AlertDialog +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.json.JsonMapper +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.fasterxml.jackson.module.kotlin.readValue 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.CastSession import com.google.android.gms.cast.framework.media.uicontroller.UIController import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.UIHelper.hideSystemUI +import com.lagradost.cloudstream3.utils.Coroutines +import kotlinx.coroutines.delay import org.json.JSONObject +import java.lang.Exception class SkipOpController(val view: ImageView) : UIController() { init { @@ -24,7 +32,12 @@ class SkipOpController(val view: ImageView) : UIController() { } } -class SelectSourceController(val view: ImageView) : UIController() { +data class MetadataSource(val name: String) +data class MetadataHolder(val data: List) + +class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() { + private val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule()) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() init { view.setImageResource(R.drawable.ic_baseline_playlist_play_24) @@ -33,45 +46,52 @@ class SelectSourceController(val view: ImageView) : UIController() { //println(remoteMediaClient.mediaInfo.customData) //remoteMediaClient.queueJumpToItem() lateinit var dialog: AlertDialog - val items = mutableListOf>() - for (i in 0 until remoteMediaClient.mediaQueue.itemCount) { - (remoteMediaClient.mediaQueue.getItemAtIndex(i)?.media?.customData?.get("data") as? String)?.let { name -> - items.add( - remoteMediaClient.mediaQueue.getItemAtIndex(i)!!.itemId to name - ) + val holder = getCurrentMetaData() + + if (holder != null) { + val items = holder.data + if (items.isNotEmpty() && remoteMediaClient.currentItem != null) { + val builder = AlertDialog.Builder(view.context, R.style.AlertDialogCustom) + builder.setTitle("Pick source") + + builder.setSingleChoiceItems( + items.map { it.name }.toTypedArray(), + remoteMediaClient.mediaQueue.indexOfItemWithId(remoteMediaClient.currentItem.itemId) + ) { _, which -> + val itemId = remoteMediaClient.mediaQueue.itemIds?.get(which) + + itemId?.let { id -> + remoteMediaClient.queueJumpToItem( + id, + remoteMediaClient.approximateStreamPosition, + remoteMediaClient.mediaInfo.customData + ) + } + + dialog.dismiss() + } + dialog = builder.create() + dialog.show() } } + } + } - // TODO FIX - if (items.isNotEmpty()) { - val builder = AlertDialog.Builder(view.context, R.style.AlertDialogCustom) - builder.setTitle("Pick source") - - builder.setSingleChoiceItems( - items.map { it.second }.toTypedArray(), - remoteMediaClient.currentItem.itemId - 1 - ) { _, which -> - println( - remoteMediaClient.queueJumpToItem( - items[which].first, - remoteMediaClient.approximateStreamPosition, - null - ) - ) - dialog.dismiss() - } - dialog = builder.create() - dialog.show() - } + private fun getCurrentMetaData(): MetadataHolder? { + return try { + val data = remoteMediaClient.mediaInfo.customData + mapper.readValue(data.toString()) + } catch (e: Exception) { + null } } override fun onMediaStatusUpdated() { super.onMediaStatusUpdated() - // If there's 1 item it won't show - val dataString = remoteMediaClient.mediaQueue.getItemAtIndex(1)?.media?.customData?.get("data") as? String - - view.visibility = if (dataString != null) VISIBLE else INVISIBLE + view.visibility = + if ((getCurrentMetaData()?.data?.size + ?: 0) > 1 + ) VISIBLE else INVISIBLE } override fun onSessionConnected(castSession: CastSession?) { @@ -107,7 +127,7 @@ class ControllerActivity : ExpandedControllerActivity() { val skipBackButton: ImageView = getButtonImageViewAt(1) val skipForwardButton: ImageView = getButtonImageViewAt(2) val skipOpButton: ImageView = getButtonImageViewAt(3) - uiMediaController.bindViewToUIController(sourcesButton, SelectSourceController(sourcesButton)) + uiMediaController.bindViewToUIController(sourcesButton, SelectSourceController(sourcesButton, this)) uiMediaController.bindViewToUIController(skipBackButton, SkipTimeController(skipBackButton, false)) uiMediaController.bindViewToUIController(skipForwardButton, SkipTimeController(skipForwardButton, true)) uiMediaController.bindViewToUIController(skipOpButton, SkipOpController(skipOpButton)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/MiniControllerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/MiniControllerFragment.kt new file mode 100644 index 00000000..32134c72 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/MiniControllerFragment.kt @@ -0,0 +1,37 @@ +package com.lagradost.cloudstream3.ui + +import android.os.Bundle +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.RelativeLayout +import com.google.android.gms.cast.framework.media.widget.MiniControllerFragment +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.UIHelper.adjustAlpha +import com.lagradost.cloudstream3.UIHelper.colorFromAttribute +import com.lagradost.cloudstream3.UIHelper.toPx + +class MyMiniControllerFragment : MiniControllerFragment() { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // SEE https://github.com/dandar3/android-google-play-services-cast-framework/blob/master/res/layout/cast_mini_controller.xml + try { + val progressBar: ProgressBar? = view.findViewById(R.id.progressBar) + val containerAll: LinearLayout? = view.findViewById(R.id.container_all) + + context?.let { ctx -> + progressBar?.setBackgroundColor(adjustAlpha(ctx.colorFromAttribute(R.attr.colorPrimary), 0.35f)) + val params = RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, 2.toPx) + + progressBar?.layoutParams = params + } + val child = containerAll?.getChildAt(0) + child?.alpha = 0f // REMOVE GRADIENT + + } catch (e: Exception) { + // JUST IN CASE + } + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt index 637104ab..0133b5e5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerFragment.kt @@ -84,6 +84,7 @@ import com.lagradost.cloudstream3.utils.getId import kotlinx.android.synthetic.main.fragment_player.* import kotlinx.android.synthetic.main.player_custom_layout.* import kotlinx.coroutines.* +import org.json.JSONArray import org.json.JSONObject import java.io.File import javax.net.ssl.HttpsURLConnection @@ -608,6 +609,7 @@ class PlayerFragment : Fragment() { private var allEpisodes: HashMap> = HashMap() private var episodes: ArrayList = ArrayList() var currentPoster: String? = null + var currentHeaderName: String? = null //region PIP MODE private fun getPen(code: PlayerEventType): PendingIntent { @@ -717,7 +719,7 @@ class PlayerFragment : Fragment() { if (activity?.isCastApiAvailable() == true) { CastButtonFactory.setUpMediaRouteButton(activity, player_media_route_button) - val castContext = CastContext.getSharedInstance(requireActivity().applicationContext) + val castContext = CastContext.getSharedInstance(requireContext()) if (castContext.castState != CastState.NO_DEVICES_AVAILABLE) player_media_route_button.visibility = VISIBLE castContext.addCastStateListener { state -> @@ -732,20 +734,22 @@ class PlayerFragment : Fragment() { val index = links.indexOf(getCurrentUrl()) + val customData = + links.map { JSONObject().put("name", it.name) } + val jsonArray = JSONArray() + for (item in customData) { + jsonArray.put(item) + } + 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, + movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, epData.name ?: "Episode ${epData.episode}") + if (currentHeaderName != null) + movieMetadata.putString(MediaMetadata.KEY_TITLE, currentHeaderName) + val srcPoster = epData.poster ?: currentPoster if (srcPoster != null) { movieMetadata.addImage(WebImage(Uri.parse(srcPoster))) @@ -755,7 +759,8 @@ class PlayerFragment : Fragment() { MediaInfo.Builder(it.url) .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) .setContentType(MimeTypes.VIDEO_UNKNOWN) - .setCustomData(JSONObject().put("data", it.name)) + + .setCustomData(JSONObject().put("data", jsonArray)) .setMetadata(movieMetadata) .build() ) @@ -838,6 +843,7 @@ class PlayerFragment : Fragment() { if (d is LoadResponse) { localData = d currentPoster = d.posterUrl + currentHeaderName = d.name } } is Resource.Failure -> { 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 fbacb555..dc08cea5 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,7 +1,6 @@ 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 @@ -16,7 +15,6 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.widget.NestedScrollView import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import androidx.mediarouter.app.MediaRouteButton import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide @@ -33,6 +31,7 @@ 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.* import com.lagradost.cloudstream3.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.UIHelper.isCastApiAvailable import com.lagradost.cloudstream3.mvvm.Resource @@ -42,9 +41,9 @@ 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.JSONArray import org.json.JSONObject -import android.app.ProgressDialog -import com.lagradost.cloudstream3.* + const val MAX_SYNO_LENGH = 300 @@ -80,6 +79,7 @@ class ResultFragment : Fragment() { private lateinit var viewModel: ResultViewModel private var allEpisodes: HashMap> = HashMap() + var currentHeaderName: String? = null override fun onCreateView( inflater: LayoutInflater, @@ -157,8 +157,8 @@ class ResultFragment : Fragment() { //dialog.show() Toast.makeText(activity, "Loading links", Toast.LENGTH_SHORT).show() - viewModel.loadEpisode(episodeClick.data, true) { data -> - // dialog.dismiss() + viewModel.loadEpisode(episodeClick.data, false) { data -> + // dialog.dismiss() when (data) { is Resource.Failure -> { Toast.makeText(activity, "Failed to load links", Toast.LENGTH_SHORT).show() @@ -169,30 +169,30 @@ class ResultFragment : Fragment() { val castContext = CastContext.getSharedInstance(requireContext()) + val customData = + links.map { JSONObject().put("name", it.name) } + val jsonArray = JSONArray() + for (item in customData) { + jsonArray.put(item) + } + 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, + movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, epData.name ?: "Episode ${epData.episode}") + if (currentHeaderName != null) + movieMetadata.putString(MediaMetadata.KEY_TITLE, currentHeaderName) + 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)) + .setCustomData(JSONObject().put("data", jsonArray)) .setMetadata(movieMetadata) .build() ) @@ -255,6 +255,8 @@ class ResultFragment : Fragment() { if (d is LoadResponse) { result_bookmark_button.text = "Watching" + currentHeaderName = d.name + currentPoster = d.posterUrl result_openinbrower.setOnClickListener { 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 32961de4..3ffe92b1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/CastOptionsProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/CastOptionsProvider.kt @@ -38,7 +38,8 @@ class CastOptionsProvider : OptionsProvider { return CastOptions.Builder() .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID) - //.setReceiverApplicationId("C0868879") // C0868879 = SAMPLE, CHANGE TO A NICE ID at https://developers.google.com/cast/docs/registration + //.setReceiverApplicationId("") + // C0868879 = SAMPLE, CHANGE TO A NICE ID at https://developers.google.com/cast/docs/registration .setStopReceiverApplicationWhenEndingSession(true) .setCastMediaOptions(mediaOptions) .build() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt new file mode 100644 index 00000000..6bfbc1a3 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -0,0 +1,13 @@ +package com.lagradost.cloudstream3.utils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +object Coroutines { + fun main(work: suspend (() -> Unit)) { + CoroutineScope(Dispatchers.Main).launch { + work() + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/solid_primary.xml b/app/src/main/res/drawable/solid_primary.xml new file mode 100644 index 00000000..9ddd6f04 --- /dev/null +++ b/app/src/main/res/drawable/solid_primary.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 39f62bc8..72cc9101 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -42,13 +42,14 @@ app:layout_constraintBottom_toTopOf="@+id/nav_view" android:id="@+id/cast_mini_controller_holder" > + diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index da8d8b6d..56eecfa9 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -229,7 +229,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="gone" - class="com.google.android.gms.cast.framework.media.widget.MiniControllerFragment" + class="com.lagradost.cloudstream3.ui.MyMiniControllerFragment" tools:ignore="FragmentTagUsage"> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 9a5a0e84..436ab06f 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -99,6 +99,7 @@ scrollable