Merge branch 'recloudstream:master' into githubAccount

This commit is contained in:
antonydp 2022-11-02 14:37:12 +01:00 committed by GitHub
commit 3f4d28b7df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 2131 additions and 1650 deletions

76
.github/workflows/build_to_archive.yml vendored Normal file
View file

@ -0,0 +1,76 @@
name: Archive build
on:
push:
branches: [ master ]
paths-ignore:
- '*.md'
- '*.json'
- '**/wcokey.txt'
workflow_dispatch:
concurrency:
group: "Archive-build"
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Generate access token
id: generate_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/secrets"
- name: Generate access token (archive)
id: generate_archive_token
uses: tibdex/github-app-token@v1
with:
app_id: ${{ secrets.GH_APP_ID }}
private_key: ${{ secrets.GH_APP_KEY }}
repository: "recloudstream/cloudstream-archive"
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v2
with:
java-version: '11'
distribution: 'adopt'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Fetch keystore
id: fetch_keystore
run: |
TMP_KEYSTORE_FILE_PATH="${RUNNER_TEMP}"/keystore
mkdir -p "${TMP_KEYSTORE_FILE_PATH}"
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore.jks"
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
KEY_PWD="$(cat keystore_password.txt)"
echo "::add-mask::${KEY_PWD}"
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
- name: Run Gradle
run: |
./gradlew assemblePrerelease
env:
SIGNING_KEY_ALIAS: "key0"
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
- uses: actions/checkout@v3
with:
repository: "recloudstream/cloudstream-archive"
token: ${{ steps.generate_archive_token.outputs.token }}
path: "archive"
- name: Move build
run: |
cp app/build/outputs/apk/prerelease/release/*.apk "archive/$(git rev-parse --short HEAD).apk"
- name: Push archive
run: |
cd $GITHUB_WORKSPACE/archive
git config --local user.email "actions@github.com"
git config --local user.name "GitHub Actions"
git add .
git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit
git push --force

View file

@ -155,10 +155,10 @@ dependencies {
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
// Exoplayer
implementation("com.google.android.exoplayer:exoplayer:2.16.1")
implementation("com.google.android.exoplayer:extension-cast:2.16.1")
implementation("com.google.android.exoplayer:extension-mediasession:2.16.1")
implementation("com.google.android.exoplayer:extension-okhttp:2.16.1")
implementation("com.google.android.exoplayer:exoplayer:2.18.1")
implementation("com.google.android.exoplayer:extension-cast:2.18.1")
implementation("com.google.android.exoplayer:extension-mediasession:2.18.1")
implementation("com.google.android.exoplayer:extension-okhttp:2.18.1")
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
@ -176,7 +176,9 @@ dependencies {
implementation("com.jaredrummler:colorpicker:1.1.0")
//run JS
implementation("org.mozilla:rhino:1.7.14")
// do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not
// available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown
implementation("org.mozilla:rhino:1.7.13")
// TorrentStream
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
@ -211,9 +213,9 @@ dependencies {
// slow af yt
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
// newpipe yt
implementation("com.github.TeamNewPipe:NewPipeExtractor:dev-SNAPSHOT")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L190
implementation("com.github.TeamNewPipe:NewPipeExtractor:9ffdd0948b2ecd82655f5ff2a3e127b2b7695d5b")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
// Library/extensions searching with Levenshtein distance
implementation("me.xdrop:fuzzywuzzy:1.4.0")

View file

@ -18,7 +18,6 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SubtitleHelper
import okhttp3.Interceptor
import java.text.SimpleDateFormat
import java.util.*
@ -31,6 +30,12 @@ const val USER_AGENT =
val mapper = JsonMapper.builder().addModule(KotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
/**
* Defines the constant for the all languages preference, if this is set then it is
* the equivalent of all languages being set
**/
const val AllLanguagesName = "universal"
object APIHolder {
val unixTime: Long
get() = System.currentTimeMillis() / 1000L
@ -159,7 +164,8 @@ object APIHolder {
val hashSet = HashSet<String>()
val activeLangs = getApiProviderLangSettings()
hashSet.addAll(apis.filter { activeLangs.contains(it.lang) }.map { it.name })
val hasUniversal = activeLangs.contains(AllLanguagesName)
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }.map { it.name })
/*val set = settingsManager.getStringSet(
this.getString(R.string.search_providers_list_key),
@ -193,26 +199,17 @@ object APIHolder {
return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet()
}
/**
* Gets all the activated provider languages
* Used to obey the preference provider_lang_key
* but it turned out too complicated and unnecessary with extensions.
**/
fun Context.getApiProviderLangSettings(): HashSet<String> {
val langs = apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) }
return langs.toHashSet()
// val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
// val hashSet = HashSet<String>()
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
val hashSet = hashSetOf(AllLanguagesName) // def is all languages
// hashSet.add("en") // def is only en
// val list = settingsManager.getStringSet(
// this.getString(R.string.provider_lang_key),
// hashSet.toMutableSet()
// )
//
// if (list.isNullOrEmpty()) return hashSet
// return list.toHashSet()
val list = settingsManager.getStringSet(
this.getString(R.string.provider_lang_key),
hashSet
)
if (list.isNullOrEmpty()) return hashSet
return list.toHashSet()
}
fun Context.getApiTypeSettings(): HashSet<TvType> {
@ -254,7 +251,8 @@ object APIHolder {
null
} ?: default
val langs = this.getApiProviderLangSettings()
val allApis = apis.filter { langs.contains(it.lang) }
val hasUniversal = langs.contains(AllLanguagesName)
val allApis = apis.filter { hasUniversal || langs.contains(it.lang) }
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
return if (currentPrefMedia.isEmpty()) {
allApis

View file

@ -0,0 +1,72 @@
package com.lagradost.cloudstream3.extractors
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.SubtitleFile
import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
open class Jeniusplay : ExtractorApi() {
override val name = "Jeniusplay"
override val mainUrl = "https://jeniusplay.com"
override val requiresReferer = true
override suspend fun getUrl(
url: String,
referer: String?,
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
val document = app.get(url, referer = "$mainUrl/").document
val hash = url.split("/").last().substringAfter("data=")
val m3uLink = app.post(
url = "$mainUrl/player/index.php?data=$hash&do=getVideo",
data = mapOf("hash" to hash, "r" to "$referer"),
referer = url,
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
).parsed<ResponseSource>().videoSource
M3u8Helper.generateM3u8(
this.name,
m3uLink,
url,
).forEach(callback)
document.select("script").map { script ->
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
val subData =
getAndUnpack(script.data()).substringAfter("\"tracks\":[").substringBefore("],")
tryParseJson<List<Tracks>>("[$subData]")?.map { subtitle ->
subtitleCallback.invoke(
SubtitleFile(
getLanguage(subtitle.label ?: ""),
subtitle.file
)
)
}
}
}
}
private fun getLanguage(str: String): String {
return when {
str.contains("indonesia", true) || str
.contains("bahasa", true) -> "Indonesian"
else -> str
}
}
data class ResponseSource(
@JsonProperty("hls") val hls: Boolean,
@JsonProperty("videoSource") val videoSource: String,
@JsonProperty("securedLink") val securedLink: String?,
)
data class Tracks(
@JsonProperty("kind") val kind: String?,
@JsonProperty("file") val file: String,
@JsonProperty("label") val label: String?,
)
}

View file

@ -6,7 +6,11 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.M3u8Helper
class Moviehab : ExtractorApi() {
class MoviehabNet : Moviehab() {
override var mainUrl = "https://play.moviehab.net"
}
open class Moviehab : ExtractorApi() {
override var name = "Moviehab"
override var mainUrl = "https://play.moviehab.com"
override val requiresReferer = false

View file

@ -46,6 +46,7 @@ open class YoutubeExtractor : ExtractorApi() {
subtitleCallback: (SubtitleFile) -> Unit,
callback: (ExtractorLink) -> Unit
) {
println("TRYING TO ExTRACT: $url")
if (ytVideos[url].isNullOrEmpty()) {
val link =
YoutubeStreamLinkHandlerFactory.getInstance().fromUrl(

View file

@ -1,40 +1,39 @@
package com.lagradost.cloudstream3.plugins
import android.app.*
import dalvik.system.PathClassLoader
import com.google.gson.Gson
import android.content.Context
import android.content.res.AssetManager
import android.content.res.Resources
import android.os.Environment
import android.widget.Toast
import android.content.Context
import android.os.Build
import android.os.Environment
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.fasterxml.jackson.annotation.JsonProperty
import com.google.gson.Gson
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
import com.lagradost.cloudstream3.mvvm.debugPrint
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.RepositoryManager.ONLINE_PLUGINS_FOLDER
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.plugins.RepositoryManager.downloadPluginToFile
import com.lagradost.cloudstream3.plugins.RepositoryManager.getRepoPlugins
import com.lagradost.cloudstream3.ui.settings.extensions.REPOSITORIES_KEY
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.ExtractorApi
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
import com.lagradost.cloudstream3.utils.extractorApis
import dalvik.system.PathClassLoader
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.File
@ -217,7 +216,7 @@ object PluginManager {
* 3. If outdated download and load the plugin
* 4. Else load the plugin normally
**/
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) = ioSafe {
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) {
// Load all plugins as fast as possible!
loadAllOnlinePlugins(activity)
@ -227,7 +226,7 @@ object PluginManager {
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
?: emptyArray()) + PREBUILT_REPOSITORIES
val onlinePlugins = urls.toList().amap {
val onlinePlugins = urls.toList().apmap {
getRepoPlugins(it.url)?.toList() ?: emptyList()
}.flatten().distinctBy { it.second.url }
@ -248,7 +247,7 @@ object PluginManager {
val updatedPlugins = mutableListOf<String>()
outdatedPlugins.amap { pluginData ->
outdatedPlugins.apmap { pluginData ->
if (pluginData.isDisabled) {
//updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name))
unloadPlugin(pluginData.savedData.filePath)
@ -279,9 +278,9 @@ object PluginManager {
/**
* Use updateAllOnlinePluginsAndLoadThem
* */
fun loadAllOnlinePlugins(activity: Activity) = ioSafe {
fun loadAllOnlinePlugins(activity: Activity) {
// Load all plugins as fast as possible!
(getPluginsOnline()).toList().amap { pluginData ->
(getPluginsOnline()).toList().apmap { pluginData ->
loadPlugin(
activity,
File(pluginData.filePath),
@ -290,7 +289,7 @@ object PluginManager {
}
}
fun loadAllLocalPlugins(activity: Activity) = ioSafe {
fun loadAllLocalPlugins(activity: Activity) {
val dir = File(LOCAL_PLUGINS_PATH)
removeKey(PLUGINS_KEY_LOCAL)
@ -298,7 +297,7 @@ object PluginManager {
val res = dir.mkdirs()
if (!res) {
Log.w(TAG, "Failed to create local directories")
return@ioSafe
return
}
}

View file

@ -70,6 +70,28 @@ object RepositoryManager {
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
}
suspend fun parseRepoUrl(url: String): String? {
val fixedUrl = url.trim()
return if (fixedUrl.contains("^https?://".toRegex())) {
fixedUrl
} else if (fixedUrl.contains("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex())) {
fixedUrl.replace("^(cloudstreamrepo://)|(https://cs\\.repo/\\??)".toRegex(), "").let {
return@let if (!it.contains("^https?://".toRegex()))
"https://${it}"
else fixedUrl
}
} else if (fixedUrl.matches("^[a-zA-Z0-9!_-]+$".toRegex())) {
suspendSafeApiCall {
app.get("https://l.cloudstream.cf/${fixedUrl}").let {
return@let if (it.isSuccessful && !it.url.startsWith("https://cutt.ly/branded-domains")) it.url
else app.get("https://cutt.ly/${fixedUrl}").let let2@{ it2 ->
return@let2 if (it2.isSuccessful) it2.url else null
}
}
}
} else null
}
suspend fun parseRepository(url: String): Repository? {
return suspendSafeApiCall {
// Take manifestVersion and such into account later

View file

@ -150,7 +150,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
} else {
ChromecastSubtitlesFragment.getCurrentSavedStyle().apply {
val font = TextTrackStyle()
font.fontFamily = fontFamily ?: "Google Sans"
font.setFontFamily(fontFamily ?: "Google Sans")
fontGenericFamily?.let {
font.fontGenericFamily = it
}
@ -183,7 +183,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl
?: remoteMediaClient?.currentItem?.media?.contentId)
val sortingMethods = items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" }.toTypedArray()
val sortingMethods =
items.map { "${it.name} ${Qualities.getStringByInt(it.quality)}" }
.toTypedArray()
val sotringIndex = items.indexOfFirst { it.url == contentUrl }
val arrayAdapter =
@ -279,7 +281,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
val currentPosition = remoteMediaClient?.approximateStreamPosition
if (currentDuration != null && currentPosition != null)
DataStoreHelper.setViewPos(epData.id, currentPosition, currentDuration)
} catch (t : Throwable) {
} catch (t: Throwable) {
logError(t)
}
@ -358,10 +360,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
}
}
override fun onSessionConnected(castSession: CastSession?) {
castSession?.let {
super.onSessionConnected(it)
}
override fun onSessionConnected(castSession: CastSession) {
super.onSessionConnected(castSession)
remoteMediaClient?.queueSetRepeatMode(REPEAT_MODE_REPEAT_OFF, JSONObject())
}
}

View file

@ -23,6 +23,9 @@ import androidx.fragment.app.activityViewModels
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
@ -346,7 +349,9 @@ class HomeFragment : Fragment() {
builder.setContentView(R.layout.home_select_mainpage)
builder.show()
builder.let { dialog ->
val isMultiLang = getApiProviderLangSettings().size > 1
val isMultiLang = getApiProviderLangSettings().let { set ->
set.size > 1 || set.contains(AllLanguagesName)
}
//dialog.window?.setGravity(Gravity.BOTTOM)
var currentApiName = selectedApiName
@ -548,7 +553,9 @@ class HomeFragment : Fragment() {
is Resource.Success -> {
home_preview?.isVisible = true
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.apply {
setItems(preview.value)
if (!setItems(preview.value.second, preview.value.first)) {
home_preview_viewpager?.setCurrentItem(0, false)
}
// home_preview_viewpager?.setCurrentItem(1000, false)
}
@ -557,6 +564,10 @@ class HomeFragment : Fragment() {
//}
}
else -> {
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.setItems(
listOf(),
false
)
home_preview?.isVisible = false
context?.fixPaddingStatusbar(home_watch_holder)
}
@ -571,13 +582,26 @@ class HomeFragment : Fragment() {
}
home_preview_viewpager?.apply {
setPageTransformer(false, HomeScrollTransformer())
adapter = HomeScrollAdapter { load ->
load.apply {
home_preview_tags?.text = tags?.joinToString("") ?: ""
home_preview_tags?.isGone = tags.isNullOrEmpty()
home_preview_image?.setImage(posterUrl, posterHeaders)
home_preview_title?.text = name
setPageTransformer(HomeScrollTransformer())
val callback: OnPageChangeCallback = object : OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.apply {
if (position >= itemCount - 1 && hasMoreItems) {
hasMoreItems = false // dont make two requests
homeViewModel.loadMoreHomeScrollResponses()
}
getItem(position)
?.apply {
home_preview_title_holder?.let { parent ->
TransitionManager.beginDelayedTransition(parent, ChangeBounds())
}
// home_preview_tags?.text = tags?.joinToString(" • ") ?: ""
// home_preview_tags?.isGone = tags.isNullOrEmpty()
// home_preview_image?.setImage(posterUrl, posterHeaders)
// home_preview_title?.text = name
home_preview_play?.setOnClickListener {
activity?.loadResult(url, apiName, START_ACTION_RESUME_LATEST)
//activity.loadSearchResult(url, START_ACTION_RESUME_LATEST)
@ -587,7 +611,7 @@ class HomeFragment : Fragment() {
//activity.loadSearchResult(random)
}
// very ugly code, but I dont care
val watchType = DataStoreHelper.getResultWatchState(load.getId())
val watchType = DataStoreHelper.getResultWatchState(this.getId())
home_preview_bookmark?.setText(watchType.stringRes)
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
null,
@ -597,29 +621,37 @@ class HomeFragment : Fragment() {
)
home_preview_bookmark?.setOnClickListener { fab ->
activity?.showBottomDialog(
WatchType.values().map { fab.context.getString(it.stringRes) }
WatchType.values()
.map { fab.context.getString(it.stringRes) }
.toList(),
DataStoreHelper.getResultWatchState(load.getId()).ordinal,
DataStoreHelper.getResultWatchState(this.getId()).ordinal,
fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false,
{}) {
val newValue = WatchType.values()[it]
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
null,
getDrawable(home_preview_bookmark.context, newValue.iconRes),
getDrawable(
home_preview_bookmark.context,
newValue.iconRes
),
null,
null
)
home_preview_bookmark?.setText(newValue.stringRes)
updateWatchStatus(load, newValue)
updateWatchStatus(this, newValue)
reloadStored()
}
}
}
}
}
}
}
registerOnPageChangeCallback(callback)
adapter = HomeScrollAdapter()
}
observe(homeViewModel.apiName) { apiName ->
currentApiName = apiName

View file

@ -1,60 +1,89 @@
package com.lagradost.cloudstream3.ui.home
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.viewpager.widget.PagerAdapter
import androidx.core.view.isGone
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import kotlinx.android.synthetic.main.home_scroll_view.view.*
class HomeScrollAdapter(private val onPrimaryCallback: (LoadResponse) -> Unit) : PagerAdapter() {
private var items: List<LoadResponse> = listOf()
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items: MutableList<LoadResponse> = mutableListOf()
var hasMoreItems: Boolean = false
fun setItems(newItems: List<LoadResponse>) {
items = newItems
notifyDataSetChanged()
fun getItem(position: Int) : LoadResponse? {
return items.getOrNull(position)
}
override fun getCount(): Int {
return Int.MAX_VALUE//items.size
fun setItems(newItems: List<LoadResponse>, hasNext: Boolean): Boolean {
val isSame = newItems.firstOrNull()?.url == items.firstOrNull()?.url
hasMoreItems = hasNext
val diffResult = DiffUtil.calculateDiff(
HomeScrollDiffCallback(this.items, newItems)
)
items.clear()
items.addAll(newItems)
diffResult.dispatchUpdatesTo(this)
return isSame
}
override fun getItemPosition(`object`: Any): Int {
return POSITION_NONE//super.getItemPosition(`object`)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return CardViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.home_scroll_view, parent, false),
)
}
private fun getItemAtPosition(idx: Int): LoadResponse {
return items[idx % items.size]
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is CardViewHolder -> {
holder.bind(items[position])
}
}
}
override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) {
super.setPrimaryItem(container, position, `object`)
onPrimaryCallback.invoke(getItemAtPosition(position))
class CardViewHolder
constructor(
itemView: View,
) :
RecyclerView.ViewHolder(itemView) {
fun bind(card: LoadResponse) {
card.apply {
itemView.home_scroll_preview_tags?.text = tags?.joinToString("") ?: ""
itemView.home_scroll_preview_tags?.isGone = tags.isNullOrEmpty()
itemView.home_scroll_preview?.setImage(posterUrl, posterHeaders)
itemView.home_scroll_preview_title?.text = name
}
}
}
override fun instantiateItem(container: ViewGroup, position: Int): Any {
val image = ImageView(container.context)
val item = getItemAtPosition(position)
image.scaleType = ImageView.ScaleType.CENTER_CROP
image.setImage(item.posterUrl ?: item.backgroundPosterUrl, item.posterHeaders)
class HomeScrollDiffCallback(
private val oldList: List<LoadResponse>,
private val newList: List<LoadResponse>
) :
DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition].url == newList[newItemPosition].url
// val itemView: View = mLayoutInflater.inflate(R.layout.pager_item, container, false)
override fun getOldListSize() = oldList.size
// val imageView: ImageView = itemView.findViewById<View>(R.id.imageView) as ImageView
// imageView.setImageResource(mResources.get(position))
override fun getNewListSize() = newList.size
container.addView(image)
return image
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
oldList[oldItemPosition] == newList[newItemPosition]
}
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
container.removeView(`object` as View)
}
override fun isViewFromObject(view: View, `object`: Any): Boolean {
return view === `object`
override fun getItemCount(): Int {
return items.size
}
}

View file

@ -1,13 +1,23 @@
package com.lagradost.cloudstream3.ui.home
import android.view.View
import androidx.viewpager.widget.ViewPager
import androidx.viewpager2.widget.ViewPager2
class HomeScrollTransformer : ViewPager.PageTransformer {
class HomeScrollTransformer : ViewPager2.PageTransformer {
override fun transformPage(page: View, position: Float) {
//page.translationX = -position * page.width / 2.0f
//val params = RecyclerView.LayoutParams(
// RecyclerView.LayoutParams.MATCH_PARENT,
// 0
//)
//page.layoutParams = params
//progressBar?.layoutParams = params
val padding = (-position * page.width / 2).toInt()
page.setPadding(
maxOf(0, (-position * page.width / 2).toInt()), 0,
maxOf(0, (position * page.width / 2).toInt()), 0
padding, 0,
-padding, 0
)
}
}

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
@ -12,17 +13,12 @@ import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainAPI
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.mvvm.*
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
@ -35,7 +31,6 @@ import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.collections.set
@ -49,6 +44,8 @@ class HomeViewModel : ViewModel() {
private val _randomItems = MutableLiveData<List<SearchResponse>?>(null)
val randomItems: LiveData<List<SearchResponse>?> = _randomItems
private var currentShuffledList: List<SearchResponse> = listOf()
private fun autoloadRepo(): APIRepository {
return APIRepository(apis.first { it.hasMainPage })
}
@ -61,9 +58,12 @@ class HomeViewModel : ViewModel() {
val bookmarks: LiveData<Pair<Boolean, List<SearchResponse>>> = _bookmarks
private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
private val _preview = MutableLiveData<Resource<List<LoadResponse>>>()
private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>()
private val previewResponses = mutableListOf<LoadResponse>()
private val previewResponsesAdded = mutableSetOf<String>()
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
val preview: LiveData<Resource<List<LoadResponse>>> = _preview
val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview
fun loadResumeWatching() = viewModelScope.launchSafe {
val resumeWatching = withContext(Dispatchers.IO) {
@ -212,6 +212,40 @@ class HomeViewModel : ViewModel() {
expandAndReturn(name)
}
// returns the amount of items added and modifies current
private suspend fun updatePreviewResponses(
current: MutableList<LoadResponse>,
alreadyAdded: MutableSet<String>,
shuffledList: List<SearchResponse>,
size: Int
): Int {
var count = 0
val addItems = arrayListOf<SearchResponse>()
for (searchResponse in shuffledList) {
if (!alreadyAdded.contains(searchResponse.url)) {
addItems.add(searchResponse)
previewResponsesAdded.add(searchResponse.url)
if (++count >= size) {
break
}
}
}
val add = addItems.amap { searchResponse ->
repo?.load(searchResponse.url)
}.mapNotNull { if (it != null && it is Resource.Success) it.value else null }
current.addAll(add)
return add.size
}
private var addJob: Job? = null
fun loadMoreHomeScrollResponses() {
addJob = ioSafe {
updatePreviewResponses(previewResponses, previewResponsesAdded, currentShuffledList, 1)
_preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses))
}
}
private fun load(api: MainAPI?) = ioSafe {
repo = if (api != null) {
@ -226,6 +260,7 @@ class HomeViewModel : ViewModel() {
if (repo?.hasMainPage == true) {
_page.postValue(Resource.Loading())
_preview.postValue(Resource.Loading())
addJob?.cancel()
when (val data = repo?.getMainPage(1, null)) {
is Resource.Success -> {
@ -241,64 +276,10 @@ class HomeViewModel : ViewModel() {
}
val items = data.value.mapNotNull { it?.items }.flatten()
val responses = ioWork {
items.flatMap { it.list }.shuffled().take(6).map { searchResponse ->
async { repo?.load(searchResponse.url) }
}.map { it.await() }.mapNotNull { if (it != null && it is Resource.Success) it.value else null } }
//.amap { searchResponse ->
// repo?.load(searchResponse.url)
///}
//.map { searchResponse ->
// async { repo?.load(searchResponse.url) }
// }.map { it.await() }
if (responses.isEmpty()) {
_preview.postValue(
Resource.Failure(
false,
null,
null,
"No homepage responses"
)
)
} else {
_preview.postValue(Resource.Success(responses))
}
/*
items.randomOrNull()?.list?.randomOrNull()?.url?.let { url ->
// backup request in case first fails
var first = repo?.load(url)
if(first == null ||first is Resource.Failure) {
first = repo?.load(items.random().list.random().url)
}
first?.let {
_preview.postValue(it)
} ?: run {
_preview.postValue(
Resource.Failure(
false,
null,
null,
"No repo found, this should never happen"
)
)
}
} ?: run {
_preview.postValue(
Resource.Failure(
false,
null,
null,
"No homepage items"
)
)
}*/
_page.postValue(Resource.Success(expandable))
previewResponses.clear()
previewResponsesAdded.clear()
//val home = data.value
if (items.isNotEmpty()) {
@ -313,9 +294,30 @@ class HomeViewModel : ViewModel() {
context?.filterSearchResultByFilmQuality(currentList.shuffled())
?: currentList.shuffled()
updatePreviewResponses(
previewResponses,
previewResponsesAdded,
randomItems,
3
)
_randomItems.postValue(randomItems)
currentShuffledList = randomItems
}
}
if (previewResponses.isEmpty()) {
_preview.postValue(
Resource.Failure(
false,
null,
null,
"No homepage responses"
)
)
} else {
_preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses))
}
_page.postValue(Resource.Success(expandable))
} catch (e: Exception) {
_randomItems.postValue(emptyList())
logError(e)

View file

@ -15,6 +15,7 @@ import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
import com.google.android.exoplayer2.source.*
import com.google.android.exoplayer2.text.TextRenderer
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
import com.google.android.exoplayer2.trackselection.TrackSelectionOverride
import com.google.android.exoplayer2.trackselection.TrackSelector
import com.google.android.exoplayer2.ui.SubtitleView
import com.google.android.exoplayer2.upstream.*
@ -218,7 +219,43 @@ class CS3IPlayer : IPlayer {
var currentSubtitles: SubtitleData? = null
override fun setMaxVideoSize(width: Int, height: Int) {
private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? {
if (id == null) return null
// This beast of an expression does:
// 1. Filter all audio tracks
// 2. Get all formats in said audio tacks
// 3. Gets all ids of the formats
// 4. Filters to find the first audio track with the same id as the audio track we are looking for
// 5. Returns the media group and the index of the audio track in the group
return this.firstNotNullOfOrNull { group ->
(0 until group.mediaTrackGroup.length).map {
group.getTrackFormat(it) to it
}.firstOrNull { it.first.id == id }
?.let { group.mediaTrackGroup to it.second }
}
}
override fun setMaxVideoSize(width: Int, height: Int, id: String?) {
if (id != null) {
val videoTrack =
exoPlayer?.currentTracks?.groups?.filter { it.type == TRACK_TYPE_VIDEO }
?.getTrack(id)
if (videoTrack != null) {
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setOverrideForType(
TrackSelectionOverride(
videoTrack.first,
videoTrack.second
)
)
?.build()
?: return
return
}
}
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setMaxVideoSize(width, height)
@ -226,8 +263,29 @@ class CS3IPlayer : IPlayer {
?: return
}
override fun setPreferredAudioTrack(trackLanguage: String?) {
override fun setPreferredAudioTrack(trackLanguage: String?, id: String?) {
preferredAudioTrackLanguage = trackLanguage
if (id != null) {
val audioTrack =
exoPlayer?.currentTracks?.groups?.filter { it.type == TRACK_TYPE_AUDIO }
?.getTrack(id)
if (audioTrack != null) {
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setOverrideForType(
TrackSelectionOverride(
audioTrack.first,
audioTrack.second
)
)
?.build()
?: return
return
}
}
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
?.buildUpon()
?.setPreferredAudioLanguage(trackLanguage)
@ -239,11 +297,11 @@ class CS3IPlayer : IPlayer {
/**
* Gets all supported formats in a list
* */
private fun List<TracksInfo.TrackGroupInfo>.getFormats(): List<Format> {
private fun List<Tracks.Group>.getFormats(): List<Pair<Format, Int>> {
return this.map {
(0 until it.trackGroup.length).mapNotNull { i ->
(0 until it.mediaTrackGroup.length).mapNotNull { i ->
if (it.isSupported)
it.trackGroup.getFormat(i) // to it.isSelected
it.mediaTrackGroup.getFormat(i) to i
else null
}
}.flatten()
@ -270,11 +328,12 @@ class CS3IPlayer : IPlayer {
}
override fun getVideoTracks(): CurrentTracks {
val allTracks = exoPlayer?.currentTracksInfo?.trackGroupInfos ?: emptyList()
val videoTracks = allTracks.filter { it.trackType == TRACK_TYPE_VIDEO }.getFormats()
.map { it.toVideoTrack() }
val audioTracks = allTracks.filter { it.trackType == TRACK_TYPE_AUDIO }.getFormats()
.map { it.toAudioTrack() }
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
.getFormats()
.map { it.first.toVideoTrack() }
val audioTracks = allTracks.filter { it.type == TRACK_TYPE_AUDIO }.getFormats()
.map { it.first.toAudioTrack() }
return CurrentTracks(
exoPlayer?.videoFormat?.toVideoTrack(),
@ -611,7 +670,12 @@ class CS3IPlayer : IPlayer {
} else it
}.toTypedArray()
}
.setTrackSelector(trackSelector ?: getTrackSelector(context, maxVideoHeight))
.setTrackSelector(
trackSelector ?: getTrackSelector(
context,
maxVideoHeight
)
)
.setLoadControl(
DefaultLoadControl.Builder()
.setTargetBufferBytes(
@ -781,10 +845,7 @@ class CS3IPlayer : IPlayer {
isPlaying = exo.isPlaying
}
exoPlayer?.addListener(object : Player.Listener {
/**
* Records the current used subtitle/track. Needed as exoplayer seems to have loose track language selection.
* */
override fun onTracksInfoChanged(tracksInfo: TracksInfo) {
override fun onTracksChanged(tracks: Tracks) {
fun Format.isSubtitle(): Boolean {
return this.sampleMimeType?.contains("video/") == false &&
this.sampleMimeType?.contains("audio/") == false
@ -792,17 +853,17 @@ class CS3IPlayer : IPlayer {
normalSafeApiCall {
exoPlayerSelectedTracks =
tracksInfo.trackGroupInfos.mapNotNull {
val format = it.trackGroup.getFormat(0)
tracks.groups.mapNotNull {
val format = it.mediaTrackGroup.getFormat(0)
if (format.isSubtitle())
format.language?.let { lang -> lang to it.isSelected }
else null
}
val exoPlayerReportedTracks = tracksInfo.trackGroupInfos.mapNotNull {
val exoPlayerReportedTracks = tracks.groups.mapNotNull {
// Filter out unsupported tracks
if (it.isSupported)
it.trackGroup.getFormat(0)
it.mediaTrackGroup.getFormat(0)
else
null
}.mapNotNull {
@ -827,7 +888,6 @@ class CS3IPlayer : IPlayer {
onTracksInfoChanged?.invoke()
subtitlesUpdates?.invoke()
}
super.onTracksInfoChanged(tracksInfo)
}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {

View file

@ -796,15 +796,16 @@ class GeneratorPlayer : FullScreenPlayer() {
}
tracksDialog.apply_btt?.setOnClickListener {
val currentTrack = currentAudioTracks.getOrNull(audioIndexStart)
player.setPreferredAudioTrack(
currentAudioTracks.getOrNull(audioIndexStart)?.language
currentTrack?.language, currentTrack?.id
)
val currentVideo = currentVideoTracks.getOrNull(videoIndex)
val width = currentVideo?.width ?: NO_VALUE
val height = currentVideo?.height ?: NO_VALUE
if (width != NO_VALUE && height != NO_VALUE) {
player.setMaxVideoSize(width, height)
player.setMaxVideoSize(width, height, currentVideo?.id)
}
tracksDialog.dismissSafe(activity)

View file

@ -161,9 +161,9 @@ interface IPlayer {
fun getVideoTracks(): CurrentTracks
/** If no parameters are set it'll default to no set size */
fun setMaxVideoSize(width: Int = Int.MAX_VALUE, height: Int = Int.MAX_VALUE)
/** If no parameters are set it'll default to no set size, Specifying the id allows for track overrides to force the player to pick the quality. */
fun setMaxVideoSize(width: Int = Int.MAX_VALUE, height: Int = Int.MAX_VALUE, id: String? = null)
/** If no trackLanguage is set it'll default to first track */
fun setPreferredAudioTrack(trackLanguage: String?)
/** If no trackLanguage is set it'll default to first track. Specifying the id allows for track overrides as the language can be identical. */
fun setPreferredAudioTrack(trackLanguage: String?, id: String? = null)
}

View file

@ -0,0 +1,423 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lagradost.cloudstream3.ui.player;
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
import static com.google.android.exoplayer2.util.Assertions.checkState;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.os.Handler;
import android.os.Handler.Callback;
import android.os.Looper;
import android.os.Message;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import com.google.android.exoplayer2.BaseRenderer;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.FormatHolder;
import com.google.android.exoplayer2.RendererCapabilities;
import com.google.android.exoplayer2.source.SampleStream.ReadDataResult;
import com.google.android.exoplayer2.text.Cue;
import com.google.android.exoplayer2.text.CueGroup;
import com.google.android.exoplayer2.text.Subtitle;
import com.google.android.exoplayer2.text.SubtitleDecoder;
import com.google.android.exoplayer2.text.SubtitleDecoderException;
import com.google.android.exoplayer2.text.SubtitleDecoderFactory;
import com.google.android.exoplayer2.text.SubtitleInputBuffer;
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
import com.google.android.exoplayer2.text.TextOutput;
import com.google.android.exoplayer2.util.Log;
import com.google.android.exoplayer2.util.MimeTypes;
import com.google.android.exoplayer2.util.Util;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.List;
// DO NOT CONVERT TO KOTLIN AUTOMATICALLY, IT FUCKS UP AND DOES NOT DISPLAY SUBS FOR SOME REASON
/**
* A renderer for text.
*
* <p>{@link Subtitle}s are decoded from sample data using {@link SubtitleDecoder} instances
* obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s
* is delegated to a {@link TextOutput}.
*/
public class NonFinalTextRenderer extends BaseRenderer implements Callback {
private static final String TAG = "TextRenderer";
/**
* @param trackType The track type that the renderer handles. One of the {@link C} {@code
* TRACK_TYPE_*} constants.
* @param outputHandler
*/
public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) {
super(trackType);
this.outputHandler = outputHandler;
}
@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(TYPE_USE)
@IntDef({
REPLACEMENT_STATE_NONE,
REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,
REPLACEMENT_STATE_WAIT_END_OF_STREAM
})
private @interface ReplacementState {}
/** The decoder does not need to be replaced. */
private static final int REPLACEMENT_STATE_NONE = 0;
/**
* The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing
* decoder. We need to do so in order to ensure that it outputs any remaining buffers before we
* release it.
*/
private static final int REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1;
/**
* The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder.
* We're waiting for the decoder to output an end of stream signal to indicate that it has output
* any remaining buffers before we release it.
*/
private static final int REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2;
private static final int MSG_UPDATE_OUTPUT = 0;
@Nullable private final Handler outputHandler;
private TextOutput output = null;
private SubtitleDecoderFactory decoderFactory = null;
private FormatHolder formatHolder = null;
private boolean inputStreamEnded;
private boolean outputStreamEnded;
private boolean waitingForKeyFrame;
private @ReplacementState int decoderReplacementState;
@Nullable private Format streamFormat;
@Nullable private SubtitleDecoder decoder;
@Nullable private SubtitleInputBuffer nextInputBuffer;
@Nullable private SubtitleOutputBuffer subtitle;
@Nullable private SubtitleOutputBuffer nextSubtitle;
private int nextSubtitleEventIndex;
private long finalStreamEndPositionUs;
/**
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using {@link
* android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
* directly on the player's internal rendering thread.
*/
public NonFinalTextRenderer(TextOutput output, @Nullable Looper outputLooper) {
this(output, outputLooper, SubtitleDecoderFactory.DEFAULT);
}
/**
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using {@link
* android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
* directly on the player's internal rendering thread.
* @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances.
*/
public NonFinalTextRenderer(
TextOutput output, @Nullable Looper outputLooper, SubtitleDecoderFactory decoderFactory) {
super(C.TRACK_TYPE_TEXT);
this.output = checkNotNull(output);
this.outputHandler =
outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
this.decoderFactory = decoderFactory;
formatHolder = new FormatHolder();
finalStreamEndPositionUs = C.TIME_UNSET;
}
@Override
public String getName() {
return TAG;
}
@Override
public @Capabilities int supportsFormat(Format format) {
if (decoderFactory.supportsFormat(format)) {
return RendererCapabilities.create(
format.cryptoType == C.CRYPTO_TYPE_NONE ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_DRM);
} else if (MimeTypes.isText(format.sampleMimeType)) {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);
} else {
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
}
}
/**
* Sets the position at which to stop rendering the current stream.
*
* <p>Must be called after {@link #setCurrentStreamFinal()}.
*
* @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to
* render until the end of the current stream.
*/
// TODO(internal b/181312195): Remove this when it's no longer needed once subtitles are decoded
// on the loading side of SampleQueue.
public void setFinalStreamEndPositionUs(long streamEndPositionUs) {
checkState(isCurrentStreamFinal());
this.finalStreamEndPositionUs = streamEndPositionUs;
}
@Override
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
streamFormat = formats[0];
if (decoder != null) {
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;
} else {
initDecoder();
}
}
@Override
protected void onPositionReset(long positionUs, boolean joining) {
clearOutput();
inputStreamEnded = false;
outputStreamEnded = false;
finalStreamEndPositionUs = C.TIME_UNSET;
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
replaceDecoder();
} else {
releaseBuffers();
checkNotNull(decoder).flush();
}
}
@Override
public void render(long positionUs, long elapsedRealtimeUs) {
if (isCurrentStreamFinal()
&& finalStreamEndPositionUs != C.TIME_UNSET
&& positionUs >= finalStreamEndPositionUs) {
releaseBuffers();
outputStreamEnded = true;
}
if (outputStreamEnded) {
return;
}
if (nextSubtitle == null) {
checkNotNull(decoder).setPositionUs(positionUs);
try {
nextSubtitle = checkNotNull(decoder).dequeueOutputBuffer();
} catch (SubtitleDecoderException e) {
handleDecoderError(e);
return;
}
}
if (getState() != STATE_STARTED) {
return;
}
boolean textRendererNeedsUpdate = false;
if (subtitle != null) {
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
// advance to the next event.
long subtitleNextEventTimeUs = getNextEventTime();
while (subtitleNextEventTimeUs <= positionUs) {
nextSubtitleEventIndex++;
subtitleNextEventTimeUs = getNextEventTime();
textRendererNeedsUpdate = true;
}
}
if (nextSubtitle != null) {
SubtitleOutputBuffer nextSubtitle = this.nextSubtitle;
if (nextSubtitle.isEndOfStream()) {
if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) {
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
replaceDecoder();
} else {
releaseBuffers();
outputStreamEnded = true;
}
}
} else if (nextSubtitle.timeUs <= positionUs) {
// Advance to the next subtitle. Sync the next event index and trigger an update.
if (subtitle != null) {
subtitle.release();
}
nextSubtitleEventIndex = nextSubtitle.getNextEventTimeIndex(positionUs);
subtitle = nextSubtitle;
this.nextSubtitle = null;
textRendererNeedsUpdate = true;
}
}
if (textRendererNeedsUpdate) {
// If textRendererNeedsUpdate then subtitle must be non-null.
checkNotNull(subtitle);
// textRendererNeedsUpdate is set and we're playing. Update the renderer.
updateOutput(subtitle.getCues(positionUs));
}
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
return;
}
try {
while (!inputStreamEnded) {
@Nullable SubtitleInputBuffer nextInputBuffer = this.nextInputBuffer;
if (nextInputBuffer == null) {
nextInputBuffer = checkNotNull(decoder).dequeueInputBuffer();
if (nextInputBuffer == null) {
return;
}
this.nextInputBuffer = nextInputBuffer;
}
if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {
nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
checkNotNull(decoder).queueInputBuffer(nextInputBuffer);
this.nextInputBuffer = null;
decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM;
return;
}
// Try and read the next subtitle from the source.
@ReadDataResult int result = readSource(formatHolder, nextInputBuffer, /* readFlags= */ 0);
if (result == C.RESULT_BUFFER_READ) {
if (nextInputBuffer.isEndOfStream()) {
inputStreamEnded = true;
waitingForKeyFrame = false;
} else {
@Nullable Format format = formatHolder.format;
if (format == null) {
// We haven't received a format yet.
return;
}
nextInputBuffer.subsampleOffsetUs = format.subsampleOffsetUs;
nextInputBuffer.flip();
waitingForKeyFrame &= !nextInputBuffer.isKeyFrame();
}
if (!waitingForKeyFrame) {
checkNotNull(decoder).queueInputBuffer(nextInputBuffer);
this.nextInputBuffer = null;
}
} else if (result == C.RESULT_NOTHING_READ) {
return;
}
}
} catch (SubtitleDecoderException e) {
handleDecoderError(e);
}
}
@Override
protected void onDisabled() {
streamFormat = null;
finalStreamEndPositionUs = C.TIME_UNSET;
clearOutput();
releaseDecoder();
}
@Override
public boolean isEnded() {
return outputStreamEnded;
}
@Override
public boolean isReady() {
// Don't block playback whilst subtitles are loading.
// Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
return true;
}
private void releaseBuffers() {
nextInputBuffer = null;
nextSubtitleEventIndex = C.INDEX_UNSET;
if (subtitle != null) {
subtitle.release();
subtitle = null;
}
if (nextSubtitle != null) {
nextSubtitle.release();
nextSubtitle = null;
}
}
private void releaseDecoder() {
releaseBuffers();
checkNotNull(decoder).release();
decoder = null;
decoderReplacementState = REPLACEMENT_STATE_NONE;
}
private void initDecoder() {
waitingForKeyFrame = true;
decoder = decoderFactory.createDecoder(checkNotNull(streamFormat));
}
private void replaceDecoder() {
releaseDecoder();
initDecoder();
}
private long getNextEventTime() {
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
return Long.MAX_VALUE;
}
checkNotNull(subtitle);
return nextSubtitleEventIndex >= subtitle.getEventTimeCount()
? Long.MAX_VALUE
: subtitle.getEventTime(nextSubtitleEventIndex);
}
private void updateOutput(List<Cue> cues) {
if (outputHandler != null) {
outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget();
} else {
invokeUpdateOutputInternal(cues);
}
}
private void clearOutput() {
updateOutput(Collections.emptyList());
}
@SuppressWarnings("unchecked")
@Override
public boolean handleMessage(Message msg) {
switch (msg.what) {
case MSG_UPDATE_OUTPUT:
invokeUpdateOutputInternal((List<Cue>) msg.obj);
return true;
default:
throw new IllegalStateException();
}
}
private void invokeUpdateOutputInternal(List<Cue> cues) {
output.onCues(cues);
output.onCues(new CueGroup(cues));
}
/**
* Called when {@link #decoder} throws an exception, so it can be logged and playback can
* continue.
*
* <p>Logs {@code e} and resets state to allow decoding the next sample.
*/
private void handleDecoderError(SubtitleDecoderException e) {
Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e);
clearOutput();
replaceDecoder();
}
}

View file

@ -1,382 +0,0 @@
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lagradost.cloudstream3.ui.player
import android.os.Handler
import android.os.Looper
import android.os.Message
import androidx.annotation.IntDef
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.source.SampleStream.ReadDataResult
import com.google.android.exoplayer2.text.*
import com.google.android.exoplayer2.text.Cue.DIMEN_UNSET
import com.google.android.exoplayer2.text.Cue.LINE_TYPE_NUMBER
import com.google.android.exoplayer2.util.Assertions
import com.google.android.exoplayer2.util.Log
import com.google.android.exoplayer2.util.MimeTypes
import com.google.android.exoplayer2.util.Util
/**
* A renderer for text.
*
*
* [Subtitle]s are decoded from sample data using [SubtitleDecoder] instances
* obtained from a [SubtitleDecoderFactory]. The actual rendering of the subtitle [Cue]s
* is delegated to a [TextOutput].
*/
open class NonFinalTextRenderer @JvmOverloads constructor(
output: TextOutput?,
outputLooper: Looper?,
private val decoderFactory: SubtitleDecoderFactory = SubtitleDecoderFactory.DEFAULT
) :
BaseRenderer(C.TRACK_TYPE_TEXT), Handler.Callback {
@MustBeDocumented
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
@IntDef(
REPLACEMENT_STATE_NONE,
REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,
REPLACEMENT_STATE_WAIT_END_OF_STREAM
)
private annotation class ReplacementState
private val outputHandler: Handler? = if (outputLooper == null) null else Util.createHandler(
outputLooper, /* callback= */
this
)
private val output: TextOutput = Assertions.checkNotNull(output)
private val formatHold: FormatHolder = FormatHolder()
private var inputStreamEnded = false
private var outputStreamEnded = false
private var waitingForKeyFrame = false
@ReplacementState
private var decoderReplacementState = 0
private var streamFormat: Format? = null
private var decoder: SubtitleDecoder? = null
private var nextInputBuffer: SubtitleInputBuffer? = null
private var subtitle: SubtitleOutputBuffer? = null
private var nextSubtitle: SubtitleOutputBuffer? = null
private var nextSubtitleEventIndex = 0
private var finalStreamEndPositionUs: Long
override fun getName(): String {
return TAG
}
@RendererCapabilities.Capabilities
override fun supportsFormat(format: Format): Int {
return if (decoderFactory.supportsFormat(format)) {
RendererCapabilities.create(
if (format.cryptoType == C.CRYPTO_TYPE_NONE) C.FORMAT_HANDLED else C.FORMAT_UNSUPPORTED_DRM
)
} else if (MimeTypes.isText(format.sampleMimeType)) {
RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE)
} else {
RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE)
}
}
/**
* Sets the position at which to stop rendering the current stream.
*
*
* Must be called after [.setCurrentStreamFinal].
*
* @param streamEndPositionUs The position to stop rendering at or [C.LENGTH_UNSET] to
* render until the end of the current stream.
*/
override fun onStreamChanged(formats: Array<Format>, startPositionUs: Long, offsetUs: Long) {
streamFormat = formats[0]
if (decoder != null) {
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM
} else {
initDecoder()
}
}
override fun onPositionReset(positionUs: Long, joining: Boolean) {
clearOutput()
inputStreamEnded = false
outputStreamEnded = false
finalStreamEndPositionUs = C.TIME_UNSET
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
replaceDecoder()
} else {
releaseBuffers()
Assertions.checkNotNull(decoder).flush()
}
}
override fun render(positionUs: Long, elapsedRealtimeUs: Long) {
if (isCurrentStreamFinal
&& finalStreamEndPositionUs != C.TIME_UNSET && positionUs >= finalStreamEndPositionUs
) {
releaseBuffers()
outputStreamEnded = true
}
if (outputStreamEnded) {
return
}
if (nextSubtitle == null) {
Assertions.checkNotNull(decoder).setPositionUs(positionUs)
nextSubtitle = try {
Assertions.checkNotNull(decoder).dequeueOutputBuffer()
} catch (e: SubtitleDecoderException) {
handleDecoderError(e)
return
}
}
if (state != STATE_STARTED) {
return
}
var textRendererNeedsUpdate = false
if (subtitle != null) {
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
// advance to the next event.
var subtitleNextEventTimeUs = nextEventTime
while (subtitleNextEventTimeUs <= positionUs) {
nextSubtitleEventIndex++
subtitleNextEventTimeUs = nextEventTime
textRendererNeedsUpdate = true
}
}
if (nextSubtitle != null) {
val nextSubtitle = nextSubtitle
if (nextSubtitle!!.isEndOfStream) {
if (!textRendererNeedsUpdate && nextEventTime == Long.MAX_VALUE) {
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
replaceDecoder()
} else {
releaseBuffers()
outputStreamEnded = true
}
}
} else if (nextSubtitle.timeUs <= positionUs) {
// Advance to the next subtitle. Sync the next event index and trigger an update.
if (subtitle != null) {
subtitle!!.release()
}
nextSubtitleEventIndex = nextSubtitle.getNextEventTimeIndex(positionUs)
subtitle = nextSubtitle
this.nextSubtitle = null
textRendererNeedsUpdate = true
}
}
if (textRendererNeedsUpdate) {
// If textRendererNeedsUpdate then subtitle must be non-null.
Assertions.checkNotNull(subtitle)
// textRendererNeedsUpdate is set and we're playing. Update the renderer.
updateOutput(subtitle!!.getCues(positionUs))
}
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
return
}
try {
while (!inputStreamEnded) {
var nextInputBuffer = nextInputBuffer
if (nextInputBuffer == null) {
nextInputBuffer = Assertions.checkNotNull(decoder).dequeueInputBuffer()
if (nextInputBuffer == null) {
return
}
this.nextInputBuffer = nextInputBuffer
}
if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {
nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM)
Assertions.checkNotNull(decoder).queueInputBuffer(nextInputBuffer)
this.nextInputBuffer = null
decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM
return
}
// Try and read the next subtitle from the source.
@ReadDataResult val result =
readSource(formatHold, nextInputBuffer, /* readFlags= */0)
if (result == C.RESULT_BUFFER_READ) {
if (nextInputBuffer.isEndOfStream) {
inputStreamEnded = true
waitingForKeyFrame = false
} else {
val format = formatHold.format
?: // We haven't received a format yet.
return
nextInputBuffer.subsampleOffsetUs = format.subsampleOffsetUs
nextInputBuffer.flip()
waitingForKeyFrame = waitingForKeyFrame and !nextInputBuffer.isKeyFrame
}
if (!waitingForKeyFrame) {
Assertions.checkNotNull(decoder).queueInputBuffer(nextInputBuffer)
this.nextInputBuffer = null
}
} else if (result == C.RESULT_NOTHING_READ) {
return
}
}
} catch (e: SubtitleDecoderException) {
handleDecoderError(e)
}
}
override fun onDisabled() {
streamFormat = null
finalStreamEndPositionUs = C.TIME_UNSET
clearOutput()
releaseDecoder()
}
override fun isEnded(): Boolean {
return outputStreamEnded
}
override fun isReady(): Boolean {
// Don't block playback whilst subtitles are loading.
// Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
return true
}
private fun releaseBuffers() {
nextInputBuffer = null
nextSubtitleEventIndex = C.INDEX_UNSET
if (subtitle != null) {
subtitle!!.release()
subtitle = null
}
if (nextSubtitle != null) {
nextSubtitle!!.release()
nextSubtitle = null
}
}
private fun releaseDecoder() {
releaseBuffers()
Assertions.checkNotNull(decoder).release()
decoder = null
decoderReplacementState = REPLACEMENT_STATE_NONE
}
private fun initDecoder() {
waitingForKeyFrame = true
decoder = decoderFactory.createDecoder(Assertions.checkNotNull(streamFormat))
}
private fun replaceDecoder() {
releaseDecoder()
initDecoder()
}
private val nextEventTime: Long
get() {
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
return Long.MAX_VALUE
}
Assertions.checkNotNull(subtitle)
return if (nextSubtitleEventIndex >= subtitle!!.eventTimeCount) Long.MAX_VALUE else subtitle!!.getEventTime(
nextSubtitleEventIndex
)
}
private fun updateOutput(cues: List<Cue>) {
if (outputHandler != null) {
outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget()
} else {
invokeUpdateOutputInternal(cues)
}
}
private fun clearOutput() {
updateOutput(emptyList())
}
override fun handleMessage(msg: Message): Boolean {
return when (msg.what) {
MSG_UPDATE_OUTPUT -> {
invokeUpdateOutputInternal(msg.obj as List<Cue>)
true
}
else -> throw IllegalStateException()
}
}
private fun invokeUpdateOutputInternal(cues: List<Cue>) {
output.onCues(cues.map { cue ->
val builder = cue.buildUpon()
// See https://github.com/google/ExoPlayer/issues/7934
// SubripDecoder texts tend to be DIMEN_UNSET which pushes up the
// subs unlike WEBVTT which creates an inconsistency
if (cue.line == DIMEN_UNSET)
builder.setLine(-1f, LINE_TYPE_NUMBER)
// this fixes https://github.com/LagradOst/CloudStream-3/issues/717
builder.setSize(DIMEN_UNSET).build()
})
}
/**
* Called when [.decoder] throws an exception, so it can be logged and playback can
* continue.
*
*
* Logs `e` and resets state to allow decoding the next sample.
*/
private fun handleDecoderError(e: SubtitleDecoderException) {
Log.e(
TAG,
"Subtitle decoding failed. streamFormat=$streamFormat", e
)
clearOutput()
replaceDecoder()
}
companion object {
private const val TAG = "TextRenderer"
/** The decoder does not need to be replaced. */
private const val REPLACEMENT_STATE_NONE = 0
/**
* The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing
* decoder. We need to do so in order to ensure that it outputs any remaining buffers before we
* release it.
*/
private const val REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1
/**
* The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder.
* We're waiting for the decoder to output an end of stream signal to indicate that it has output
* any remaining buffers before we release it.
*/
private const val REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2
private const val MSG_UPDATE_OUTPUT = 0
}
/**
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using [ ][android.app.Activity.getMainLooper]. Null may be passed if the output should be called
* directly on the player's internal rendering thread.
* @param decoderFactory A factory from which to obtain [SubtitleDecoder] instances.
*/
/**
* @param output The output.
* @param outputLooper The looper associated with the thread on which the output should be called.
* If the output makes use of standard Android UI components, then this should normally be the
* looper associated with the application's main thread, which can be obtained using [ ][android.app.Activity.getMainLooper]. Null may be passed if the output should be called
* directly on the player's internal rendering thread.
*/
init {
finalStreamEndPositionUs = C.TIME_UNSET
}
}

View file

@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import com.discord.panels.OverlappingPanelsLayout
import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipDrawable
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
@ -96,11 +97,10 @@ import kotlinx.android.synthetic.main.fragment_result.result_vpn
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_result_tv.*
import kotlinx.android.synthetic.main.result_sync.*
import kotlinx.android.synthetic.main.trailer_custom_layout.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import com.google.android.material.chip.ChipDrawable
const val START_ACTION_RESUME_LATEST = 1
const val START_ACTION_LOAD_EP = 2
@ -839,6 +839,8 @@ open class ResultFragment : ResultTrailerPlayer() {
result_next_airing.setText(d.nextAiringEpisode)
result_next_airing_time.setText(d.nextAiringDate)
result_poster.setImage(d.posterImage)
result_poster_background.setImage(d.posterBackgroundImage)
//result_trailer_thumbnail.setImage(d.posterBackgroundImage, fadeIn = false)
if (d.posterImage != null && !isTrueTvSettings())
result_poster_holder?.setOnClickListener {

View file

@ -5,6 +5,9 @@ import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.view.animation.DecelerateInterpolator
import android.widget.Toast
import androidx.core.view.isGone
import androidx.core.view.isVisible
@ -20,27 +23,33 @@ import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.mvvm.Some
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result.result_cast_items
import kotlinx.android.synthetic.main.fragment_result.result_episodes_text
import kotlinx.android.synthetic.main.fragment_result.result_resume_parent
import kotlinx.android.synthetic.main.fragment_result.result_scroll
import kotlinx.android.synthetic.main.fragment_result.result_smallscreen_holder
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_result_swipe.result_back
import kotlinx.android.synthetic.main.fragment_result_tv.*
import kotlinx.android.synthetic.main.fragment_trailer.*
import kotlinx.android.synthetic.main.result_recommendations.*
import kotlinx.android.synthetic.main.result_recommendations.result_recommendations
import kotlinx.android.synthetic.main.trailer_custom_layout.*
class ResultFragmentPhone : ResultFragment() {
var currentTrailers: List<ExtractorLink> = emptyList()
var currentTrailerIndex = 0
@ -84,8 +93,36 @@ class ResultFragmentPhone : ResultFragment() {
} ?: run {
false
}
//result_trailer_thumbnail?.setImageBitmap(result_poster_background?.drawable?.toBitmap())
result_trailer_loading?.isVisible = isSuccess
result_smallscreen_holder?.isVisible = !isSuccess && !isFullScreenPlayer
val turnVis = !isSuccess && !isFullScreenPlayer
result_smallscreen_holder?.isVisible = turnVis
result_poster_background_holder?.apply {
val fadeIn: Animation = AlphaAnimation(alpha, if (turnVis) 1.0f else 0.0f).apply {
interpolator = DecelerateInterpolator()
duration = 200
fillAfter = true
}
clearAnimation()
startAnimation(fadeIn)
}
//player_view?.apply {
//alpha = 0.0f
//ObjectAnimator.ofFloat(player_view, "alpha", 1f).apply {
// duration = 200
// start()
//}
//val fadeIn: Animation = AlphaAnimation(0.0f, 1f).apply {
// interpolator = DecelerateInterpolator()
// duration = 2000
// fillAfter = true
//}
//startAnimation(fadeIn)
// }
// We don't want the trailer to be focusable if it's not visible
result_smallscreen_holder?.descendantFocusability = if (isSuccess) {
@ -129,7 +166,7 @@ class ResultFragmentPhone : ResultFragment() {
down.nextFocusUpId = upper.id
}
var selectSeason : String? = null
var selectSeason: String? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val apiName = arguments?.getString(API_NAME_BUNDLE) ?: return
@ -301,7 +338,8 @@ class ResultFragmentPhone : ResultFragment() {
observe(viewModel.selectedSeason) { text ->
result_season_button.setText(text)
selectSeason = (if (text is Some.Success) text.value else null)?.asStringNull(result_season_button?.context)
selectSeason =
(if (text is Some.Success) text.value else null)?.asStringNull(result_season_button?.context)
// If the season button is visible the result season button will be next focus down
if (result_season_button?.isVisible == true)
if (result_resume_parent?.isVisible == true)
@ -378,12 +416,16 @@ class ResultFragmentPhone : ResultFragment() {
r to (text?.asStringNull(ctx) ?: return@mapNotNull null)
}
activity?.showDialog(names.map { it.second },names.indexOfFirst { it.second == selectSeason },"",false,{}) { itemId->
activity?.showDialog(
names.map { it.second },
names.indexOfFirst { it.second == selectSeason },
"",
false,
{}) { itemId ->
viewModel.changeSeason(names[itemId].first)
}
//view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) ->
// index to name
//}) {

View file

@ -13,7 +13,9 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.utils.IOnBackPressed
import kotlinx.android.synthetic.main.fragment_result.*
import kotlinx.android.synthetic.main.fragment_result.result_smallscreen_holder
import kotlinx.android.synthetic.main.fragment_result_swipe.*
import kotlinx.android.synthetic.main.fragment_result_tv.*
import kotlinx.android.synthetic.main.fragment_trailer.*
import kotlinx.android.synthetic.main.trailer_custom_layout.*

View file

@ -88,6 +88,7 @@ data class ResultData(
var syncData: Map<String, String>,
val posterImage: UiImage?,
val posterBackgroundImage: UiImage?,
val plotText: UiText,
val apiName: UiText,
val ratingText: UiText?,
@ -170,6 +171,9 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
posterImage = img(
posterUrl, posterHeaders
) ?: img(R.drawable.default_cover),
posterBackgroundImage = img(
backgroundPosterUrl ?: posterUrl, posterHeaders
) ?: img(R.drawable.default_cover),
titleText = txt(name),
url = url,
tags = tags ?: emptyList(),

View file

@ -70,9 +70,9 @@ sealed class UiImage {
data class Drawable(@DrawableRes val resId: Int) : UiImage()
}
fun ImageView?.setImage(value: UiImage?) {
fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) {
when (value) {
is UiImage.Image -> setImageImage(value)
is UiImage.Image -> setImageImage(value,fadeIn)
is UiImage.Drawable -> setImageDrawable(value)
null -> {
this?.isVisible = false
@ -80,9 +80,9 @@ fun ImageView?.setImage(value: UiImage?) {
}
}
fun ImageView?.setImageImage(value: UiImage.Image) {
fun ImageView?.setImageImage(value: UiImage.Image, fadeIn: Boolean = true) {
if (this == null) return
this.isVisible = setImage(value.url, value.headers, value.errorDrawable)
this.isVisible = setImage(value.url, value.headers, value.errorDrawable, fadeIn)
}
fun ImageView?.setImageDrawable(value: UiImage.Drawable) {

View file

@ -221,7 +221,9 @@ class SearchFragment : Fragment() {
builder.setContentView(R.layout.home_select_mainpage)
builder.show()
builder.let { dialog ->
val isMultiLang = ctx.getApiProviderLangSettings().size > 1
val isMultiLang = ctx.getApiProviderLangSettings().let { set ->
set.size > 1 || set.contains(AllLanguagesName)
}
val cancelBtt = dialog.findViewById<MaterialButton>(R.id.cancel_btt)
val applyBtt = dialog.findViewById<MaterialButton>(R.id.apply_btt)

View file

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.ui.settings
import android.content.Context
import android.os.Bundle
import android.view.View
import androidx.preference.PreferenceFragmentCompat
@ -9,17 +8,15 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.APIRepository
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import kotlin.reflect.jvm.internal.impl.descriptors.deserialization.PlatformDependentDeclarationFilter.All
class SettingsProviders : PreferenceFragmentCompat() {
@ -63,13 +60,17 @@ class SettingsProviders : PreferenceFragmentCompat() {
getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener {
val names = enumValues<TvType>().sorted().map { it.name }
val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }.map { it.ordinal }
val default =
enumValues<TvType>().sorted().filter { it != TvType.NSFW }.map { it.ordinal }
val defaultSet = default.map { it.toString() }.toSet()
val currentList = try {
settingsManager.getStringSet(getString(R.string.prefer_media_type_key), defaultSet)?.map {
settingsManager.getStringSet(getString(R.string.prefer_media_type_key), defaultSet)
?.map {
it.toInt()
}
} catch (e: Throwable) { null } ?: default
} catch (e: Throwable) {
null
} ?: default
activity?.showMultiDialog(
names,
@ -89,20 +90,23 @@ class SettingsProviders : PreferenceFragmentCompat() {
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
activity?.getApiProviderLangSettings()?.let { current ->
val langs = APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) }
val languages = APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
val currentList = ArrayList<Int>()
for (i in current) {
currentList.add(langs.indexOf(i))
val currentList = current.map {
languages.indexOf(it)
}
val names = langs.map {
val names = languages.map {
if (it == AllLanguagesName) {
Pair(it, getString(R.string.all_languages_preference))
} else {
val emoji = SubtitleHelper.getFlagFromIso(it)
val name = SubtitleHelper.fromTwoLettersToLanguage(it)
val fullName = "$emoji $name"
Pair(it, fullName)
}
}
activity?.showMultiDialog(
names.map { it.second },

View file

@ -33,8 +33,6 @@ import com.lagradost.cloudstream3.widget.LinearRecycleViewLayoutManager
import kotlinx.android.synthetic.main.add_repo_input.*
import kotlinx.android.synthetic.main.fragment_extensions.*
const val PUBLIC_REPOSITORIES_LIST = "https://recloudstream.github.io/repos/"
class ExtensionsFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
@ -186,15 +184,7 @@ class ExtensionsFragment : Fragment() {
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt(
0
)?.text?.toString()?.let { copy ->
// Fix our own repo links and only paste the text if it's a link.
if (copy.startsWith("http")) {
val fixedUrl = if (copy.startsWith("https://cs.repo")) {
"https://" + copy.substringAfter("?")
} else {
copy
}
dialog.repo_url_input?.setText(fixedUrl)
}
dialog.repo_url_input?.setText(copy)
}
// dialog.list_repositories?.setOnClickListener {
@ -206,13 +196,12 @@ class ExtensionsFragment : Fragment() {
// dialog.text2?.text = provider.name
dialog.apply_btt?.setOnClickListener secondListener@{
val name = dialog.repo_name_input?.text?.toString()
ioSafe {
val url = dialog.repo_url_input?.text?.toString()
?.let { it1 -> RepositoryManager.parseRepoUrl(it1) }
if (url.isNullOrBlank()) {
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
return@secondListener
}
ioSafe {
} else {
val fixedName = if (!name.isNullOrBlank()) name
else RepositoryManager.parseRepository(url)?.name ?: "No name"
@ -222,6 +211,7 @@ class ExtensionsFragment : Fragment() {
extensionViewModel.loadRepositories()
this@ExtensionsFragment.activity?.downloadAllPluginsDialog(url, fixedName)
}
}
dialog.dismissSafe(activity)
}
dialog.cancel_btt?.setOnClickListener {

View file

@ -7,6 +7,9 @@ import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.lagradost.cloudstream3.APIHolder.apis
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.MainActivity.Companion.afterRepositoryLoadedEvent
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.plugins.RepositoryManager
@ -96,7 +99,14 @@ class SetupFragmentExtensions : Fragment() {
next_btt?.setOnClickListener {
// Continue setup
if (isSetup)
if (
// If any available languages
apis.distinctBy { it.lang }.size > 1
) {
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
} else {
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media)
}
else
findNavController().navigate(R.id.navigation_home)
}

View file

@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES
import com.lagradost.cloudstream3.ui.settings.appLanguages
import com.lagradost.cloudstream3.ui.settings.getCurrentLocale
import com.lagradost.cloudstream3.utils.SubtitleHelper
@ -85,7 +84,7 @@ class SetupFragmentLanguage : Fragment() {
&& PluginManager.getPluginsLocal().isEmpty()
//&& PREBUILT_REPOSITORIES.isNotEmpty()
) R.id.action_navigation_global_to_navigation_setup_extensions
else R.id.action_navigation_setup_language_to_navigation_setup_media
else R.id.action_navigation_setup_language_to_navigation_setup_provider_languages
findNavController().navigate(
nextDestination,

View file

@ -12,6 +12,7 @@ import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
import com.lagradost.cloudstream3.AllLanguagesName
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.SubtitleHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
@ -39,14 +40,21 @@ class SetupFragmentProviderLanguage : Fragment() {
val current = this.getApiProviderLangSettings()
val langs = APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) }
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
val currentList =
current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO
val currentList = current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO
val languageNames = langs.map {
if (it == AllLanguagesName) {
getString(R.string.all_languages_preference)
} else {
val emoji = SubtitleHelper.getFlagFromIso(it)
val name = SubtitleHelper.fromTwoLettersToLanguage(it)
"$emoji $name"
}
}
arrayAdapter.addAll(languageNames)
listview1?.adapter = arrayAdapter

View file

@ -49,8 +49,7 @@ data class SaveCaptionStyle(
@JsonProperty("foregroundColor") var foregroundColor: Int,
@JsonProperty("backgroundColor") var backgroundColor: Int,
@JsonProperty("windowColor") var windowColor: Int,
@CaptionStyleCompat.EdgeType
@JsonProperty("edgeType") var edgeType: Int,
@JsonProperty("edgeType") var edgeType: @CaptionStyleCompat.EdgeType Int,
@JsonProperty("edgeColor") var edgeColor: Int,
@FontRes
@JsonProperty("typeface") var typeface: Int?,

View file

@ -20,10 +20,13 @@ class CastOptionsProvider : OptionsProvider {
MediaIntentReceiver.ACTION_FORWARD,
MediaIntentReceiver.ACTION_STOP_CASTING
)
val name = ControllerActivity::class.qualifiedName!!
val compatButtonAction = intArrayOf(1, 3)
val notificationOptions =
NotificationOptions.Builder()
.setTargetActivityClassName(ControllerActivity::class.qualifiedName)
.setTargetActivityClassName(name)
.setActions(buttonActions, compatButtonAction)
.setForward30DrawableResId(R.drawable.go_forward_30)
.setRewind30DrawableResId(R.drawable.go_back_30)
@ -32,7 +35,7 @@ class CastOptionsProvider : OptionsProvider {
val mediaOptions = CastMediaOptions.Builder()
.setNotificationOptions(notificationOptions)
.setExpandedControllerActivityClassName(ControllerActivity::class.qualifiedName)
.setExpandedControllerActivityClassName(name)
.build()
return CastOptions.Builder()
@ -44,7 +47,7 @@ class CastOptionsProvider : OptionsProvider {
.build()
}
override fun getAdditionalSessionProviders(p0: Context?): MutableList<SessionProvider> {
override fun getAdditionalSessionProviders(p0: Context): MutableList<SessionProvider> {
return Collections.emptyList()
}
}

View file

@ -329,6 +329,8 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
Vidmolyme(),
Voe(),
Moviehab(),
MoviehabNet(),
Jeniusplay(),
Gdriveplayerapi(),
Gdriveplayerapp(),

View file

@ -136,7 +136,7 @@ object UIHelper {
navigation, arguments
)
}
} catch (t : Throwable) {
} catch (t: Throwable) {
logError(t)
}
}
@ -159,17 +159,20 @@ object UIHelper {
url: String?,
headers: Map<String, String>? = null,
@DrawableRes
errorImageDrawable: Int? = null
errorImageDrawable: Int? = null,
fadeIn: Boolean = true
): Boolean {
if (this == null || url.isNullOrBlank()) return false
return try {
val builder = GlideApp.with(this)
.load(GlideUrl(url) { headers ?: emptyMap() }).transition(
DrawableTransitionOptions.withCrossFade()
)
.load(GlideUrl(url) { headers ?: emptyMap() })
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.diskCacheStrategy(DiskCacheStrategy.ALL).let { req ->
if (fadeIn)
req.transition(DrawableTransitionOptions.withCrossFade())
else req
}
val res = if (errorImageDrawable != null)
builder.error(errorImageDrawable).into(this)

View file

@ -287,39 +287,23 @@
android:visibility="gone"
tools:visibility="visible">
<androidx.viewpager.widget.ViewPager
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/home_preview_viewpager"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="horizontal">
</androidx.viewpager.widget.ViewPager>
</androidx.viewpager2.widget.ViewPager2>
<ImageView
android:visibility="gone"
android:id="@+id/home_preview_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:alpha="0.8"
android:scaleType="centerCrop"
android:visibility="gone"
tools:src="@drawable/example_poster" />
<View
android:id="@+id/title_shadow_top"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_gravity="top"
android:alpha="1"
android:background="@drawable/background_shadow"
android:rotation="180"
android:visibility="visible" />
<View
android:id="@+id/title_shadow"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_gravity="bottom"
android:background="@drawable/background_shadow" />
<LinearLayout
android:id="@+id/home_padding"
android:layout_width="match_parent"
@ -363,43 +347,14 @@
-->
<LinearLayout
android:id="@+id/home_preview_title_holder"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="100dp"
android:layout_gravity="bottom"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/home_preview_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:paddingBottom="10dp"
android:textColor="?attr/white"
android:textSize="17sp"
android:textStyle="bold"
tools:text="The Perfect Run" />
<!--<TextView
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:id="@+id/home_season_tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/white"
android:textSize="14sp"
tools:text="5 seasons 50 episodes" />-->
<TextView
android:id="@+id/home_preview_tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:textColor="?attr/white"
android:textSize="14sp"
tools:text="Hello • World • Tags" />
<LinearLayout
android:layout_width="match_parent"

View file

@ -5,7 +5,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:keepScreenOn="true"
android:id="@+id/player_background"
app:backgroundTint="@android:color/black"
android:background="@android:color/black"

View file

@ -6,7 +6,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:keepScreenOn="true"
android:orientation="horizontal"
android:screenOrientation="sensorLandscape"
app:backgroundTint="@android:color/black"

View file

@ -3,9 +3,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/result_root"
style="@style/DarkFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
style="@style/DarkFragment"
android:background="?attr/primaryBlackBackground"
android:clickable="true"
android:focusable="true">
@ -122,9 +122,9 @@
android:id="@+id/result_finish_loading"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible"
android:orientation="vertical">
tools:visibility="visible">
<androidx.core.widget.NestedScrollView
android:id="@+id/result_scroll"
@ -139,18 +139,19 @@
android:background="?attr/primaryBlackBackground"
android:orientation="vertical">
<!--
<com.facebook.shimmer.ShimmerFrameLayout
tools:visibility="gone"
android:visibility="gone"
android:id="@+id/result_trailer_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:visibility="gone"
app:shimmer_auto_start="true"
app:shimmer_base_alpha="0.2"
app:shimmer_duration="@integer/loading_time"
app:shimmer_highlight_alpha="0.3">
app:shimmer_highlight_alpha="0.3"
tools:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
@ -159,22 +160,15 @@
android:orientation="vertical">
<androidx.cardview.widget.CardView
android:background="@color/grayShimmer"
app:cardCornerRadius="@dimen/loading_radius"
android:layout_width="match_parent"
android:layout_height="150dp"
android:foreground="@drawable/outline_drawable" />
android:background="@color/grayShimmer"
android:foreground="@drawable/outline_drawable"
app:cardCornerRadius="@dimen/loading_radius" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
-->
<FrameLayout
android:descendantFocusability="blocksDescendants"
android:id="@+id/result_smallscreen_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<include layout="@layout/fragment_trailer" />
</FrameLayout>
<!--
<FrameLayout
@ -272,6 +266,36 @@
app:tint="?attr/textColor" />
</LinearLayout>
</FrameLayout>-->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="200dp">
<FrameLayout
android:id="@+id/result_smallscreen_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants">
<include layout="@layout/fragment_trailer" />
</FrameLayout>
<FrameLayout
android:id="@+id/result_poster_background_holder"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/result_poster_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
<View
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="bottom"
android:background="@drawable/background_shadow" />
</FrameLayout>
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
@ -281,6 +305,7 @@
android:paddingStart="@dimen/result_padding"
android:paddingEnd="@dimen/result_padding">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -289,10 +314,12 @@
android:orientation="horizontal"
android:visibility="visible">
<androidx.cardview.widget.CardView
android:id="@+id/result_poster_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:cardCornerRadius="@dimen/rounded_image_radius">
<ImageView
@ -312,7 +339,6 @@
android:orientation="vertical">
<TextView
android:layout_marginHorizontal="10dp"
android:id="@+id/result_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -324,7 +350,6 @@
tools:text="The Perfect Run The Perfect Run" />
<com.lagradost.cloudstream3.widget.FlowLayout
android:layout_marginHorizontal="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:itemSpacing="10dp">
@ -366,20 +391,19 @@
The focus outline now settles between the poster and text.
-->
<FrameLayout
android:layout_marginHorizontal="5dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:padding="5dp"
android:maxLength="1000"
android:ellipsize="end"
android:id="@+id/result_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:foreground="@drawable/outline_drawable"
android:maxLength="1000"
android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_bookmark_button"
android:paddingTop="5dp"
android:textColor="?attr/textColor"
android:textSize="15sp"
tools:text="Ryan Quicksave Romano is an eccentric adventurer with a strange power: he can create a save-point in time and redo his life whenever he dies. Arriving in New Rome, the glitzy capital of sin of a rebuilding Europe, he finds the city torn between mega-corporations, sponsored heroes, superpowered criminals, and true monsters. It's a time of chaos, where potions can grant the power to rule the world and dangers lurk everywhere. " />
@ -389,12 +413,13 @@
android:layout_height="30dp"
android:layout_gravity="bottom"
android:src="@drawable/background_shadow"
android:visibility="gone"
tools:ignore="ContentDescription" />
</FrameLayout>
</LinearLayout>
</LinearLayout>
<!--
<!--
<com.google.android.material.button.MaterialButton
android:id="@+id/result_bookmark_button"
style="@style/BlackButton"
@ -417,7 +442,7 @@
app:icon="@drawable/ic_baseline_bookmark_24"
tools:text="Bookmark"
tools:visibility="visible" />
-->
-->
<TextView
android:id="@+id/result_cast_text"
@ -431,23 +456,23 @@
tools:text="Cast: Joe Ligma" />
<androidx.recyclerview.widget.RecyclerView
tools:visibility="gone"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_play_movie"
android:id="@+id/result_cast_items"
android:layout_width="match_parent"
android:descendantFocusability="afterDescendants"
android:layout_height="wrap_content"
android:descendantFocusability="afterDescendants"
android:fadingEdge="horizontal"
android:focusableInTouchMode="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:nextFocusUp="@id/result_bookmark_button"
android:nextFocusDown="@id/result_play_movie"
android:orientation="horizontal"
android:paddingTop="5dp"
android:requiresFadingEdge="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="2"
tools:listitem="@layout/cast_item" />
tools:listitem="@layout/cast_item"
tools:visibility="gone" />
<TextView
android:id="@+id/result_vpn"
@ -847,29 +872,29 @@
<!--TODO add next airing-->
<LinearLayout
android:id="@+id/result_next_airing_holder"
android:layout_gravity="start"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:layout_gravity="start"
android:orientation="horizontal">
<TextView
android:gravity="center"
android:id="@+id/result_next_airing"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="?attr/grayTextColor"
android:textSize="17sp"
android:textStyle="normal"
tools:text="Episode 1022 will be released in" />
<TextView
android:paddingEnd="5dp"
android:paddingStart="5dp"
android:gravity="center"
android:id="@+id/result_next_airing_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingStart="5dp"
android:paddingEnd="5dp"
android:textColor="?attr/textColor"
android:textSize="17sp"
android:textStyle="normal"
@ -917,9 +942,9 @@
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
android:descendantFocusability="afterDescendants"
android:paddingBottom="100dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/result_episode_both_tv" />
</LinearLayout>
</LinearLayout>

View file

@ -6,7 +6,6 @@
android:layout_height="0dp"
android:visibility="visible"
android:orientation="horizontal"
android:keepScreenOn="true"
android:id="@+id/player_background"
app:backgroundTint="@android:color/black"
android:background="@android:color/black"

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:background="?attr/primaryGrayBackground"
android:layout_height="match_parent">
<ImageView
android:id="@+id/home_scroll_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
tools:src="@drawable/example_poster" />
<View
android:id="@+id/title_shadow_top"
android:layout_width="match_parent"
android:layout_height="150dp"
android:layout_gravity="top"
android:alpha="1"
android:background="@drawable/background_shadow"
android:rotation="180"
android:visibility="visible" />
<View
android:id="@+id/title_shadow"
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_gravity="bottom"
android:background="@drawable/background_shadow" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:layout_marginBottom="100dp"
android:orientation="vertical">
<TextView
android:id="@+id/home_scroll_preview_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:paddingHorizontal="30dp"
android:paddingBottom="10dp"
android:textColor="?attr/white"
android:textSize="17sp"
android:textStyle="bold"
tools:text="The Perfect Run" />
<!--<TextView
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:id="@+id/home_season_tags"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textColor="@color/white"
android:textSize="14sp"
tools:text="5 seasons 50 episodes" />-->
<TextView
android:id="@+id/home_scroll_preview_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:paddingStart="30dp"
android:paddingEnd="30dp"
android:textColor="?attr/white"
android:textSize="14sp"
tools:text="Hello • World • Tags" />
</LinearLayout>
</FrameLayout>

View file

@ -38,26 +38,41 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<View
<!-- <View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/player_gradient_tv" />
-->
<!--
<ImageView
android:id="@+id/result_trailer_thumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:scaleType="centerCrop" />-->
<View
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_gravity="bottom"
android:background="@drawable/background_shadow" />
<TextView
android:textSize="20sp"
android:textStyle="bold"
android:textColor="@android:color/white"
android:text="@string/trailer"
android:padding="10dp"
android:layout_gravity="start|bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:layout_gravity="start|bottom"
android:padding="10dp"
android:text="@string/trailer"
android:textColor="@android:color/white"
android:textSize="20sp"
android:textStyle="bold" />
<ImageView
android:layout_gravity="center"
android:src="@drawable/play_button"
android:layout_width="60dp"
android:layout_height="60dp" />
android:layout_height="60dp"
android:layout_gravity="center"
android:src="@drawable/play_button" />
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
@ -67,54 +82,54 @@
<!--use for thinner app:trackThickness="3dp" com.google.android.material.progressindicator.CircularProgressIndicator-->
<ProgressBar
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:id="@+id/player_buffering"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:focusable="false"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:indeterminate="true"
android:visibility="gone"
tools:visibility="visible"
android:id="@+id/player_buffering"
android:layout_gravity="center"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
<!-- This nested layout is necessary because of buffering and clicking-->
<FrameLayout
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<!-- This nested layout is necessary because of buffering and clicking-->
<FrameLayout
android:id="@+id/player_pause_play_holder_holder"
android:layout_width="100dp"
android:layout_height="100dp"
android:id="@+id/player_pause_play_holder_holder">
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
<FrameLayout
tools:ignore="uselessParent"
android:id="@+id/player_pause_play_holder"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
tools:ignore="uselessParent">
<ImageView
app:tint="@color/white"
android:id="@+id/player_pause_play"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_gravity="center"
android:background="@drawable/video_tap_button"
android:nextFocusLeft="@id/exo_rew"
android:nextFocusRight="@id/exo_ffwd"
android:nextFocusUp="@id/player_go_back"
android:nextFocusDown="@id/player_lock"
android:layout_gravity="center"
android:src="@drawable/netflix_pause"
android:background="@drawable/video_tap_button"
android:layout_width="70dp"
android:layout_height="70dp"
app:tint="@color/white"
tools:ignore="ContentDescription" />
</FrameLayout>
</FrameLayout>
@ -135,12 +150,12 @@
android:id="@+id/player_rew_holder"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintWidth_percent="0.5"
android:layout_gravity="center_vertical|start"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@id/player_ffwd_holder"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.5">
<TextView
android:id="@+id/exo_rew_text"
@ -169,21 +184,21 @@
android:scaleType="fitCenter"
android:scaleX="-1"
android:src="@drawable/netflix_skip_forward"
app:tint="@color/white"
android:tintMode="src_in"
app:tint="@color/white"
tools:ignore="ContentDescription" />
</FrameLayout>
<FrameLayout
android:id="@+id/player_ffwd_holder"
android:layout_width="0dp"
app:layout_constraintWidth_percent="0.5"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="@id/player_rew_holder"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_percent="0.5">
<TextView
android:id="@+id/exo_ffwd_text"
@ -209,8 +224,8 @@
android:padding="10dp"
android:scaleType="fitCenter"
android:src="@drawable/netflix_skip_forward"
app:tint="@color/white"
android:tintMode="src_in"
app:tint="@color/white"
tools:ignore="ContentDescription" />
</FrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@ -230,60 +245,60 @@
<ImageButton
android:id="@id/exo_prev"
style="@style/ExoMediaButton.Previous"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
app:tint="?attr/colorPrimaryDark"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@id/exo_repeat_toggle"
style="@style/ExoMediaButton"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
app:tint="?attr/colorPrimaryDark"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@id/exo_next"
style="@style/ExoMediaButton.Next"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
app:tint="?attr/colorPrimaryDark"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@id/exo_vr"
style="@style/ExoMediaButton.VR"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
app:tint="?attr/colorPrimaryDark"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@id/exo_play"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
tools:ignore="ContentDescription"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="0dp" />
android:tintMode="src_in"
app:tint="?attr/colorPrimaryDark"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@id/exo_pause"
app:tint="?attr/colorPrimaryDark"
android:tintMode="src_in"
tools:ignore="ContentDescription"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="0dp" />
android:tintMode="src_in"
app:tint="?attr/colorPrimaryDark"
tools:ignore="ContentDescription" />
</LinearLayout>
<ImageView
android:background="?android:attr/selectableItemBackgroundBorderless"
android:id="@+id/player_open_source"
app:tint="@color/white"
android:src="@drawable/ic_baseline_public_24"
android:layout_margin="20dp"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="20dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_baseline_public_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/white" />
<LinearLayout
android:id="@+id/bottom_player_bar"
@ -296,10 +311,10 @@
app:layout_constraintEnd_toEndOf="parent">
<LinearLayout
android:layout_weight="1"
android:id="@+id/player_video_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="horizontal">
<TextView
@ -350,14 +365,14 @@
</LinearLayout>
<ImageView
android:background="?android:attr/selectableItemBackgroundBorderless"
android:id="@+id/player_fullscreen"
app:tint="@color/white"
android:layout_gravity="center_vertical"
android:src="@drawable/baseline_fullscreen_24"
android:layout_marginEnd="20dp"
android:layout_width="30dp"
android:layout_height="30dp" />
android:layout_height="30dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="20dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@drawable/baseline_fullscreen_24"
app:tint="@color/white" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@ -397,14 +412,14 @@
style="@android:style/Widget.Material.ProgressBar.Horizontal"
android:layout_width="4dp"
android:layout_height="150dp"
android:layout_centerInParent="true"
android:layout_gravity="end|center_vertical"
android:layout_marginStart="40dp"
android:indeterminate="false"
android:max="100"
android:progress="100"
android:progressDrawable="@drawable/progress_drawable_vertical"
tools:progress="30"
android:layout_centerInParent="true" />
tools:progress="30" />
</RelativeLayout>
<RelativeLayout

View file

@ -522,13 +522,6 @@
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
<action
android:id="@+id/action_navigation_setup_language_to_navigation_setup_media"
app:destination="@id/navigation_setup_media"
app:enterAnim="@anim/enter_anim"
app:exitAnim="@anim/exit_anim"
app:popEnterAnim="@anim/enter_anim"
app:popExitAnim="@anim/exit_anim" />
</fragment>
<action

View file

@ -643,4 +643,5 @@
<string name="player_settings_play_in_web">Web Video Cast</string>
<string name="player_settings_play_in_browser">Browser</string>
<string name="app_not_found_error">App not found</string>
<string name="all_languages_preference">All Languages</string>
</resources>

View file

@ -81,6 +81,9 @@
<item name="chipStrokeColor">@color/transparent</item>
<item name="textColor">@color/chip_color_text</item>
<item name="android:textColor">@color/chip_color_text</item>
<item name="checkedIconTint">@color/chip_color_text</item>
<item name="fontFamily">@font/google_sans</item>
<item name="android:fontFamily">@font/google_sans</item>
</style>
<style name="AmoledMode">
@ -137,13 +140,13 @@
</style>
<style name="OverlayPrimaryColorMonetTwo">
<item name="colorPrimary">@color/material_dynamic_tertiary80</item>
<item name="android:colorPrimary">@color/material_dynamic_tertiary80</item>
<item name="colorPrimaryDark">@color/material_dynamic_tertiary30</item>
<item name="colorAccent">@color/material_dynamic_tertiary80</item>
<item name="colorOnPrimary">@color/material_dynamic_tertiary20</item>
<item name="colorPrimary">@color/material_dynamic_secondary80</item>
<item name="android:colorPrimary">@color/material_dynamic_secondary80</item>
<item name="colorPrimaryDark">@color/material_dynamic_secondary30</item>
<item name="colorAccent">@color/material_dynamic_secondary80</item>
<item name="colorOnPrimary">@color/material_dynamic_secondary20</item>
<!-- Needed for leanback fuckery -->
<item name="android:colorAccent">@color/material_dynamic_tertiary30</item>
<item name="android:colorAccent">@color/material_dynamic_secondary30</item>
</style>
<style name="OverlayPrimaryColorBlue">

View file

@ -2,7 +2,6 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Preference
app:isPreferenceVisible="false"
android:icon="@drawable/ic_baseline_language_24"
android:key="@string/provider_lang_key"
android:title="@string/provider_lang_settings" />