diff --git a/.github/workflows/build_to_archive.yml b/.github/workflows/build_to_archive.yml new file mode 100644 index 00000000..83430766 --- /dev/null +++ b/.github/workflows/build_to_archive.yml @@ -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 \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 42ca93fd..99eec423 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index da257d69..190c0cf5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -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() 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 { - val langs = apis.map { it.lang }.toSet() - .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } - return langs.toHashSet() - -// val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) -// val hashSet = HashSet() + 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 { @@ -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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt new file mode 100644 index 00000000..11b66d99 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Jeniusplay.kt @@ -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().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>("[$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?, + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt index e2eb7bf0..aaa33ca1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Moviehab.kt @@ -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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt index 23704e90..572d93a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/YoutubeExtractor.kt @@ -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( diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index c0f35601..b9c775c0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -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>(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() - 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 } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt index 2564abd0..0f23782d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/RepositoryManager.kt @@ -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 @@ -191,4 +213,4 @@ object RepositoryManager { output.write(dataBuffer, 0, readBytes) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt index ad0b4399..46ddce09 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/ControllerActivity.kt @@ -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()) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index 9792fa43..4b9dd5be 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -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,8 +553,10 @@ class HomeFragment : Fragment() { is Resource.Success -> { home_preview?.isVisible = true (home_preview_viewpager?.adapter as? HomeScrollAdapter)?.apply { - setItems(preview.value) - // home_preview_viewpager?.setCurrentItem(1000, false) + if (!setItems(preview.value.second, preview.value.first)) { + home_preview_viewpager?.setCurrentItem(0, false) + } + // home_preview_viewpager?.setCurrentItem(1000, false) } //.also { @@ -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,54 +582,75 @@ 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 - home_preview_play?.setOnClickListener { - activity?.loadResult(url, apiName, START_ACTION_RESUME_LATEST) - //activity.loadSearchResult(url, START_ACTION_RESUME_LATEST) - } - home_preview_info?.setOnClickListener { - activity?.loadResult(url, apiName) - //activity.loadSearchResult(random) - } - // very ugly code, but I dont care - val watchType = DataStoreHelper.getResultWatchState(load.getId()) - home_preview_bookmark?.setText(watchType.stringRes) - home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds( - null, - getDrawable(home_preview_bookmark.context, watchType.iconRes), - null, - null - ) - home_preview_bookmark?.setOnClickListener { fab -> - activity?.showBottomDialog( - WatchType.values().map { fab.context.getString(it.stringRes) } - .toList(), - DataStoreHelper.getResultWatchState(load.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), - null, - null - ) - home_preview_bookmark?.setText(newValue.stringRes) - - updateWatchStatus(load, newValue) - reloadStored() + 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) + } + home_preview_info?.setOnClickListener { + activity?.loadResult(url, apiName) + //activity.loadSearchResult(random) + } + // very ugly code, but I dont care + val watchType = DataStoreHelper.getResultWatchState(this.getId()) + home_preview_bookmark?.setText(watchType.stringRes) + home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds( + null, + getDrawable(home_preview_bookmark.context, watchType.iconRes), + null, + null + ) + home_preview_bookmark?.setOnClickListener { fab -> + activity?.showBottomDialog( + WatchType.values() + .map { fab.context.getString(it.stringRes) } + .toList(), + 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 + ), + null, + null + ) + home_preview_bookmark?.setText(newValue.stringRes) + + updateWatchStatus(this, newValue) + reloadStored() + } + } + + } } } - } + registerOnPageChangeCallback(callback) + adapter = HomeScrollAdapter() } observe(homeViewModel.apiName) { apiName -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt index b36d7582..a3eaf7c7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt @@ -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 = listOf() +class HomeScrollAdapter : RecyclerView.Adapter() { + private var items: MutableList = mutableListOf() + var hasMoreItems: Boolean = false - fun setItems(newItems: List) { - 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, 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, + private val newList: List + ) : + 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(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 } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt index 04b6964b..2d6757f9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollTransformer.kt @@ -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 ) } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 43d849cf..9d75b0f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -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?>(null) val randomItems: LiveData?> = _randomItems + private var currentShuffledList: List = listOf() + private fun autoloadRepo(): APIRepository { return APIRepository(apis.first { it.hasMainPage }) } @@ -61,9 +58,12 @@ class HomeViewModel : ViewModel() { val bookmarks: LiveData>> = _bookmarks private val _resumeWatching = MutableLiveData>() - private val _preview = MutableLiveData>>() + private val _preview = MutableLiveData>>>() + private val previewResponses = mutableListOf() + private val previewResponsesAdded = mutableSetOf() + val resumeWatching: LiveData> = _resumeWatching - val preview: LiveData>> = _preview + val preview: LiveData>>> = _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, + alreadyAdded: MutableSet, + shuffledList: List, + size: Int + ): Int { + var count = 0 + + val addItems = arrayListOf() + 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) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index a838c85c..f60d8c78 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -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.getTrack(id: String?): Pair? { + 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.getFormats(): List { + private fun List.getFormats(): List> { 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) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 816b1298..f466dd7e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -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) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index e8934250..473b3e65 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java new file mode 100644 index 00000000..8602ce25 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.java @@ -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. + * + *

{@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. + * + *

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 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) msg.obj); + return true; + default: + throw new IllegalStateException(); + } + } + + private void invokeUpdateOutputInternal(List 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. + * + *

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(); + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.kt deleted file mode 100644 index f7fb3139..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/NonFinalTextRenderer.kt +++ /dev/null @@ -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, 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) { - 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) - true - } - else -> throw IllegalStateException() - } - } - - private fun invokeUpdateOutputInternal(cues: List) { - 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 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index db71b714..26dbb03e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -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 { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index 813872c9..9bae8753 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -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 = 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,13 +338,14 @@ 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) setFocusUpAndDown(result_resume_series_button, result_season_button) - //else - // setFocusUpAndDown(result_bookmark_button, result_season_button) + //else + // setFocusUpAndDown(result_bookmark_button, result_season_button) } observe(viewModel.selectedDubStatus) { status -> @@ -317,8 +355,8 @@ class ResultFragmentPhone : ResultFragment() { if (result_season_button?.isVisible != true && result_episode_select?.isVisible != true) { if (result_resume_parent?.isVisible == true) setFocusUpAndDown(result_resume_series_button, result_dub_select) - //else - // setFocusUpAndDown(result_bookmark_button, result_dub_select) + //else + // setFocusUpAndDown(result_bookmark_button, result_dub_select) } } observe(viewModel.selectedRange) { range -> @@ -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 //}) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt index 26c9249d..8a6e5b37 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultTrailerPlayer.kt @@ -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.* diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index f424989e..6b04ebf9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -88,6 +88,7 @@ data class ResultData( var syncData: Map, 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(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index 0ca232e1..81ef8d57 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -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) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index a4b2be8f..bcb36f7e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -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(R.id.cancel_btt) val applyBtt = dialog.findViewById(R.id.apply_btt) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 7bba9d88..4371fc39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -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().sorted().map { it.name } - val default = enumValues().sorted().filter { it != TvType.NSFW }.map { it.ordinal } + val default = + enumValues().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 { - it.toInt() - } - } catch (e: Throwable) { null } ?: default + settingsManager.getStringSet(getString(R.string.prefer_media_type_key), defaultSet) + ?.map { + it.toInt() + } + } catch (e: Throwable) { + null + } ?: default activity?.showMultiDialog( names, @@ -89,19 +90,22 @@ 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() - for (i in current) { - currentList.add(langs.indexOf(i)) + val currentList = current.map { + languages.indexOf(it) } - val names = langs.map { - val emoji = SubtitleHelper.getFlagFromIso(it) - val name = SubtitleHelper.fromTwoLettersToLanguage(it) - val fullName = "$emoji $name" - Pair(it, fullName) + 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( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index 418519e2..49f40879 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -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,21 +196,21 @@ class ExtensionsFragment : Fragment() { // dialog.text2?.text = provider.name dialog.apply_btt?.setOnClickListener secondListener@{ val name = dialog.repo_name_input?.text?.toString() - val url = dialog.repo_url_input?.text?.toString() - if (url.isNullOrBlank()) { - showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT) - return@secondListener - } - ioSafe { - val fixedName = if (!name.isNullOrBlank()) name - else RepositoryManager.parseRepository(url)?.name ?: "No name" + 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) + } else { + val fixedName = if (!name.isNullOrBlank()) name + else RepositoryManager.parseRepository(url)?.name ?: "No name" - val newRepo = RepositoryData(fixedName, url) - RepositoryManager.addRepository(newRepo) - extensionViewModel.loadStats() - extensionViewModel.loadRepositories() - this@ExtensionsFragment.activity?.downloadAllPluginsDialog(url, fixedName) + val newRepo = RepositoryData(fixedName, url) + RepositoryManager.addRepository(newRepo) + extensionViewModel.loadStats() + extensionViewModel.loadRepositories() + this@ExtensionsFragment.activity?.downloadAllPluginsDialog(url, fixedName) + } } dialog.dismissSafe(activity) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index c4bf7580..b7d2fff6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -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) - findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media) + 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) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt index f9268d77..80db59ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentLanguage.kt @@ -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, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index 3e24cc4d..51abee90 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -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 { - val emoji = SubtitleHelper.getFlagFromIso(it) - val name = SubtitleHelper.fromTwoLettersToLanguage(it) - "$emoji $name" + 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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index b97468e7..ff0e0e82 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -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?, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/CastOptionsProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/CastOptionsProvider.kt index 17ec775b..ece9a4c0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/CastOptionsProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/CastOptionsProvider.kt @@ -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 { + override fun getAdditionalSessionProviders(p0: Context): MutableList { return Collections.emptyList() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index ca612385..1a1afb68 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -329,6 +329,8 @@ val extractorApis: MutableList = arrayListOf( Vidmolyme(), Voe(), Moviehab(), + MoviehabNet(), + Jeniusplay(), Gdriveplayerapi(), Gdriveplayerapp(), diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index ab49492a..20d6d03e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -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? = 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) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 3d7e3ba5..4eb7b9ee 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -287,39 +287,23 @@ android:visibility="gone" tools:visibility="visible"> - + android:layout_height="match_parent" + android:orientation="horizontal"> - + - - - - - - - + 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" + android:background="?attr/primaryBlackBackground" + android:clickable="true" + android:focusable="true"> + android:id="@+id/result_loading" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:orientation="vertical" + app:shimmer_auto_start="true" + app:shimmer_base_alpha="0.2" + app:shimmer_duration="@integer/loading_time" + app:shimmer_highlight_alpha="0.3" + tools:visibility="gone"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/result_padding" + android:orientation="vertical"> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="@dimen/loading_margin" + android:orientation="horizontal"> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginStart="@dimen/loading_margin" + android:layout_marginEnd="@dimen/loading_margin" + android:orientation="vertical"> @@ -56,9 +56,9 @@ + android:layout_width="match_parent" + android:layout_height="20dp" + tools:ignore="ContentDescription" /> @@ -75,106 +75,100 @@ android:layout_width="50dp" android:layout_height="50dp"> --> + + + + + + - - - - - - + android:layout_margin="5dp" + android:gravity="center" + android:textColor="?attr/textColor" /> + 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:layout_height="wrap_content" + android:background="?attr/primaryGrayBackground"> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?attr/primaryBlackBackground" + android:orientation="vertical"> + - - - - + - + + + + + + android:scaleType="centerCrop" /> + + + + + + + + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:layout_marginBottom="15dp" + android:orientation="horizontal" + android:visibility="visible"> + + 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"> + android:layout_width="100dp" + android:layout_height="140dp" + android:contentDescription="@string/result_poster_img_des" + android:foreground="@drawable/outline_drawable" + android:scaleType="centerCrop" + tools:src="@drawable/example_poster" /> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + android:id="@+id/result_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="5dp" + android:maxLines="2" + android:textColor="?attr/textColor" + android:textSize="20sp" + android:textStyle="bold" + tools:text="The Perfect Run The Perfect Run" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:itemSpacing="10dp"> + android:id="@+id/result_meta_site" + style="@style/SmallBlackButton" + android:layout_gravity="center_vertical" + tools:text="Gogoanime" /> + android:id="@+id/result_meta_type" + style="@style/ResultInfoText" + tools:text="Movie" /> + android:id="@+id/result_meta_year" + style="@style/ResultInfoText" + tools:text="2022" /> + android:id="@+id/result_meta_rating" + style="@style/ResultInfoText" + tools:text="Rated: 8.5/10.0" /> + android:id="@+id/result_meta_status" + style="@style/ResultInfoText" + tools:text="Ongoing" /> + android:id="@+id/result_meta_duration" + style="@style/ResultInfoText" + tools:text="121min" /> + android:layout_width="match_parent" + android:layout_height="match_parent"> + 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. " /> + android:layout_width="match_parent" + android:layout_height="30dp" + android:layout_gravity="bottom" + android:src="@drawable/background_shadow" + android:visibility="gone" + tools:ignore="ContentDescription" /> - + android:nextFocusDown="@id/result_cast_items" + android:paddingTop="0dp" + app:cornerRadius="4dp" + app:icon="@drawable/ic_baseline_bookmark_24" + tools:text="Bookmark" + tools:visibility="visible" /> + --> + android:id="@+id/result_cast_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="5dp" + android:ellipsize="end" + android:maxLines="2" + android:textColor="?attr/grayTextColor" + android:textSize="15sp" + tools:text="Cast: Joe Ligma" /> + android:descendantFocusability="afterDescendants" + android:fadingEdge="horizontal" + 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:visibility="gone" /> + android:id="@+id/result_vpn" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="?attr/grayTextColor" + android:textSize="15sp" + tools:text="@string/vpn_torrent" /> + android:id="@+id/result_info" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="5dp" + android:textColor="?attr/grayTextColor" + android:textSize="15sp" + tools:text="@string/provider_info_meta" /> + android:id="@+id/result_no_episodes" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="5dp" + android:textColor="?attr/grayTextColor" + android:textSize="15sp" + tools:text="@string/no_episodes_found" /> + android:id="@+id/result_tag_holder" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp" + android:text="@string/result_tags" + android:textColor="?attr/textColor" + android:textSize="17sp" + android:textStyle="normal" + android:visibility="gone" /> --> + android:id="@+id/result_coming_soon" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + android:paddingTop="50dp" + android:text="@string/coming_soon" + android:textColor="?attr/textColor" + android:textSize="20sp" + android:textStyle="bold" + android:visibility="gone" /> + android:id="@+id/result_data_holder" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + android:layout_marginBottom="10dp" + android:text="@string/add_sync" + android:visibility="gone" + app:icon="@drawable/ic_baseline_add_24" /> + android:id="@+id/result_movie_parent" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="5dp" + android:orientation="vertical" + tools:visibility="visible"> + android:layout_gravity="center_vertical" + android:layout_marginStart="0dp" + android:layout_marginEnd="0dp" + android:layout_marginBottom="10dp" + android:nextFocusUp="@id/result_bookmark_button" + android:nextFocusDown="@id/result_download_movie" + android:text="@string/play_movie_button" + android:visibility="visible" + app:icon="@drawable/ic_baseline_play_arrow_24"> @@ -574,85 +599,85 @@ android:layout_width="match_parent" />--> + android:id="@+id/result_movie_progress_downloaded_holder" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + android:nextFocusUp="@id/result_play_movie" + android:nextFocusDown="@id/result_season_button" + android:visibility="visible" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + android:orientation="horizontal"> + android:id="@+id/result_movie_progress_downloaded" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="25dp" + android:layout_height="25dp" + android:layout_gravity="end|center_vertical" + android:layout_margin="5dp" + android:background="@drawable/circle_shape" + android:indeterminate="false" + android:max="100" + android:paddingStart="5dp" + android:paddingEnd="5dp" + android:progress="30" + android:progressDrawable="@drawable/circular_progress_bar_filled" + android:visibility="visible" /> + android:id="@+id/result_movie_download_icon" + android:layout_width="30dp" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="?selectableItemBackgroundBorderless" + android:contentDescription="@string/download" + android:src="@drawable/ic_baseline_play_arrow_24" + android:visibility="visible" + app:tint="?attr/white" /> + android:id="@+id/result_movie_download_text" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center" + android:letterSpacing="0.09" + android:textAllCaps="false" + android:textColor="?attr/textColor" + android:textSize="15sp" + android:textStyle="bold" + tools:text="Downloading" /> + android:id="@+id/result_movie_download_text_precentage" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center" + android:letterSpacing="0.09" + android:paddingStart="5dp" + android:paddingEnd="5dp" + android:textAllCaps="false" + android:textColor="?attr/textColor" + android:textSize="15sp" + android:textStyle="bold" + android:visibility="gone" + tools:text="68%" /> @@ -682,217 +707,217 @@ + + + + + - - - - - + app:icon="@drawable/cast_ic_mini_controller_skip_next" /> + android:id="@+id/result_resume_series_title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="?attr/textColor" + android:textSize="17sp" + android:textStyle="bold" + tools:text="S1E1 Episode 1" /> + android:id="@+id/result_resume_progress_holder" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingTop="10dp" + android:visibility="gone" + tools:visibility="visible"> + android:id="@+id/result_resume_series_progress" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="20dp" + android:layout_gravity="end|center_vertical" + android:layout_weight="1" + android:indeterminate="false" + android:max="100" + android:progress="0" + android:progressBackgroundTint="?attr/colorPrimary" + android:visibility="visible" + tools:progress="50" + tools:visibility="visible" /> + android:id="@+id/result_resume_series_progress_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_weight="0" + android:gravity="center" + android:paddingStart="10dp" + android:textColor="?attr/grayTextColor" + tools:ignore="RtlSymmetry" + tools:text="69m\nremaining" /> + android:id="@+id/result_episodes_tab" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:gravity="center_vertical" + android:orientation="horizontal"> + android:id="@+id/result_season_button" + style="@style/MultiSelectButton" + android:layout_gravity="center_vertical" + android:layout_marginStart="0dp" + android:layout_marginEnd="10dp" + android:nextFocusLeft="@id/result_episode_select" + android:nextFocusRight="@id/result_episode_select" + android:nextFocusUp="@id/result_description" + android:nextFocusDown="@id/result_episodes" + android:visibility="gone" + tools:text="Season 1" + tools:visibility="visible" /> + android:nextFocusUp="@id/result_description" + android:nextFocusDown="@id/result_episodes" + android:visibility="gone" + tools:text="50-100" + tools:visibility="visible" /> + android:nextFocusUp="@id/result_description" + android:nextFocusDown="@id/result_episodes" + android:visibility="gone" + tools:text="Dubbed" + tools:visibility="visible" /> + android:id="@+id/result_episodes_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:paddingTop="10dp" + android:paddingBottom="10dp" + android:textColor="?attr/textColor" + android:textSize="17sp" + android:textStyle="normal" + tools:text="8 Episodes" /> + android:id="@+id/result_next_airing_holder" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="start" + android:orientation="horizontal"> + 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" /> + 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" + tools:text="5d 3h 30m" /> + android:id="@+id/result_episode_loading" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:layout_marginTop="15dp" + android:orientation="vertical" + app:shimmer_auto_start="true" + app:shimmer_base_alpha="0.2" + app:shimmer_duration="@integer/loading_time" + app:shimmer_highlight_alpha="0.3" + tools:visibility="visible"> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> @@ -912,15 +937,15 @@ android:layout_height="50dp" />--> + android:id="@+id/result_episodes" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="0dp" + android:clipToPadding="false" + android:descendantFocusability="afterDescendants" + android:paddingBottom="100dp" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + tools:listitem="@layout/result_episode_both_tv" /> diff --git a/app/src/main/res/layout/fragment_trailer.xml b/app/src/main/res/layout/fragment_trailer.xml index 64633483..fd348760 100644 --- a/app/src/main/res/layout/fragment_trailer.xml +++ b/app/src/main/res/layout/fragment_trailer.xml @@ -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" diff --git a/app/src/main/res/layout/home_scroll_view.xml b/app/src/main/res/layout/home_scroll_view.xml new file mode 100644 index 00000000..c106bee0 --- /dev/null +++ b/app/src/main/res/layout/home_scroll_view.xml @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/trailer_custom_layout.xml b/app/src/main/res/layout/trailer_custom_layout.xml index 22137a1c..854855ef 100644 --- a/app/src/main/res/layout/trailer_custom_layout.xml +++ b/app/src/main/res/layout/trailer_custom_layout.xml @@ -1,452 +1,467 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/player_holder" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:screenOrientation="landscape" + tools:orientation="vertical"> + android:id="@+id/player_time_text" + android:layout_width="match_parent" + android:layout_height="200dp" + android:gravity="center" + android:shadowColor="@android:color/black" + android:shadowRadius="10.0" + android:textColor="@android:color/white" + android:textSize="30sp" + tools:text="+100" /> + android:id="@+id/subtitle_holder" + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:id="@+id/shadow_overlay" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@color/black_overlay" /> + + + + + android:layout_width="match_parent" + android:layout_height="60dp" + android:layout_gravity="bottom" + android:background="@drawable/background_shadow" /> + android:layout_width="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" /> + android:layout_width="60dp" + android:layout_height="60dp" + android:layout_gravity="center" + android:src="@drawable/play_button" /> + android:id="@+id/player_video_holder" + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:indeterminate="true" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> + android:id="@+id/player_pause_play_holder_holder" + android:layout_width="100dp" + android:layout_height="100dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:id="@+id/player_pause_play_holder" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:ignore="uselessParent"> + android:src="@drawable/netflix_pause" + app:tint="@color/white" + tools:ignore="ContentDescription" /> + android:id="@+id/player_center_menu" + android:layout_width="match_parent" + android:layout_height="100dp" + android:layout_gravity="center" + android:gravity="center" + android:orientation="horizontal" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent"> + android:id="@+id/player_rew_holder" + android:layout_width="0dp" + android:layout_height="wrap_content" + 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_constraintWidth_percent="0.5"> + android:textStyle="bold" + tools:text="10" /> + android:background="@drawable/video_tap_button_skip" + android:nextFocusLeft="@id/exo_rew" + android:nextFocusUp="@id/player_go_back" + android:nextFocusDown="@id/player_lock" + android:padding="10dp" + android:scaleType="fitCenter" + android:scaleX="-1" + android:src="@drawable/netflix_skip_forward" + android:tintMode="src_in" + app:tint="@color/white" + tools:ignore="ContentDescription" /> + android:id="@+id/player_ffwd_holder" + android:layout_width="0dp" + 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_constraintWidth_percent="0.5"> + android:id="@+id/exo_ffwd_text" + android:layout_width="200dp" + android:layout_height="40dp" + android:layout_gravity="center" + android:gravity="center" + android:textColor="@color/white" + android:textSize="19sp" + android:textStyle="bold" + tools:text="10" /> + android:background="@drawable/video_tap_button_skip" + android:nextFocusRight="@id/exo_rew" + android:nextFocusUp="@id/player_go_back" + android:nextFocusDown="@id/player_lock" + android:padding="10dp" + android:scaleType="fitCenter" + android:src="@drawable/netflix_skip_forward" + android:tintMode="src_in" + app:tint="@color/white" + tools:ignore="ContentDescription" /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:layout_marginBottom="20dp" + android:gravity="center" + android:orientation="horizontal" + android:paddingTop="4dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent"> + android:id="@id/exo_prev" + style="@style/ExoMediaButton.Previous" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_repeat_toggle" + style="@style/ExoMediaButton" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_next" + style="@style/ExoMediaButton.Next" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_vr" + style="@style/ExoMediaButton.VR" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_play" + android:layout_width="0dp" + android:layout_height="0dp" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@id/exo_pause" + android:layout_width="0dp" + android:layout_height="0dp" + android:tintMode="src_in" + app:tint="?attr/colorPrimaryDark" + tools:ignore="ContentDescription" /> + android:id="@+id/player_open_source" + 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:tint="@color/white" /> + android:id="@+id/bottom_player_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="10dp" + android:gravity="center_vertical" + android:orientation="horizontal" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent"> + android:id="@+id/player_video_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="1" + android:orientation="horizontal"> + android:id="@id/exo_position" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginStart="20dp" + android:gravity="end" + android:includeFontPadding="false" + android:minWidth="50dp" + android:paddingLeft="4dp" + android:paddingRight="4dp" + android:textColor="@android:color/white" + android:textSize="14sp" + android:textStyle="normal" + tools:text="15:30" /> + app:scrubber_color="?attr/colorPrimary" + app:scrubber_dragged_size="26dp" + app:scrubber_enabled_size="24dp" + app:unplayed_color="@color/videoProgress" /> + android:id="@id/exo_duration" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_marginEnd="20dp" + android:includeFontPadding="false" + android:minWidth="50dp" + android:paddingLeft="4dp" + android:paddingRight="4dp" + android:textColor="@android:color/white" + android:textSize="14sp" + android:textStyle="normal" + tools:text="23:20" /> + android:id="@+id/player_fullscreen" + android:layout_width="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" /> + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal"> + android:id="@+id/player_progressbar_left_holder" + android:layout_width="100dp" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:gravity="start" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@+id/centerMenuView" + app:layout_constraintTop_toTopOf="parent" + tools:alpha="1" + tools:visibility="visible"> + android:id="@+id/player_progressbar_left_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_marginBottom="220dp" + android:src="@drawable/ic_baseline_volume_up_24" + app:tint="@android:color/white" + tools:ignore="ContentDescription"> + android:id="@+id/player_progressbar_left" + 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:id="@+id/player_progressbar_right_holder" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:gravity="right" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toRightOf="@+id/centerMenuView" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:alpha="1" + tools:ignore="RtlHardcoded" + tools:visibility="visible"> + android:id="@+id/player_progressbar_right_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_marginBottom="220dp" + android:src="@drawable/ic_baseline_brightness_7_24" + app:tint="@android:color/white" + tools:ignore="ContentDescription"> + android:id="@+id/player_progressbar_right" + style="@android:style/Widget.Material.ProgressBar.Horizontal" + android:layout_width="4dp" + android:layout_height="150dp" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:layout_gravity="end|center_vertical" + android:layout_marginEnd="40dp" + android:indeterminate="false" + android:max="100" + android:progress="100" + android:progressDrawable="@drawable/progress_drawable_vertical" /> diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 94d489fd..6ae2fa04 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -522,13 +522,6 @@ app:exitAnim="@anim/exit_anim" app:popEnterAnim="@anim/enter_anim" app:popExitAnim="@anim/exit_anim" /> - Web Video Cast Browser App not found + All Languages diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 70ac7516..13089cc2 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -81,6 +81,9 @@ @color/transparent @color/chip_color_text @color/chip_color_text + @color/chip_color_text + @font/google_sans + @font/google_sans