forked from recloudstream/cloudstream
		
	chromecast stuff
This commit is contained in:
		
							parent
							
								
									6579eb3083
								
							
						
					
					
						commit
						50abf2c74c
					
				
					 14 changed files with 323 additions and 118 deletions
				
			
		|  | @ -53,7 +53,7 @@ abstract class MainAPI { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // callback is fired once a link is found, will return true if method is executed successfully |     // callback is fired once a link is found, will return true if method is executed successfully | ||||||
|     open fun loadLinks(data: Any, isCasting: Boolean, callback: (ExtractorLink) -> Unit): Boolean { |     open fun loadLinks(data: String, isCasting: Boolean, callback: (ExtractorLink) -> Unit): Boolean { | ||||||
|         return false |         return false | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -169,8 +169,8 @@ data class AnimeLoadResponse( | ||||||
|     override val posterUrl: String?, |     override val posterUrl: String?, | ||||||
|     override val year: Int?, |     override val year: Int?, | ||||||
| 
 | 
 | ||||||
|     val dubEpisodes: ArrayList<Any>?, |     val dubEpisodes: ArrayList<String>?, | ||||||
|     val subEpisodes: ArrayList<Any>?, |     val subEpisodes: ArrayList<String>?, | ||||||
|     val showStatus: ShowStatus?, |     val showStatus: ShowStatus?, | ||||||
| 
 | 
 | ||||||
|     override val plot: String?, |     override val plot: String?, | ||||||
|  |  | ||||||
|  | @ -2,32 +2,27 @@ package com.lagradost.cloudstream3 | ||||||
| 
 | 
 | ||||||
| import android.app.PictureInPictureParams | import android.app.PictureInPictureParams | ||||||
| import android.content.pm.PackageManager | import android.content.pm.PackageManager | ||||||
| import android.net.Uri |  | ||||||
| import android.os.Build | import android.os.Build | ||||||
| import android.os.Bundle | import android.os.Bundle | ||||||
| import com.google.android.material.bottomnavigation.BottomNavigationView |  | ||||||
| import androidx.appcompat.app.AppCompatActivity | import androidx.appcompat.app.AppCompatActivity | ||||||
| import androidx.core.content.ContentProviderCompat.requireContext |  | ||||||
| import androidx.lifecycle.ViewModelStore |  | ||||||
| import androidx.lifecycle.ViewModelStoreOwner |  | ||||||
| import androidx.navigation.findNavController | import androidx.navigation.findNavController | ||||||
| import androidx.navigation.ui.AppBarConfiguration | import androidx.navigation.ui.AppBarConfiguration | ||||||
| import androidx.navigation.ui.setupWithNavController | import androidx.navigation.ui.setupWithNavController | ||||||
| import com.google.android.exoplayer2.ext.cast.CastPlayer | import com.google.android.gms.cast.ApplicationMetadata | ||||||
| import com.google.android.exoplayer2.util.MimeTypes | import com.google.android.gms.cast.Cast | ||||||
| import com.google.android.gms.cast.MediaInfo | import com.google.android.gms.cast.LaunchOptions | ||||||
| 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.CastButtonFactory | ||||||
| import com.google.android.gms.cast.framework.CastContext | import com.google.android.gms.cast.framework.CastContext | ||||||
| import com.google.android.gms.cast.framework.CastState | import com.google.android.gms.cast.framework.CastSession | ||||||
|  | import com.google.android.gms.cast.framework.SessionManagerListener | ||||||
|  | import com.google.android.material.bottomnavigation.BottomNavigationView | ||||||
| import com.lagradost.cloudstream3.UIHelper.checkWrite | import com.lagradost.cloudstream3.UIHelper.checkWrite | ||||||
| import com.lagradost.cloudstream3.UIHelper.hasPIPPermission | import com.lagradost.cloudstream3.UIHelper.hasPIPPermission | ||||||
| import com.lagradost.cloudstream3.UIHelper.requestRW | import com.lagradost.cloudstream3.UIHelper.requestRW | ||||||
| import com.lagradost.cloudstream3.UIHelper.shouldShowPIPMode | import com.lagradost.cloudstream3.UIHelper.shouldShowPIPMode | ||||||
| import kotlinx.android.synthetic.main.fragment_result.* | import kotlinx.android.synthetic.main.fragment_result.* | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| class MainActivity : AppCompatActivity() { | class MainActivity : AppCompatActivity() { | ||||||
|     /*, ViewModelStoreOwner { |     /*, ViewModelStoreOwner { | ||||||
|         private val appViewModelStore: ViewModelStore by lazy { |         private val appViewModelStore: ViewModelStore by lazy { | ||||||
|  | @ -110,8 +105,8 @@ class MainActivity : AppCompatActivity() { | ||||||
|             requestRW() |             requestRW() | ||||||
|             if (checkWrite()) return |             if (checkWrite()) return | ||||||
|         } |         } | ||||||
| 
 |  | ||||||
|         CastButtonFactory.setUpMediaRouteButton(this, media_route_button) |         CastButtonFactory.setUpMediaRouteButton(this, media_route_button) | ||||||
|  | 
 | ||||||
|         /* |         /* | ||||||
|         val castContext = CastContext.getSharedInstance(applicationContext) |         val castContext = CastContext.getSharedInstance(applicationContext) | ||||||
|          fun buildMediaQueueItem(video: String): MediaQueueItem { |          fun buildMediaQueueItem(video: String): MediaQueueItem { | ||||||
|  |  | ||||||
|  | @ -178,7 +178,7 @@ class ShiroProvider : MainAPI() { | ||||||
|         val mapped = response.let { mapper.readValue<AnimePage>(it.text) } |         val mapped = response.let { mapper.readValue<AnimePage>(it.text) } | ||||||
|         val data = mapped.data |         val data = mapped.data | ||||||
|         val isDubbed = data.language == "dubbed" |         val isDubbed = data.language == "dubbed" | ||||||
|         val episodes = ArrayList<Any>(data.episodes ?: ArrayList()) |         val episodes = ArrayList<String>(data.episodes?.map { it.videos[0].video_id } ?: ArrayList<String>()) | ||||||
|         val status = when (data.status) { |         val status = when (data.status) { | ||||||
|             "current" -> ShowStatus.Ongoing |             "current" -> ShowStatus.Ongoing | ||||||
|             "finished" -> ShowStatus.Completed |             "finished" -> ShowStatus.Completed | ||||||
|  | @ -205,12 +205,9 @@ class ShiroProvider : MainAPI() { | ||||||
|         ) |         ) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun loadLinks(data: Any, isCasting: Boolean, callback: (ExtractorLink) -> Unit): Boolean { |     override fun loadLinks(data: String, isCasting: Boolean, callback: (ExtractorLink) -> Unit): Boolean { | ||||||
|         if (data is ShiroEpisodes) { |         return Vidstream().getUrl(data, isCasting) { | ||||||
|             return Vidstream().getUrl(data.videos[0].video_id, isCasting) { |  | ||||||
|             callback.invoke(it) |             callback.invoke(it) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|         return false |  | ||||||
|     } |  | ||||||
| } | } | ||||||
|  | @ -9,19 +9,25 @@ import androidx.appcompat.app.AlertDialog | ||||||
| import com.fasterxml.jackson.databind.DeserializationFeature | import com.fasterxml.jackson.databind.DeserializationFeature | ||||||
| import com.fasterxml.jackson.databind.json.JsonMapper | import com.fasterxml.jackson.databind.json.JsonMapper | ||||||
| import com.fasterxml.jackson.module.kotlin.KotlinModule | import com.fasterxml.jackson.module.kotlin.KotlinModule | ||||||
| import com.fasterxml.jackson.module.kotlin.readValue | import com.google.android.gms.cast.MediaQueueItem | ||||||
| import com.google.android.gms.cast.MediaStatus.REPEAT_MODE_REPEAT_SINGLE | import com.google.android.gms.cast.MediaStatus.REPEAT_MODE_REPEAT_OFF | ||||||
| import com.google.android.gms.cast.framework.CastButtonFactory | import com.google.android.gms.cast.framework.CastButtonFactory | ||||||
| import com.google.android.gms.cast.framework.CastSession | 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.uicontroller.UIController | ||||||
| import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity | import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity | ||||||
|  | import com.lagradost.cloudstream3.APIHolder.getApiFromName | ||||||
| import com.lagradost.cloudstream3.R | import com.lagradost.cloudstream3.R | ||||||
| import com.lagradost.cloudstream3.UIHelper.hideSystemUI | import com.lagradost.cloudstream3.mvvm.Resource | ||||||
| import com.lagradost.cloudstream3.utils.Coroutines | import com.lagradost.cloudstream3.mvvm.safeApiCall | ||||||
| import kotlinx.coroutines.delay | import com.lagradost.cloudstream3.sortUrls | ||||||
| 
 | import com.lagradost.cloudstream3.ui.result.ResultEpisode | ||||||
|  | import com.lagradost.cloudstream3.utils.CastHelper.getMediaInfo | ||||||
|  | import com.lagradost.cloudstream3.utils.Coroutines.main | ||||||
|  | import com.lagradost.cloudstream3.utils.DataStore.toKotlinObject | ||||||
|  | import com.lagradost.cloudstream3.utils.ExtractorLink | ||||||
|  | import kotlinx.coroutines.Dispatchers | ||||||
|  | import kotlinx.coroutines.withContext | ||||||
| import org.json.JSONObject | import org.json.JSONObject | ||||||
| import java.lang.Exception |  | ||||||
| 
 | 
 | ||||||
| class SkipOpController(val view: ImageView) : UIController() { | class SkipOpController(val view: ImageView) : UIController() { | ||||||
|     init { |     init { | ||||||
|  | @ -32,8 +38,14 @@ class SkipOpController(val view: ImageView) : UIController() { | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| data class MetadataSource(val name: String) | data class MetadataHolder( | ||||||
| data class MetadataHolder(val data: List<MetadataSource>) |     val apiName: String, | ||||||
|  |     val title: String?, | ||||||
|  |     val poster: String?, | ||||||
|  |     val currentEpisodeIndex: Int, | ||||||
|  |     val episodes: List<ResultEpisode>, | ||||||
|  |     val currentLinks: List<ExtractorLink>, | ||||||
|  | ) | ||||||
| 
 | 
 | ||||||
| class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() { | class SelectSourceController(val view: ImageView, val activity: ControllerActivity) : UIController() { | ||||||
|     private val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule()) |     private val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule()) | ||||||
|  | @ -42,30 +54,48 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi | ||||||
|     init { |     init { | ||||||
|         view.setImageResource(R.drawable.ic_baseline_playlist_play_24) |         view.setImageResource(R.drawable.ic_baseline_playlist_play_24) | ||||||
|         view.setOnClickListener { |         view.setOnClickListener { | ||||||
|             //remoteMediaClient.mediaQueue.itemCount |  | ||||||
|             //println(remoteMediaClient.mediaInfo.customData) |  | ||||||
|             //remoteMediaClient.queueJumpToItem() |  | ||||||
|             lateinit var dialog: AlertDialog |             lateinit var dialog: AlertDialog | ||||||
|             val holder = getCurrentMetaData() |             val holder = getCurrentMetaData() | ||||||
| 
 | 
 | ||||||
|             if (holder != null) { |             if (holder != null) { | ||||||
|                 val items = holder.data |                 val items = holder.currentLinks | ||||||
|                 if (items.isNotEmpty() && remoteMediaClient.currentItem != null) { |                 if (items.isNotEmpty() && remoteMediaClient.currentItem != null) { | ||||||
|                     val builder = AlertDialog.Builder(view.context, R.style.AlertDialogCustom) |                     val builder = AlertDialog.Builder(view.context, R.style.AlertDialogCustom) | ||||||
|                     builder.setTitle("Pick source") |                     builder.setTitle("Pick source") | ||||||
| 
 | 
 | ||||||
|  |                     //https://developers.google.com/cast/docs/reference/web_receiver/cast.framework.messages.MediaInformation | ||||||
|  |                     val contentUrl = (remoteMediaClient.currentItem.media.contentUrl | ||||||
|  |                         ?: remoteMediaClient.currentItem.media.contentId) | ||||||
|  | 
 | ||||||
|                     builder.setSingleChoiceItems( |                     builder.setSingleChoiceItems( | ||||||
|                         items.map { it.name }.toTypedArray(), |                         items.map { it.name }.toTypedArray(), | ||||||
|                         remoteMediaClient.mediaQueue.indexOfItemWithId(remoteMediaClient.currentItem.itemId) |                         items.indexOfFirst { it.url == contentUrl } | ||||||
|                     ) { _, which -> |                     ) { _, which -> | ||||||
|                         val itemId = remoteMediaClient.mediaQueue.itemIds?.get(which) |                         val epData = holder.episodes[holder.currentEpisodeIndex] | ||||||
| 
 | 
 | ||||||
|                         itemId?.let { id -> |                         val mediaItem = getMediaInfo(epData, | ||||||
|                             remoteMediaClient.queueJumpToItem( |                             holder, | ||||||
|                                 id, |                             holder.currentLinks[which], | ||||||
|                                 remoteMediaClient.approximateStreamPosition, |                             remoteMediaClient.mediaInfo.customData) | ||||||
|                                 remoteMediaClient.mediaInfo.customData | 
 | ||||||
|                             ) |                         val startAt = remoteMediaClient.approximateStreamPosition | ||||||
|  | 
 | ||||||
|  |                         try { | ||||||
|  | 
 | ||||||
|  |                             val currentIdIndex = getItemIndex() ?: return@setSingleChoiceItems | ||||||
|  | 
 | ||||||
|  |                             val nextId = remoteMediaClient.mediaQueue.itemIds?.get(currentIdIndex + 1) | ||||||
|  | 
 | ||||||
|  |                             if (nextId != null) { | ||||||
|  |                                 remoteMediaClient.queueInsertAndPlayItem(MediaQueueItem.Builder(mediaItem).build(), | ||||||
|  |                                     nextId, | ||||||
|  |                                     startAt, | ||||||
|  |                                     JSONObject()) | ||||||
|  |                             } else { | ||||||
|  |                                 remoteMediaClient.load(mediaItem, true, startAt) | ||||||
|  |                             } | ||||||
|  |                         } catch (e: Exception) { | ||||||
|  |                             remoteMediaClient.load(mediaItem, true, startAt) | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|                         dialog.dismiss() |                         dialog.dismiss() | ||||||
|  | @ -77,26 +107,78 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private fun getItemIndex(): Int? { | ||||||
|  |         val index = remoteMediaClient?.mediaQueue?.itemIds?.indexOf(remoteMediaClient.currentItem.itemId) | ||||||
|  |         return if (index == null || index < 0) null else index | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private fun getCurrentMetaData(): MetadataHolder? { |     private fun getCurrentMetaData(): MetadataHolder? { | ||||||
|         return try { |         return try { | ||||||
|             val data = remoteMediaClient.mediaInfo.customData |             val data = remoteMediaClient?.mediaInfo?.customData?.toString() | ||||||
|             mapper.readValue<MetadataHolder>(data.toString()) |             data?.toKotlinObject() | ||||||
|         } catch (e: Exception) { |         } catch (e: Exception) { | ||||||
|             null |             null | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     var isLoadingMore = false | ||||||
|  | 
 | ||||||
|     override fun onMediaStatusUpdated() { |     override fun onMediaStatusUpdated() { | ||||||
|         super.onMediaStatusUpdated() |         super.onMediaStatusUpdated() | ||||||
|         view.visibility = |         val meta = getCurrentMetaData() | ||||||
|             if ((getCurrentMetaData()?.data?.size | 
 | ||||||
|  |         view.visibility = if ((meta?.currentLinks?.size | ||||||
|                 ?: 0) > 1 |                 ?: 0) > 1 | ||||||
|         ) VISIBLE else INVISIBLE |         ) VISIBLE else INVISIBLE | ||||||
|  |         try { | ||||||
|  |             if (meta != null && meta.episodes.size > meta.currentEpisodeIndex + 1) { | ||||||
|  | 
 | ||||||
|  |                 val currentIdIndex = getItemIndex() ?: return | ||||||
|  |                 val itemCount = remoteMediaClient?.mediaQueue?.itemCount | ||||||
|  | 
 | ||||||
|  |                 if (itemCount != null && itemCount - currentIdIndex == 1 && !isLoadingMore) { | ||||||
|  |                     isLoadingMore = true | ||||||
|  | 
 | ||||||
|  |                     main { | ||||||
|  |                         val index = meta.currentEpisodeIndex + 1 | ||||||
|  |                         val epData = meta.episodes[index] | ||||||
|  |                         val links = ArrayList<ExtractorLink>() | ||||||
|  | 
 | ||||||
|  |                         val res = safeApiCall { | ||||||
|  |                             getApiFromName(meta.apiName).loadLinks(epData.data, true) { | ||||||
|  |                                 for (i in links) { | ||||||
|  |                                     if (i.url == it.url) return@loadLinks | ||||||
|  |                                 } | ||||||
|  |                                 links.add(it) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         if (res is Resource.Success) { | ||||||
|  |                             val sorted = sortUrls(links) | ||||||
|  |                             if (sorted.isNotEmpty()) { | ||||||
|  |                                 val jsonCopy = meta.copy(currentLinks = sorted, currentEpisodeIndex = index) | ||||||
|  | 
 | ||||||
|  |                                 val done = withContext(Dispatchers.IO) { | ||||||
|  |                                     getMediaInfo(epData, | ||||||
|  |                                         meta, | ||||||
|  |                                         sorted.first(), | ||||||
|  |                                         JSONObject(mapper.writeValueAsString(jsonCopy))) | ||||||
|  |                                 } | ||||||
|  | 
 | ||||||
|  |                                 remoteMediaClient?.queueAppendItem(MediaQueueItem.Builder(done).build(), JSONObject()) | ||||||
|  |                                 isLoadingMore = false | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } catch (e: Exception) { | ||||||
|  |             println(e) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     override fun onSessionConnected(castSession: CastSession?) { |     override fun onSessionConnected(castSession: CastSession?) { | ||||||
|         super.onSessionConnected(castSession) |         super.onSessionConnected(castSession) | ||||||
|         remoteMediaClient.queueSetRepeatMode(REPEAT_MODE_REPEAT_SINGLE, JSONObject()) |         remoteMediaClient?.queueSetRepeatMode(REPEAT_MODE_REPEAT_OFF, JSONObject()) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -74,9 +74,11 @@ import com.lagradost.cloudstream3.UIHelper.requestLocalAudioFocus | ||||||
| import com.lagradost.cloudstream3.UIHelper.showSystemUI | import com.lagradost.cloudstream3.UIHelper.showSystemUI | ||||||
| import com.lagradost.cloudstream3.UIHelper.toPx | import com.lagradost.cloudstream3.UIHelper.toPx | ||||||
| import com.lagradost.cloudstream3.mvvm.Resource | import com.lagradost.cloudstream3.mvvm.Resource | ||||||
|  | import com.lagradost.cloudstream3.mvvm.observe | ||||||
| import com.lagradost.cloudstream3.mvvm.observeDirectly | import com.lagradost.cloudstream3.mvvm.observeDirectly | ||||||
| import com.lagradost.cloudstream3.ui.result.ResultEpisode | import com.lagradost.cloudstream3.ui.result.ResultEpisode | ||||||
| import com.lagradost.cloudstream3.ui.result.ResultViewModel | import com.lagradost.cloudstream3.ui.result.ResultViewModel | ||||||
|  | import com.lagradost.cloudstream3.utils.CastHelper.startCast | ||||||
| import com.lagradost.cloudstream3.utils.DataStore.getKey | import com.lagradost.cloudstream3.utils.DataStore.getKey | ||||||
| import com.lagradost.cloudstream3.utils.DataStore.setKey | import com.lagradost.cloudstream3.utils.DataStore.setKey | ||||||
| import com.lagradost.cloudstream3.utils.ExtractorLink | import com.lagradost.cloudstream3.utils.ExtractorLink | ||||||
|  | @ -142,6 +144,8 @@ class PlayerFragment : Fragment() { | ||||||
|     private val mapper = JsonMapper.builder().addModule(KotlinModule()) |     private val mapper = JsonMapper.builder().addModule(KotlinModule()) | ||||||
|         .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() |         .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() | ||||||
| 
 | 
 | ||||||
|  |     lateinit var apiName: String | ||||||
|  | 
 | ||||||
|     private var isFullscreen = false |     private var isFullscreen = false | ||||||
|     private var isPlayerPlaying = true |     private var isPlayerPlaying = true | ||||||
|     private lateinit var viewModel: ResultViewModel |     private lateinit var viewModel: ResultViewModel | ||||||
|  | @ -733,7 +737,17 @@ class PlayerFragment : Fragment() { | ||||||
|                         val epData = getEpisode() ?: return@addCastStateListener |                         val epData = getEpisode() ?: return@addCastStateListener | ||||||
| 
 | 
 | ||||||
|                         val index = links.indexOf(getCurrentUrl()) |                         val index = links.indexOf(getCurrentUrl()) | ||||||
|  |                         context?.startCast( | ||||||
|  |                             apiName, | ||||||
|  |                             currentHeaderName, | ||||||
|  |                             currentPoster, | ||||||
|  |                             epData.index, | ||||||
|  |                             episodes, | ||||||
|  |                             links, | ||||||
|  |                             index, | ||||||
|  |                             exoPlayer.currentPosition) | ||||||
| 
 | 
 | ||||||
|  |                         /* | ||||||
|                         val customData = |                         val customData = | ||||||
|                             links.map { JSONObject().put("name", it.name) } |                             links.map { JSONObject().put("name", it.name) } | ||||||
|                         val jsonArray = JSONArray() |                         val jsonArray = JSONArray() | ||||||
|  | @ -773,8 +787,9 @@ class PlayerFragment : Fragment() { | ||||||
|                             if (index > 0) index else 0, |                             if (index > 0) index else 0, | ||||||
|                             exoPlayer.currentPosition, |                             exoPlayer.currentPosition, | ||||||
|                             MediaStatus.REPEAT_MODE_REPEAT_SINGLE |                             MediaStatus.REPEAT_MODE_REPEAT_SINGLE | ||||||
|                         ) |                         )*/ | ||||||
|                         //  activity?.popCurrentPage(isInPlayer = true, isInExpandedView = false, isInResults = false) |                         //  activity?.popCurrentPage(isInPlayer = true, isInExpandedView = false, isInResults = false) | ||||||
|  |                         releasePlayer() | ||||||
|                         activity?.popCurrentPage() |                         activity?.popCurrentPage() | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  | @ -820,6 +835,10 @@ class PlayerFragment : Fragment() { | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         observe(viewModel.apiName) { | ||||||
|  |             apiName = it | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         overlay_loading_skip_button?.alpha = 0.5f |         overlay_loading_skip_button?.alpha = 0.5f | ||||||
|         observeDirectly(viewModel.allEpisodes) { _allEpisodes -> |         observeDirectly(viewModel.allEpisodes) { _allEpisodes -> | ||||||
|             allEpisodes = _allEpisodes |             allEpisodes = _allEpisodes | ||||||
|  |  | ||||||
|  | @ -20,11 +20,13 @@ import androidx.recyclerview.widget.RecyclerView | ||||||
| import com.bumptech.glide.Glide | import com.bumptech.glide.Glide | ||||||
| import com.bumptech.glide.load.model.GlideUrl | import com.bumptech.glide.load.model.GlideUrl | ||||||
| import com.bumptech.glide.request.RequestOptions.bitmapTransform | import com.bumptech.glide.request.RequestOptions.bitmapTransform | ||||||
|  | import com.fasterxml.jackson.annotation.JsonProperty | ||||||
| import com.google.android.exoplayer2.ext.cast.CastPlayer | import com.google.android.exoplayer2.ext.cast.CastPlayer | ||||||
| import com.google.android.exoplayer2.util.MimeTypes | import com.google.android.exoplayer2.util.MimeTypes | ||||||
| import com.google.android.gms.cast.MediaInfo | import com.google.android.gms.cast.MediaInfo | ||||||
| import com.google.android.gms.cast.MediaMetadata | import com.google.android.gms.cast.MediaMetadata | ||||||
| import com.google.android.gms.cast.MediaQueueItem | import com.google.android.gms.cast.MediaQueueItem | ||||||
|  | import com.google.android.gms.cast.MediaStatus.REPEAT_MODE_REPEAT_OFF | ||||||
| import com.google.android.gms.cast.MediaStatus.REPEAT_MODE_REPEAT_SINGLE | 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.CastButtonFactory | ||||||
| import com.google.android.gms.cast.framework.CastContext | import com.google.android.gms.cast.framework.CastContext | ||||||
|  | @ -38,6 +40,7 @@ import com.lagradost.cloudstream3.mvvm.Resource | ||||||
| import com.lagradost.cloudstream3.mvvm.observe | import com.lagradost.cloudstream3.mvvm.observe | ||||||
| import com.lagradost.cloudstream3.ui.player.PlayerData | import com.lagradost.cloudstream3.ui.player.PlayerData | ||||||
| import com.lagradost.cloudstream3.ui.player.PlayerFragment | import com.lagradost.cloudstream3.ui.player.PlayerFragment | ||||||
|  | import com.lagradost.cloudstream3.utils.CastHelper.startCast | ||||||
| import com.lagradost.cloudstream3.utils.ExtractorLink | import com.lagradost.cloudstream3.utils.ExtractorLink | ||||||
| import jp.wasabeef.glide.transformations.BlurTransformation | import jp.wasabeef.glide.transformations.BlurTransformation | ||||||
| import kotlinx.android.synthetic.main.fragment_result.* | import kotlinx.android.synthetic.main.fragment_result.* | ||||||
|  | @ -52,7 +55,7 @@ data class ResultEpisode( | ||||||
|     val poster: String?, |     val poster: String?, | ||||||
|     val episode: Int, |     val episode: Int, | ||||||
|     val season: Int?, |     val season: Int?, | ||||||
|     val data: Any, |     val data: String, | ||||||
|     val apiName: String, |     val apiName: String, | ||||||
|     val id: Int, |     val id: Int, | ||||||
|     val index: Int, |     val index: Int, | ||||||
|  | @ -80,6 +83,7 @@ class ResultFragment : Fragment() { | ||||||
|     private lateinit var viewModel: ResultViewModel |     private lateinit var viewModel: ResultViewModel | ||||||
|     private var allEpisodes: HashMap<Int, ArrayList<ExtractorLink>> = HashMap() |     private var allEpisodes: HashMap<Int, ArrayList<ExtractorLink>> = HashMap() | ||||||
|     var currentHeaderName: String? = null |     var currentHeaderName: String? = null | ||||||
|  |     var currentEpisodes: ArrayList<ResultEpisode>? = null | ||||||
| 
 | 
 | ||||||
|     override fun onCreateView( |     override fun onCreateView( | ||||||
|         inflater: LayoutInflater, |         inflater: LayoutInflater, | ||||||
|  | @ -142,28 +146,35 @@ class ResultFragment : Fragment() { | ||||||
|                 ArrayList(), |                 ArrayList(), | ||||||
|                 result_episodes, |                 result_episodes, | ||||||
|             ) { episodeClick -> |             ) { episodeClick -> | ||||||
|                 val id = episodeClick.data.id |                 //val id = episodeClick.data.id | ||||||
|                 val index = episodeClick.data.index |                 val index = episodeClick.data.index | ||||||
|                 val buildInPlayer = true |                 val buildInPlayer = true | ||||||
|                 when (episodeClick.action) { |                 when (episodeClick.action) { | ||||||
|                     ACTION_CHROME_CAST_EPISODE -> { |                     ACTION_CHROME_CAST_EPISODE -> { | ||||||
| 
 |  | ||||||
|                         /* |  | ||||||
|                         val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) |                         val builder = AlertDialog.Builder(requireContext(), R.style.AlertDialogCustom) | ||||||
|                         val customLayout = layoutInflater.inflate(R.layout.dialog_loading, null); |                         val customLayout = layoutInflater.inflate(R.layout.dialog_loading, null); | ||||||
|                         builder.setView(customLayout) |                         builder.setView(customLayout) | ||||||
| 
 | 
 | ||||||
|                         val dialog = builder.create()*/ |                         val dialog = builder.create() | ||||||
|                         //dialog.show() |                         dialog.show() | ||||||
|                         Toast.makeText(activity, "Loading links", Toast.LENGTH_SHORT).show() |                         Toast.makeText(activity, "Loading links", Toast.LENGTH_SHORT).show() | ||||||
| 
 | 
 | ||||||
|                         viewModel.loadEpisode(episodeClick.data, false) { data -> |                         viewModel.loadEpisode(episodeClick.data, true) { data -> | ||||||
|                             //  dialog.dismiss() |                             dialog.dismiss() | ||||||
|                             when (data) { |                             when (data) { | ||||||
|                                 is Resource.Failure -> { |                                 is Resource.Failure -> { | ||||||
|                                     Toast.makeText(activity, "Failed to load links", Toast.LENGTH_SHORT).show() |                                     Toast.makeText(activity, "Failed to load links", Toast.LENGTH_SHORT).show() | ||||||
|                                 } |                                 } | ||||||
|                                 is Resource.Success -> { |                                 is Resource.Success -> { | ||||||
|  |                                     val eps = currentEpisodes ?: return@loadEpisode | ||||||
|  |                                     context?.startCast( | ||||||
|  |                                         apiName ?: return@loadEpisode, | ||||||
|  |                                         currentHeaderName, | ||||||
|  |                                         currentPoster, | ||||||
|  |                                         episodeClick.data.index, | ||||||
|  |                                         eps, | ||||||
|  |                                         sortUrls(data.value)) | ||||||
|  |                                     /* | ||||||
|                                         val epData = episodeClick.data |                                         val epData = episodeClick.data | ||||||
|                                         val links = sortUrls(data.value) |                                         val links = sortUrls(data.value) | ||||||
| 
 | 
 | ||||||
|  | @ -200,12 +211,14 @@ class ResultFragment : Fragment() { | ||||||
|                                         }.toTypedArray() |                                         }.toTypedArray() | ||||||
| 
 | 
 | ||||||
|                                         val castPlayer = CastPlayer(castContext) |                                         val castPlayer = CastPlayer(castContext) | ||||||
|  | 
 | ||||||
|                                         castPlayer.loadItems( |                                         castPlayer.loadItems( | ||||||
|                                             mediaItems, |                                             mediaItems, | ||||||
|                                             0, |                                             0, | ||||||
|                                             epData.progress, |                                             epData.progress, | ||||||
|                                             REPEAT_MODE_REPEAT_SINGLE |                                             REPEAT_MODE_REPEAT_SINGLE | ||||||
|                                     ) |                                             //REPEAT_MODE_REPEAT_SINGLE | ||||||
|  |                                         )*/ | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|  | @ -244,6 +257,7 @@ class ResultFragment : Fragment() { | ||||||
|         observe(viewModel.episodes) { episodes -> |         observe(viewModel.episodes) { episodes -> | ||||||
|             if (result_episodes == null || result_episodes.adapter == null) return@observe |             if (result_episodes == null || result_episodes.adapter == null) return@observe | ||||||
|             result_episodes_text.text = "${episodes.size} Episode${if (episodes.size == 1) "" else "s"}" |             result_episodes_text.text = "${episodes.size} Episode${if (episodes.size == 1) "" else "s"}" | ||||||
|  |             currentEpisodes = episodes | ||||||
|             (result_episodes.adapter as EpisodeAdapter).cardList = episodes |             (result_episodes.adapter as EpisodeAdapter).cardList = episodes | ||||||
|             (result_episodes.adapter as EpisodeAdapter).notifyDataSetChanged() |             (result_episodes.adapter as EpisodeAdapter).notifyDataSetChanged() | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -82,7 +82,8 @@ class ResultViewModel : ViewModel() { | ||||||
|                                 d.apiName, |                                 d.apiName, | ||||||
|                                 (d.url).hashCode(), |                                 (d.url).hashCode(), | ||||||
|                                 0, |                                 0, | ||||||
|                                 0, 0))) |                                 0, 0, | ||||||
|  |                             ))) | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  | @ -99,6 +100,8 @@ class ResultViewModel : ViewModel() { | ||||||
|     val allEpisodes: LiveData<HashMap<Int, ArrayList<ExtractorLink>>> get() = _allEpisodes |     val allEpisodes: LiveData<HashMap<Int, ArrayList<ExtractorLink>>> get() = _allEpisodes | ||||||
| 
 | 
 | ||||||
|     private var _apiName: MutableLiveData<String> = MutableLiveData() |     private var _apiName: MutableLiveData<String> = MutableLiveData() | ||||||
|  |     val apiName: LiveData<String> get() = _apiName | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|     fun loadEpisode( |     fun loadEpisode( | ||||||
|         episode: ResultEpisode, |         episode: ResultEpisode, | ||||||
|  | @ -110,7 +113,7 @@ class ResultViewModel : ViewModel() { | ||||||
| 
 | 
 | ||||||
|     private fun loadEpisode( |     private fun loadEpisode( | ||||||
|         id: Int, |         id: Int, | ||||||
|         data: Any, |         data: String, | ||||||
|         isCasting: Boolean, |         isCasting: Boolean, | ||||||
|         callback: (Resource<ArrayList<ExtractorLink>>) -> Unit, |         callback: (Resource<ArrayList<ExtractorLink>>) -> Unit, | ||||||
|     ) = |     ) = | ||||||
|  | @ -124,7 +127,6 @@ class ResultViewModel : ViewModel() { | ||||||
|                     for (i in links) { |                     for (i in links) { | ||||||
|                         if (i.url == it.url) return@loadLinks |                         if (i.url == it.url) return@loadLinks | ||||||
|                     } |                     } | ||||||
|                     println("LINK ADDED::::: " + it.url) |  | ||||||
| 
 | 
 | ||||||
|                     links.add(it) |                     links.add(it) | ||||||
|                     _allEpisodes.value?.set(id, links) |                     _allEpisodes.value?.set(id, links) | ||||||
|  |  | ||||||
|  | @ -0,0 +1,95 @@ | ||||||
|  | package com.lagradost.cloudstream3.utils | ||||||
|  | 
 | ||||||
|  | import android.content.Context | ||||||
|  | import android.net.Uri | ||||||
|  | import com.fasterxml.jackson.databind.DeserializationFeature | ||||||
|  | import com.fasterxml.jackson.databind.json.JsonMapper | ||||||
|  | import com.fasterxml.jackson.module.kotlin.KotlinModule | ||||||
|  | import com.google.android.exoplayer2.ext.cast.CastPlayer | ||||||
|  | import com.google.android.exoplayer2.util.MimeTypes | ||||||
|  | import com.google.android.gms.cast.CastStatusCodes | ||||||
|  | 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_OFF | ||||||
|  | import com.google.android.gms.cast.framework.CastContext | ||||||
|  | import com.google.android.gms.cast.framework.media.RemoteMediaClient | ||||||
|  | import com.google.android.gms.common.api.PendingResult | ||||||
|  | import com.google.android.gms.common.images.WebImage | ||||||
|  | import com.lagradost.cloudstream3.ui.MetadataHolder | ||||||
|  | import com.lagradost.cloudstream3.ui.result.ResultEpisode | ||||||
|  | import com.lagradost.cloudstream3.utils.Coroutines.main | ||||||
|  | import kotlinx.coroutines.awaitAll | ||||||
|  | import org.json.JSONObject | ||||||
|  | import kotlin.concurrent.thread | ||||||
|  | 
 | ||||||
|  | object CastHelper { | ||||||
|  |     private val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule()) | ||||||
|  |         .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() | ||||||
|  | 
 | ||||||
|  |     fun getMediaInfo(epData: ResultEpisode, holder: MetadataHolder, link: ExtractorLink, data: JSONObject?): MediaInfo { | ||||||
|  |         val movieMetadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_MOVIE) | ||||||
|  |         movieMetadata.putString(MediaMetadata.KEY_SUBTITLE, | ||||||
|  |             (epData.name ?: "Episode ${epData.episode}") + " - ${link.name}") | ||||||
|  | 
 | ||||||
|  |         movieMetadata.putString(MediaMetadata.KEY_TITLE, holder.title) | ||||||
|  | 
 | ||||||
|  |         val srcPoster = epData.poster ?: holder.poster | ||||||
|  |         if (srcPoster != null) { | ||||||
|  |             movieMetadata.addImage(WebImage(Uri.parse(srcPoster))) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return MediaInfo.Builder(link.url) | ||||||
|  |             .setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) | ||||||
|  |             .setContentType(MimeTypes.VIDEO_UNKNOWN) | ||||||
|  |             .setCustomData(data) | ||||||
|  |             .setMetadata(movieMetadata) | ||||||
|  |             .build() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun awaitLinks(pending: PendingResult<RemoteMediaClient.MediaChannelResult>?) { | ||||||
|  |         if (pending == null) return | ||||||
|  |         thread { | ||||||
|  |             val res = pending.await() | ||||||
|  |             when (res.status?.statusCode) { | ||||||
|  |                 CastStatusCodes.FAILED -> { | ||||||
|  |                     println("FAILED WITH DATA: " + res.customData) | ||||||
|  |                 } | ||||||
|  |                 else -> { | ||||||
|  | 
 | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     fun Context.startCast( | ||||||
|  |         apiName: String, | ||||||
|  |         title: String?, | ||||||
|  |         poster: String?, | ||||||
|  |         currentEpisodeIndex: Int, | ||||||
|  |         episodes: List<ResultEpisode>, | ||||||
|  |         currentLinks: List<ExtractorLink>, | ||||||
|  |         startIndex: Int? = null, | ||||||
|  |         startTime: Long? = null, | ||||||
|  |     ) { | ||||||
|  |         if (episodes.isEmpty()) return | ||||||
|  | 
 | ||||||
|  |         val castContext = CastContext.getSharedInstance(this) | ||||||
|  | 
 | ||||||
|  |         val epData = episodes[currentEpisodeIndex] | ||||||
|  | 
 | ||||||
|  |         val holder = MetadataHolder(apiName, title, poster, currentEpisodeIndex, episodes, currentLinks) | ||||||
|  | 
 | ||||||
|  |         val mediaItem = | ||||||
|  |             getMediaInfo(epData, holder, currentLinks[startIndex ?: 0], JSONObject(mapper.writeValueAsString(holder))) | ||||||
|  | 
 | ||||||
|  |         val castPlayer = CastPlayer(castContext) | ||||||
|  | 
 | ||||||
|  |         castPlayer.repeatMode = REPEAT_MODE_REPEAT_OFF | ||||||
|  |         castPlayer.stop() | ||||||
|  |         awaitLinks(castPlayer.loadItem( | ||||||
|  |             MediaQueueItem.Builder(mediaItem).build(), | ||||||
|  |             startTime ?: 0, | ||||||
|  |         )) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -3,7 +3,7 @@ package com.lagradost.cloudstream3.utils | ||||||
| import com.lagradost.cloudstream3.utils.extractors.MixDrop | import com.lagradost.cloudstream3.utils.extractors.MixDrop | ||||||
| import com.lagradost.cloudstream3.utils.extractors.Mp4Upload | import com.lagradost.cloudstream3.utils.extractors.Mp4Upload | ||||||
| import com.lagradost.cloudstream3.utils.extractors.Shiro | import com.lagradost.cloudstream3.utils.extractors.Shiro | ||||||
| import com.lagradost.cloudstream3.cloudstream3.extractors.StreamTape | import com.lagradost.cloudstream3.utils.extractors.StreamTape | ||||||
| import com.lagradost.cloudstream3.utils.extractors.XStreamCdn | import com.lagradost.cloudstream3.utils.extractors.XStreamCdn | ||||||
| 
 | 
 | ||||||
| data class ExtractorLink( | data class ExtractorLink( | ||||||
|  | @ -19,8 +19,6 @@ fun ExtractorLink.getId() : Int { | ||||||
|     return url.hashCode() |     return url.hashCode() | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| enum class Qualities(var value: Int) { | enum class Qualities(var value: Int) { | ||||||
|     Unknown(0), |     Unknown(0), | ||||||
|     SD(-1), // 360p - 480p |     SD(-1), // 360p - 480p | ||||||
|  |  | ||||||
|  | @ -6,7 +6,7 @@ class MixDrop : ExtractorApi() { | ||||||
|     override val name: String = "MixDrop" |     override val name: String = "MixDrop" | ||||||
|     override val mainUrl: String = "https://mixdrop.co" |     override val mainUrl: String = "https://mixdrop.co" | ||||||
|     private val srcRegex = Regex("""wurl.*?=.*?"(.*?)";""") |     private val srcRegex = Regex("""wurl.*?=.*?"(.*?)";""") | ||||||
|     override val requiresReferer = true |     override val requiresReferer = false | ||||||
| 
 | 
 | ||||||
|     override fun getExtractorUrl(id: String): String { |     override fun getExtractorUrl(id: String): String { | ||||||
|         return "$mainUrl/e/$id" |         return "$mainUrl/e/$id" | ||||||
|  |  | ||||||
|  | @ -1,4 +1,4 @@ | ||||||
| package com.lagradost.cloudstream3.cloudstream3.extractors | package com.lagradost.cloudstream3.utils.extractors | ||||||
| 
 | 
 | ||||||
| import com.lagradost.cloudstream3.utils.ExtractorApi | import com.lagradost.cloudstream3.utils.ExtractorApi | ||||||
| import com.lagradost.cloudstream3.utils.ExtractorLink | import com.lagradost.cloudstream3.utils.ExtractorLink | ||||||
|  | @ -7,9 +7,9 @@ import com.lagradost.cloudstream3.utils.Qualities | ||||||
| class StreamTape : ExtractorApi() { | class StreamTape : ExtractorApi() { | ||||||
|     override val name: String = "StreamTape" |     override val name: String = "StreamTape" | ||||||
|     override val mainUrl: String = "https://streamtape.com" |     override val mainUrl: String = "https://streamtape.com" | ||||||
|     override val requiresReferer = true |     override val requiresReferer = false | ||||||
| 
 | 
 | ||||||
|     // Because they add concatenation to fuck up scrapers |     // Because they add concatenation to fuck up scrapers, DON'T LET LAG CODE ANYTHING | ||||||
|     private val linkRegex = |     private val linkRegex = | ||||||
|         Regex("""(i(|" \+ ')d(|" \+ ')=.*?&(|" \+ ')e(|" \+ ')x(|" \+ ')p(|" \+ ')i(|" \+ ')r(|" \+ ')e(|" \+ ')s(|" \+ ')=.*?&(|" \+ ')i(|" \+ ')p(|" \+ ')=.*?&(|" \+ ')t(|" \+ ')o(|" \+ ')k(|" \+ ')e(|" \+ ')n(|" \+ ')=.*)'""") |         Regex("""(i(|" \+ ')d(|" \+ ')=.*?&(|" \+ ')e(|" \+ ')x(|" \+ ')p(|" \+ ')i(|" \+ ')r(|" \+ ')e(|" \+ ')s(|" \+ ')=.*?&(|" \+ ')i(|" \+ ')p(|" \+ ')=.*?&(|" \+ ')t(|" \+ ')o(|" \+ ')k(|" \+ ')e(|" \+ ')n(|" \+ ')=.*)'""") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ class Vidstream { | ||||||
|     private fun getExtractorUrl(id: String): String { |     private fun getExtractorUrl(id: String): String { | ||||||
|         return "$mainUrl/streaming.php?id=$id" |         return "$mainUrl/streaming.php?id=$id" | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     private val normalApis = arrayListOf(Shiro(), MultiQuality()) |     private val normalApis = arrayListOf(Shiro(), MultiQuality()) | ||||||
| 
 | 
 | ||||||
|     // https://gogo-stream.com/streaming.php?id=MTE3NDg5 |     // https://gogo-stream.com/streaming.php?id=MTE3NDg5 | ||||||
|  |  | ||||||
|  | @ -10,7 +10,7 @@ import com.lagradost.cloudstream3.utils.Qualities | ||||||
| class XStreamCdn : ExtractorApi() { | class XStreamCdn : ExtractorApi() { | ||||||
|     override val name: String = "XStreamCdn" |     override val name: String = "XStreamCdn" | ||||||
|     override val mainUrl: String = "https://fcdn.stream" |     override val mainUrl: String = "https://fcdn.stream" | ||||||
|     override val requiresReferer = true |     override val requiresReferer = false | ||||||
| 
 | 
 | ||||||
|     private data class ResponseData( |     private data class ResponseData( | ||||||
|         @JsonProperty("file") val file: String, |         @JsonProperty("file") val file: String, | ||||||
|  |  | ||||||
|  | @ -2,15 +2,17 @@ | ||||||
| <LinearLayout android:orientation="vertical" xmlns:android="http://schemas.android.com/apk/res/android" | <LinearLayout android:orientation="vertical" xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|               android:layout_width="match_parent" |               android:layout_width="match_parent" | ||||||
|               android:layout_height="match_parent"> |               android:layout_height="match_parent"> | ||||||
| 
 | <!-- | ||||||
|     <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" |     <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" | ||||||
|               android:text="Loading..." android:textColor="?attr/textColor" android:textSize="20sp" |               android:text="Loading..." android:textColor="?attr/textColor" android:textSize="20sp" | ||||||
|               android:textStyle="bold" android:layout_margin="10dp"/> |               android:textStyle="bold" android:layout_margin="10dp"/>--> | ||||||
|     <androidx.core.widget.ContentLoadingProgressBar |     <!--            style="@android:style/Widget.Material.ProgressBar.Horizontal" | ||||||
|  | --> | ||||||
|  |     <ProgressBar | ||||||
|             android:layout_margin="10dp" |             android:layout_margin="10dp" | ||||||
|             style="@android:style/Widget.Material.ProgressBar.Horizontal" | 
 | ||||||
|             android:indeterminate="true" |             android:layout_gravity="center" | ||||||
|             android:layout_width="match_parent" |             android:layout_width="60dp" | ||||||
|             android:layout_height="wrap_content" |             android:layout_height="60dp" | ||||||
|     /> |     /> | ||||||
| </LinearLayout> | </LinearLayout> | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue