mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Merge branch 'recloudstream:master' into githubAccount
This commit is contained in:
commit
3f4d28b7df
44 changed files with 2131 additions and 1650 deletions
76
.github/workflows/build_to_archive.yml
vendored
Normal file
76
.github/workflows/build_to_archive.yml
vendored
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
name: Archive build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
paths-ignore:
|
||||||
|
- '*.md'
|
||||||
|
- '*.json'
|
||||||
|
- '**/wcokey.txt'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: "Archive-build"
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Generate access token
|
||||||
|
id: generate_token
|
||||||
|
uses: tibdex/github-app-token@v1
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
repository: "recloudstream/secrets"
|
||||||
|
- name: Generate access token (archive)
|
||||||
|
id: generate_archive_token
|
||||||
|
uses: tibdex/github-app-token@v1
|
||||||
|
with:
|
||||||
|
app_id: ${{ secrets.GH_APP_ID }}
|
||||||
|
private_key: ${{ secrets.GH_APP_KEY }}
|
||||||
|
repository: "recloudstream/cloudstream-archive"
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up JDK 11
|
||||||
|
uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
java-version: '11'
|
||||||
|
distribution: 'adopt'
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
- name: Fetch keystore
|
||||||
|
id: fetch_keystore
|
||||||
|
run: |
|
||||||
|
TMP_KEYSTORE_FILE_PATH="${RUNNER_TEMP}"/keystore
|
||||||
|
mkdir -p "${TMP_KEYSTORE_FILE_PATH}"
|
||||||
|
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "${TMP_KEYSTORE_FILE_PATH}/prerelease_keystore.keystore" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore.jks"
|
||||||
|
curl -H "Authorization: token ${{ steps.generate_token.outputs.token }}" -o "keystore_password.txt" "https://raw.githubusercontent.com/recloudstream/secrets/master/keystore_password.txt"
|
||||||
|
KEY_PWD="$(cat keystore_password.txt)"
|
||||||
|
echo "::add-mask::${KEY_PWD}"
|
||||||
|
echo "key_pwd=$KEY_PWD" >> $GITHUB_OUTPUT
|
||||||
|
- name: Run Gradle
|
||||||
|
run: |
|
||||||
|
./gradlew assemblePrerelease
|
||||||
|
env:
|
||||||
|
SIGNING_KEY_ALIAS: "key0"
|
||||||
|
SIGNING_KEY_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
|
SIGNING_STORE_PASSWORD: ${{ steps.fetch_keystore.outputs.key_pwd }}
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: "recloudstream/cloudstream-archive"
|
||||||
|
token: ${{ steps.generate_archive_token.outputs.token }}
|
||||||
|
path: "archive"
|
||||||
|
|
||||||
|
- name: Move build
|
||||||
|
run: |
|
||||||
|
cp app/build/outputs/apk/prerelease/release/*.apk "archive/$(git rev-parse --short HEAD).apk"
|
||||||
|
|
||||||
|
- name: Push archive
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE/archive
|
||||||
|
git config --local user.email "actions@github.com"
|
||||||
|
git config --local user.name "GitHub Actions"
|
||||||
|
git add .
|
||||||
|
git commit --amend -m "Build $GITHUB_SHA" || exit 0 # do not error if nothing to commit
|
||||||
|
git push --force
|
|
@ -155,10 +155,10 @@ dependencies {
|
||||||
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
|
// implementation("androidx.leanback:leanback-paging:1.1.0-alpha09")
|
||||||
|
|
||||||
// Exoplayer
|
// Exoplayer
|
||||||
implementation("com.google.android.exoplayer:exoplayer:2.16.1")
|
implementation("com.google.android.exoplayer:exoplayer:2.18.1")
|
||||||
implementation("com.google.android.exoplayer:extension-cast:2.16.1")
|
implementation("com.google.android.exoplayer:extension-cast:2.18.1")
|
||||||
implementation("com.google.android.exoplayer:extension-mediasession:2.16.1")
|
implementation("com.google.android.exoplayer:extension-mediasession:2.18.1")
|
||||||
implementation("com.google.android.exoplayer:extension-okhttp:2.16.1")
|
implementation("com.google.android.exoplayer:extension-okhttp:2.18.1")
|
||||||
|
|
||||||
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
|
//implementation("com.google.android.exoplayer:extension-leanback:2.14.0")
|
||||||
|
|
||||||
|
@ -176,7 +176,9 @@ dependencies {
|
||||||
implementation("com.jaredrummler:colorpicker:1.1.0")
|
implementation("com.jaredrummler:colorpicker:1.1.0")
|
||||||
|
|
||||||
//run JS
|
//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
|
// TorrentStream
|
||||||
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
|
//implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0")
|
||||||
|
@ -211,9 +213,9 @@ dependencies {
|
||||||
// slow af yt
|
// slow af yt
|
||||||
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
|
//implementation("com.github.HaarigerHarald:android-youtubeExtractor:master-SNAPSHOT")
|
||||||
|
|
||||||
// newpipe yt
|
// newpipe yt taken from https://github.com/TeamNewPipe/NewPipe/blob/dev/app/build.gradle#L190
|
||||||
implementation("com.github.TeamNewPipe:NewPipeExtractor:dev-SNAPSHOT")
|
implementation("com.github.TeamNewPipe:NewPipeExtractor:9ffdd0948b2ecd82655f5ff2a3e127b2b7695d5b")
|
||||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6")
|
||||||
|
|
||||||
// Library/extensions searching with Levenshtein distance
|
// Library/extensions searching with Levenshtein distance
|
||||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||||
|
|
|
@ -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.AppUtils.toJson
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
import com.lagradost.cloudstream3.utils.Coroutines.threadSafeListOf
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
@ -31,6 +30,12 @@ const val USER_AGENT =
|
||||||
val mapper = JsonMapper.builder().addModule(KotlinModule())
|
val mapper = JsonMapper.builder().addModule(KotlinModule())
|
||||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
|
.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 {
|
object APIHolder {
|
||||||
val unixTime: Long
|
val unixTime: Long
|
||||||
get() = System.currentTimeMillis() / 1000L
|
get() = System.currentTimeMillis() / 1000L
|
||||||
|
@ -159,7 +164,8 @@ object APIHolder {
|
||||||
|
|
||||||
val hashSet = HashSet<String>()
|
val hashSet = HashSet<String>()
|
||||||
val activeLangs = getApiProviderLangSettings()
|
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(
|
/*val set = settingsManager.getStringSet(
|
||||||
this.getString(R.string.search_providers_list_key),
|
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()
|
return list.filter { names.contains(it) }.map { DubStatus.valueOf(it) }.toHashSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets all the activated provider languages
|
|
||||||
* Used to obey the preference provider_lang_key
|
|
||||||
* but it turned out too complicated and unnecessary with extensions.
|
|
||||||
**/
|
|
||||||
fun Context.getApiProviderLangSettings(): HashSet<String> {
|
fun Context.getApiProviderLangSettings(): HashSet<String> {
|
||||||
val langs = apis.map { it.lang }.toSet()
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) }
|
val hashSet = hashSetOf(AllLanguagesName) // def is all languages
|
||||||
return langs.toHashSet()
|
|
||||||
|
|
||||||
// val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
|
||||||
// val hashSet = HashSet<String>()
|
|
||||||
// hashSet.add("en") // def is only en
|
// hashSet.add("en") // def is only en
|
||||||
// val list = settingsManager.getStringSet(
|
val list = settingsManager.getStringSet(
|
||||||
// this.getString(R.string.provider_lang_key),
|
this.getString(R.string.provider_lang_key),
|
||||||
// hashSet.toMutableSet()
|
hashSet
|
||||||
// )
|
)
|
||||||
//
|
|
||||||
// if (list.isNullOrEmpty()) return hashSet
|
if (list.isNullOrEmpty()) return hashSet
|
||||||
// return list.toHashSet()
|
return list.toHashSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Context.getApiTypeSettings(): HashSet<TvType> {
|
fun Context.getApiTypeSettings(): HashSet<TvType> {
|
||||||
|
@ -254,7 +251,8 @@ object APIHolder {
|
||||||
null
|
null
|
||||||
} ?: default
|
} ?: default
|
||||||
val langs = this.getApiProviderLangSettings()
|
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 }
|
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
|
||||||
return if (currentPrefMedia.isEmpty()) {
|
return if (currentPrefMedia.isEmpty()) {
|
||||||
allApis
|
allApis
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
package com.lagradost.cloudstream3.extractors
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.lagradost.cloudstream3.SubtitleFile
|
||||||
|
import com.lagradost.cloudstream3.app
|
||||||
|
import com.lagradost.cloudstream3.utils.*
|
||||||
|
import com.lagradost.cloudstream3.utils.AppUtils.tryParseJson
|
||||||
|
|
||||||
|
open class Jeniusplay : ExtractorApi() {
|
||||||
|
override val name = "Jeniusplay"
|
||||||
|
override val mainUrl = "https://jeniusplay.com"
|
||||||
|
override val requiresReferer = true
|
||||||
|
|
||||||
|
override suspend fun getUrl(
|
||||||
|
url: String,
|
||||||
|
referer: String?,
|
||||||
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
|
callback: (ExtractorLink) -> Unit
|
||||||
|
) {
|
||||||
|
val document = app.get(url, referer = "$mainUrl/").document
|
||||||
|
val hash = url.split("/").last().substringAfter("data=")
|
||||||
|
|
||||||
|
val m3uLink = app.post(
|
||||||
|
url = "$mainUrl/player/index.php?data=$hash&do=getVideo",
|
||||||
|
data = mapOf("hash" to hash, "r" to "$referer"),
|
||||||
|
referer = url,
|
||||||
|
headers = mapOf("X-Requested-With" to "XMLHttpRequest")
|
||||||
|
).parsed<ResponseSource>().videoSource
|
||||||
|
|
||||||
|
M3u8Helper.generateM3u8(
|
||||||
|
this.name,
|
||||||
|
m3uLink,
|
||||||
|
url,
|
||||||
|
).forEach(callback)
|
||||||
|
|
||||||
|
|
||||||
|
document.select("script").map { script ->
|
||||||
|
if (script.data().contains("eval(function(p,a,c,k,e,d)")) {
|
||||||
|
val subData =
|
||||||
|
getAndUnpack(script.data()).substringAfter("\"tracks\":[").substringBefore("],")
|
||||||
|
tryParseJson<List<Tracks>>("[$subData]")?.map { subtitle ->
|
||||||
|
subtitleCallback.invoke(
|
||||||
|
SubtitleFile(
|
||||||
|
getLanguage(subtitle.label ?: ""),
|
||||||
|
subtitle.file
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLanguage(str: String): String {
|
||||||
|
return when {
|
||||||
|
str.contains("indonesia", true) || str
|
||||||
|
.contains("bahasa", true) -> "Indonesian"
|
||||||
|
else -> str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ResponseSource(
|
||||||
|
@JsonProperty("hls") val hls: Boolean,
|
||||||
|
@JsonProperty("videoSource") val videoSource: String,
|
||||||
|
@JsonProperty("securedLink") val securedLink: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Tracks(
|
||||||
|
@JsonProperty("kind") val kind: String?,
|
||||||
|
@JsonProperty("file") val file: String,
|
||||||
|
@JsonProperty("label") val label: String?,
|
||||||
|
)
|
||||||
|
}
|
|
@ -6,7 +6,11 @@ import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
import com.lagradost.cloudstream3.utils.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.M3u8Helper
|
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 name = "Moviehab"
|
||||||
override var mainUrl = "https://play.moviehab.com"
|
override var mainUrl = "https://play.moviehab.com"
|
||||||
override val requiresReferer = false
|
override val requiresReferer = false
|
||||||
|
|
|
@ -46,6 +46,7 @@ open class YoutubeExtractor : ExtractorApi() {
|
||||||
subtitleCallback: (SubtitleFile) -> Unit,
|
subtitleCallback: (SubtitleFile) -> Unit,
|
||||||
callback: (ExtractorLink) -> Unit
|
callback: (ExtractorLink) -> Unit
|
||||||
) {
|
) {
|
||||||
|
println("TRYING TO ExTRACT: $url")
|
||||||
if (ytVideos[url].isNullOrEmpty()) {
|
if (ytVideos[url].isNullOrEmpty()) {
|
||||||
val link =
|
val link =
|
||||||
YoutubeStreamLinkHandlerFactory.getInstance().fromUrl(
|
YoutubeStreamLinkHandlerFactory.getInstance().fromUrl(
|
||||||
|
|
|
@ -1,40 +1,39 @@
|
||||||
package com.lagradost.cloudstream3.plugins
|
package com.lagradost.cloudstream3.plugins
|
||||||
|
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import dalvik.system.PathClassLoader
|
import android.content.Context
|
||||||
import com.google.gson.Gson
|
|
||||||
import android.content.res.AssetManager
|
import android.content.res.AssetManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.os.Environment
|
|
||||||
import android.widget.Toast
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty
|
import com.fasterxml.jackson.annotation.JsonProperty
|
||||||
|
import com.google.gson.Gson
|
||||||
import com.lagradost.cloudstream3.*
|
import com.lagradost.cloudstream3.*
|
||||||
|
import com.lagradost.cloudstream3.APIHolder.removePluginMapping
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
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.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.MainActivity.Companion.afterPluginsLoadedEvent
|
||||||
import com.lagradost.cloudstream3.mvvm.debugPrint
|
import com.lagradost.cloudstream3.mvvm.debugPrint
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
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.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.Coroutines.main
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorApi
|
import com.lagradost.cloudstream3.utils.ExtractorApi
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
||||||
import com.lagradost.cloudstream3.utils.extractorApis
|
import com.lagradost.cloudstream3.utils.extractorApis
|
||||||
|
import dalvik.system.PathClassLoader
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -217,7 +216,7 @@ object PluginManager {
|
||||||
* 3. If outdated download and load the plugin
|
* 3. If outdated download and load the plugin
|
||||||
* 4. Else load the plugin normally
|
* 4. Else load the plugin normally
|
||||||
**/
|
**/
|
||||||
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) = ioSafe {
|
fun updateAllOnlinePluginsAndLoadThem(activity: Activity) {
|
||||||
// Load all plugins as fast as possible!
|
// Load all plugins as fast as possible!
|
||||||
loadAllOnlinePlugins(activity)
|
loadAllOnlinePlugins(activity)
|
||||||
|
|
||||||
|
@ -227,7 +226,7 @@ object PluginManager {
|
||||||
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
|
val urls = (getKey<Array<RepositoryData>>(REPOSITORIES_KEY)
|
||||||
?: emptyArray()) + PREBUILT_REPOSITORIES
|
?: emptyArray()) + PREBUILT_REPOSITORIES
|
||||||
|
|
||||||
val onlinePlugins = urls.toList().amap {
|
val onlinePlugins = urls.toList().apmap {
|
||||||
getRepoPlugins(it.url)?.toList() ?: emptyList()
|
getRepoPlugins(it.url)?.toList() ?: emptyList()
|
||||||
}.flatten().distinctBy { it.second.url }
|
}.flatten().distinctBy { it.second.url }
|
||||||
|
|
||||||
|
@ -248,7 +247,7 @@ object PluginManager {
|
||||||
|
|
||||||
val updatedPlugins = mutableListOf<String>()
|
val updatedPlugins = mutableListOf<String>()
|
||||||
|
|
||||||
outdatedPlugins.amap { pluginData ->
|
outdatedPlugins.apmap { pluginData ->
|
||||||
if (pluginData.isDisabled) {
|
if (pluginData.isDisabled) {
|
||||||
//updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name))
|
//updatedPlugins.add(activity.getString(R.string.single_plugin_disabled, pluginData.onlineData.second.name))
|
||||||
unloadPlugin(pluginData.savedData.filePath)
|
unloadPlugin(pluginData.savedData.filePath)
|
||||||
|
@ -279,9 +278,9 @@ object PluginManager {
|
||||||
/**
|
/**
|
||||||
* Use updateAllOnlinePluginsAndLoadThem
|
* Use updateAllOnlinePluginsAndLoadThem
|
||||||
* */
|
* */
|
||||||
fun loadAllOnlinePlugins(activity: Activity) = ioSafe {
|
fun loadAllOnlinePlugins(activity: Activity) {
|
||||||
// Load all plugins as fast as possible!
|
// Load all plugins as fast as possible!
|
||||||
(getPluginsOnline()).toList().amap { pluginData ->
|
(getPluginsOnline()).toList().apmap { pluginData ->
|
||||||
loadPlugin(
|
loadPlugin(
|
||||||
activity,
|
activity,
|
||||||
File(pluginData.filePath),
|
File(pluginData.filePath),
|
||||||
|
@ -290,7 +289,7 @@ object PluginManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAllLocalPlugins(activity: Activity) = ioSafe {
|
fun loadAllLocalPlugins(activity: Activity) {
|
||||||
val dir = File(LOCAL_PLUGINS_PATH)
|
val dir = File(LOCAL_PLUGINS_PATH)
|
||||||
removeKey(PLUGINS_KEY_LOCAL)
|
removeKey(PLUGINS_KEY_LOCAL)
|
||||||
|
|
||||||
|
@ -298,7 +297,7 @@ object PluginManager {
|
||||||
val res = dir.mkdirs()
|
val res = dir.mkdirs()
|
||||||
if (!res) {
|
if (!res) {
|
||||||
Log.w(TAG, "Failed to create local directories")
|
Log.w(TAG, "Failed to create local directories")
|
||||||
return@ioSafe
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,6 +70,28 @@ object RepositoryManager {
|
||||||
getKey("PREBUILT_REPOSITORIES") ?: emptyArray()
|
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? {
|
suspend fun parseRepository(url: String): Repository? {
|
||||||
return suspendSafeApiCall {
|
return suspendSafeApiCall {
|
||||||
// Take manifestVersion and such into account later
|
// Take manifestVersion and such into account later
|
||||||
|
|
|
@ -150,7 +150,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
||||||
} else {
|
} else {
|
||||||
ChromecastSubtitlesFragment.getCurrentSavedStyle().apply {
|
ChromecastSubtitlesFragment.getCurrentSavedStyle().apply {
|
||||||
val font = TextTrackStyle()
|
val font = TextTrackStyle()
|
||||||
font.fontFamily = fontFamily ?: "Google Sans"
|
font.setFontFamily(fontFamily ?: "Google Sans")
|
||||||
fontGenericFamily?.let {
|
fontGenericFamily?.let {
|
||||||
font.fontGenericFamily = it
|
font.fontGenericFamily = it
|
||||||
}
|
}
|
||||||
|
@ -183,7 +183,9 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
||||||
val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl
|
val contentUrl = (remoteMediaClient?.currentItem?.media?.contentUrl
|
||||||
?: remoteMediaClient?.currentItem?.media?.contentId)
|
?: 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 sotringIndex = items.indexOfFirst { it.url == contentUrl }
|
||||||
|
|
||||||
val arrayAdapter =
|
val arrayAdapter =
|
||||||
|
@ -279,7 +281,7 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
||||||
val currentPosition = remoteMediaClient?.approximateStreamPosition
|
val currentPosition = remoteMediaClient?.approximateStreamPosition
|
||||||
if (currentDuration != null && currentPosition != null)
|
if (currentDuration != null && currentPosition != null)
|
||||||
DataStoreHelper.setViewPos(epData.id, currentPosition, currentDuration)
|
DataStoreHelper.setViewPos(epData.id, currentPosition, currentDuration)
|
||||||
} catch (t : Throwable) {
|
} catch (t: Throwable) {
|
||||||
logError(t)
|
logError(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -358,10 +360,8 @@ class SelectSourceController(val view: ImageView, val activity: ControllerActivi
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSessionConnected(castSession: CastSession?) {
|
override fun onSessionConnected(castSession: CastSession) {
|
||||||
castSession?.let {
|
super.onSessionConnected(castSession)
|
||||||
super.onSessionConnected(it)
|
|
||||||
}
|
|
||||||
remoteMediaClient?.queueSetRepeatMode(REPEAT_MODE_REPEAT_OFF, JSONObject())
|
remoteMediaClient?.queueSetRepeatMode(REPEAT_MODE_REPEAT_OFF, JSONObject())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,9 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
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.BottomSheetBehavior
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import com.google.android.material.button.MaterialButton
|
import com.google.android.material.button.MaterialButton
|
||||||
|
@ -346,7 +349,9 @@ class HomeFragment : Fragment() {
|
||||||
builder.setContentView(R.layout.home_select_mainpage)
|
builder.setContentView(R.layout.home_select_mainpage)
|
||||||
builder.show()
|
builder.show()
|
||||||
builder.let { dialog ->
|
builder.let { dialog ->
|
||||||
val isMultiLang = getApiProviderLangSettings().size > 1
|
val isMultiLang = getApiProviderLangSettings().let { set ->
|
||||||
|
set.size > 1 || set.contains(AllLanguagesName)
|
||||||
|
}
|
||||||
//dialog.window?.setGravity(Gravity.BOTTOM)
|
//dialog.window?.setGravity(Gravity.BOTTOM)
|
||||||
|
|
||||||
var currentApiName = selectedApiName
|
var currentApiName = selectedApiName
|
||||||
|
@ -548,7 +553,9 @@ class HomeFragment : Fragment() {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
home_preview?.isVisible = true
|
home_preview?.isVisible = true
|
||||||
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.apply {
|
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.apply {
|
||||||
setItems(preview.value)
|
if (!setItems(preview.value.second, preview.value.first)) {
|
||||||
|
home_preview_viewpager?.setCurrentItem(0, false)
|
||||||
|
}
|
||||||
// home_preview_viewpager?.setCurrentItem(1000, false)
|
// home_preview_viewpager?.setCurrentItem(1000, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -557,6 +564,10 @@ class HomeFragment : Fragment() {
|
||||||
//}
|
//}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
|
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.setItems(
|
||||||
|
listOf(),
|
||||||
|
false
|
||||||
|
)
|
||||||
home_preview?.isVisible = false
|
home_preview?.isVisible = false
|
||||||
context?.fixPaddingStatusbar(home_watch_holder)
|
context?.fixPaddingStatusbar(home_watch_holder)
|
||||||
}
|
}
|
||||||
|
@ -571,13 +582,26 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
home_preview_viewpager?.apply {
|
home_preview_viewpager?.apply {
|
||||||
setPageTransformer(false, HomeScrollTransformer())
|
setPageTransformer(HomeScrollTransformer())
|
||||||
adapter = HomeScrollAdapter { load ->
|
val callback: OnPageChangeCallback = object : OnPageChangeCallback() {
|
||||||
load.apply {
|
override fun onPageSelected(position: Int) {
|
||||||
home_preview_tags?.text = tags?.joinToString(" • ") ?: ""
|
(home_preview_viewpager?.adapter as? HomeScrollAdapter)?.apply {
|
||||||
home_preview_tags?.isGone = tags.isNullOrEmpty()
|
if (position >= itemCount - 1 && hasMoreItems) {
|
||||||
home_preview_image?.setImage(posterUrl, posterHeaders)
|
hasMoreItems = false // dont make two requests
|
||||||
home_preview_title?.text = name
|
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 {
|
home_preview_play?.setOnClickListener {
|
||||||
activity?.loadResult(url, apiName, START_ACTION_RESUME_LATEST)
|
activity?.loadResult(url, apiName, START_ACTION_RESUME_LATEST)
|
||||||
//activity.loadSearchResult(url, START_ACTION_RESUME_LATEST)
|
//activity.loadSearchResult(url, START_ACTION_RESUME_LATEST)
|
||||||
|
@ -587,7 +611,7 @@ class HomeFragment : Fragment() {
|
||||||
//activity.loadSearchResult(random)
|
//activity.loadSearchResult(random)
|
||||||
}
|
}
|
||||||
// very ugly code, but I dont care
|
// very ugly code, but I dont care
|
||||||
val watchType = DataStoreHelper.getResultWatchState(load.getId())
|
val watchType = DataStoreHelper.getResultWatchState(this.getId())
|
||||||
home_preview_bookmark?.setText(watchType.stringRes)
|
home_preview_bookmark?.setText(watchType.stringRes)
|
||||||
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
|
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
null,
|
null,
|
||||||
|
@ -597,29 +621,37 @@ class HomeFragment : Fragment() {
|
||||||
)
|
)
|
||||||
home_preview_bookmark?.setOnClickListener { fab ->
|
home_preview_bookmark?.setOnClickListener { fab ->
|
||||||
activity?.showBottomDialog(
|
activity?.showBottomDialog(
|
||||||
WatchType.values().map { fab.context.getString(it.stringRes) }
|
WatchType.values()
|
||||||
|
.map { fab.context.getString(it.stringRes) }
|
||||||
.toList(),
|
.toList(),
|
||||||
DataStoreHelper.getResultWatchState(load.getId()).ordinal,
|
DataStoreHelper.getResultWatchState(this.getId()).ordinal,
|
||||||
fab.context.getString(R.string.action_add_to_bookmarks),
|
fab.context.getString(R.string.action_add_to_bookmarks),
|
||||||
showApply = false,
|
showApply = false,
|
||||||
{}) {
|
{}) {
|
||||||
val newValue = WatchType.values()[it]
|
val newValue = WatchType.values()[it]
|
||||||
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
|
home_preview_bookmark?.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
null,
|
null,
|
||||||
getDrawable(home_preview_bookmark.context, newValue.iconRes),
|
getDrawable(
|
||||||
|
home_preview_bookmark.context,
|
||||||
|
newValue.iconRes
|
||||||
|
),
|
||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
)
|
)
|
||||||
home_preview_bookmark?.setText(newValue.stringRes)
|
home_preview_bookmark?.setText(newValue.stringRes)
|
||||||
|
|
||||||
updateWatchStatus(load, newValue)
|
updateWatchStatus(this, newValue)
|
||||||
reloadStored()
|
reloadStored()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
registerOnPageChangeCallback(callback)
|
||||||
|
adapter = HomeScrollAdapter()
|
||||||
|
}
|
||||||
|
|
||||||
observe(homeViewModel.apiName) { apiName ->
|
observe(homeViewModel.apiName) { apiName ->
|
||||||
currentApiName = apiName
|
currentApiName = apiName
|
||||||
|
|
|
@ -1,60 +1,89 @@
|
||||||
package com.lagradost.cloudstream3.ui.home
|
package com.lagradost.cloudstream3.ui.home
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.ImageView
|
import androidx.core.view.isGone
|
||||||
import androidx.viewpager.widget.PagerAdapter
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.lagradost.cloudstream3.LoadResponse
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
|
import kotlinx.android.synthetic.main.home_scroll_view.view.*
|
||||||
|
|
||||||
|
|
||||||
class HomeScrollAdapter(private val onPrimaryCallback: (LoadResponse) -> Unit) : PagerAdapter() {
|
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
private var items: List<LoadResponse> = listOf()
|
private var items: MutableList<LoadResponse> = mutableListOf()
|
||||||
|
var hasMoreItems: Boolean = false
|
||||||
|
|
||||||
fun setItems(newItems: List<LoadResponse>) {
|
fun getItem(position: Int) : LoadResponse? {
|
||||||
items = newItems
|
return items.getOrNull(position)
|
||||||
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCount(): Int {
|
fun setItems(newItems: List<LoadResponse>, hasNext: Boolean): Boolean {
|
||||||
return Int.MAX_VALUE//items.size
|
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 {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
return POSITION_NONE//super.getItemPosition(`object`)
|
return CardViewHolder(
|
||||||
|
LayoutInflater.from(parent.context).inflate(R.layout.home_scroll_view, parent, false),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getItemAtPosition(idx: Int): LoadResponse {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
return items[idx % items.size]
|
when (holder) {
|
||||||
|
is CardViewHolder -> {
|
||||||
|
holder.bind(items[position])
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setPrimaryItem(container: ViewGroup, position: Int, `object`: Any) {
|
class CardViewHolder
|
||||||
super.setPrimaryItem(container, position, `object`)
|
constructor(
|
||||||
onPrimaryCallback.invoke(getItemAtPosition(position))
|
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 {
|
class HomeScrollDiffCallback(
|
||||||
val image = ImageView(container.context)
|
private val oldList: List<LoadResponse>,
|
||||||
val item = getItemAtPosition(position)
|
private val newList: List<LoadResponse>
|
||||||
image.scaleType = ImageView.ScaleType.CENTER_CROP
|
) :
|
||||||
image.setImage(item.posterUrl ?: item.backgroundPosterUrl, item.posterHeaders)
|
DiffUtil.Callback() {
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
oldList[oldItemPosition].url == newList[newItemPosition].url
|
||||||
|
|
||||||
// val itemView: View = mLayoutInflater.inflate(R.layout.pager_item, container, false)
|
override fun getOldListSize() = oldList.size
|
||||||
|
|
||||||
// val imageView: ImageView = itemView.findViewById<View>(R.id.imageView) as ImageView
|
override fun getNewListSize() = newList.size
|
||||||
// imageView.setImageResource(mResources.get(position))
|
|
||||||
|
|
||||||
container.addView(image)
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
||||||
|
oldList[oldItemPosition] == newList[newItemPosition]
|
||||||
return image
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun destroyItem(container: ViewGroup, position: Int, `object`: Any) {
|
override fun getItemCount(): Int {
|
||||||
container.removeView(`object` as View)
|
return items.size
|
||||||
}
|
|
||||||
|
|
||||||
override fun isViewFromObject(view: View, `object`: Any): Boolean {
|
|
||||||
return view === `object`
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,13 +1,23 @@
|
||||||
package com.lagradost.cloudstream3.ui.home
|
package com.lagradost.cloudstream3.ui.home
|
||||||
|
|
||||||
import android.view.View
|
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) {
|
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(
|
page.setPadding(
|
||||||
maxOf(0, (-position * page.width / 2).toInt()), 0,
|
padding, 0,
|
||||||
maxOf(0, (position * page.width / 2).toInt()), 0
|
-padding, 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.apis
|
import com.lagradost.cloudstream3.APIHolder.apis
|
||||||
import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality
|
import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality
|
||||||
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
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.context
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
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.mvvm.*
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
|
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
|
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
|
||||||
import com.lagradost.cloudstream3.ui.WatchType
|
import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
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.DOWNLOAD_HEADER_CACHE
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
|
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 com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.collections.set
|
import kotlin.collections.set
|
||||||
|
@ -49,6 +44,8 @@ class HomeViewModel : ViewModel() {
|
||||||
private val _randomItems = MutableLiveData<List<SearchResponse>?>(null)
|
private val _randomItems = MutableLiveData<List<SearchResponse>?>(null)
|
||||||
val randomItems: LiveData<List<SearchResponse>?> = _randomItems
|
val randomItems: LiveData<List<SearchResponse>?> = _randomItems
|
||||||
|
|
||||||
|
private var currentShuffledList: List<SearchResponse> = listOf()
|
||||||
|
|
||||||
private fun autoloadRepo(): APIRepository {
|
private fun autoloadRepo(): APIRepository {
|
||||||
return APIRepository(apis.first { it.hasMainPage })
|
return APIRepository(apis.first { it.hasMainPage })
|
||||||
}
|
}
|
||||||
|
@ -61,9 +58,12 @@ class HomeViewModel : ViewModel() {
|
||||||
val bookmarks: LiveData<Pair<Boolean, List<SearchResponse>>> = _bookmarks
|
val bookmarks: LiveData<Pair<Boolean, List<SearchResponse>>> = _bookmarks
|
||||||
|
|
||||||
private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
|
private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
|
||||||
private val _preview = MutableLiveData<Resource<List<LoadResponse>>>()
|
private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>()
|
||||||
|
private val previewResponses = mutableListOf<LoadResponse>()
|
||||||
|
private val previewResponsesAdded = mutableSetOf<String>()
|
||||||
|
|
||||||
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
|
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
|
||||||
val preview: LiveData<Resource<List<LoadResponse>>> = _preview
|
val preview: LiveData<Resource<Pair<Boolean, List<LoadResponse>>>> = _preview
|
||||||
|
|
||||||
fun loadResumeWatching() = viewModelScope.launchSafe {
|
fun loadResumeWatching() = viewModelScope.launchSafe {
|
||||||
val resumeWatching = withContext(Dispatchers.IO) {
|
val resumeWatching = withContext(Dispatchers.IO) {
|
||||||
|
@ -212,6 +212,40 @@ class HomeViewModel : ViewModel() {
|
||||||
expandAndReturn(name)
|
expandAndReturn(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// returns the amount of items added and modifies current
|
||||||
|
private suspend fun updatePreviewResponses(
|
||||||
|
current: MutableList<LoadResponse>,
|
||||||
|
alreadyAdded: MutableSet<String>,
|
||||||
|
shuffledList: List<SearchResponse>,
|
||||||
|
size: Int
|
||||||
|
): Int {
|
||||||
|
var count = 0
|
||||||
|
|
||||||
|
val addItems = arrayListOf<SearchResponse>()
|
||||||
|
for (searchResponse in shuffledList) {
|
||||||
|
if (!alreadyAdded.contains(searchResponse.url)) {
|
||||||
|
addItems.add(searchResponse)
|
||||||
|
previewResponsesAdded.add(searchResponse.url)
|
||||||
|
if (++count >= size) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val add = addItems.amap { searchResponse ->
|
||||||
|
repo?.load(searchResponse.url)
|
||||||
|
}.mapNotNull { if (it != null && it is Resource.Success) it.value else null }
|
||||||
|
current.addAll(add)
|
||||||
|
return add.size
|
||||||
|
}
|
||||||
|
|
||||||
|
private var addJob: Job? = null
|
||||||
|
fun loadMoreHomeScrollResponses() {
|
||||||
|
addJob = ioSafe {
|
||||||
|
updatePreviewResponses(previewResponses, previewResponsesAdded, currentShuffledList, 1)
|
||||||
|
_preview.postValue(Resource.Success((previewResponsesAdded.size < currentShuffledList.size) to previewResponses))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun load(api: MainAPI?) = ioSafe {
|
private fun load(api: MainAPI?) = ioSafe {
|
||||||
repo = if (api != null) {
|
repo = if (api != null) {
|
||||||
|
@ -226,6 +260,7 @@ class HomeViewModel : ViewModel() {
|
||||||
if (repo?.hasMainPage == true) {
|
if (repo?.hasMainPage == true) {
|
||||||
_page.postValue(Resource.Loading())
|
_page.postValue(Resource.Loading())
|
||||||
_preview.postValue(Resource.Loading())
|
_preview.postValue(Resource.Loading())
|
||||||
|
addJob?.cancel()
|
||||||
|
|
||||||
when (val data = repo?.getMainPage(1, null)) {
|
when (val data = repo?.getMainPage(1, null)) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
|
@ -241,64 +276,10 @@ class HomeViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
val items = data.value.mapNotNull { it?.items }.flatten()
|
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()) {
|
previewResponses.clear()
|
||||||
_preview.postValue(
|
previewResponsesAdded.clear()
|
||||||
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))
|
|
||||||
|
|
||||||
|
|
||||||
//val home = data.value
|
//val home = data.value
|
||||||
if (items.isNotEmpty()) {
|
if (items.isNotEmpty()) {
|
||||||
|
@ -313,9 +294,30 @@ class HomeViewModel : ViewModel() {
|
||||||
context?.filterSearchResultByFilmQuality(currentList.shuffled())
|
context?.filterSearchResultByFilmQuality(currentList.shuffled())
|
||||||
?: currentList.shuffled()
|
?: currentList.shuffled()
|
||||||
|
|
||||||
|
updatePreviewResponses(
|
||||||
|
previewResponses,
|
||||||
|
previewResponsesAdded,
|
||||||
|
randomItems,
|
||||||
|
3
|
||||||
|
)
|
||||||
|
|
||||||
_randomItems.postValue(randomItems)
|
_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) {
|
} catch (e: Exception) {
|
||||||
_randomItems.postValue(emptyList())
|
_randomItems.postValue(emptyList())
|
||||||
logError(e)
|
logError(e)
|
||||||
|
|
|
@ -15,6 +15,7 @@ import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
|
||||||
import com.google.android.exoplayer2.source.*
|
import com.google.android.exoplayer2.source.*
|
||||||
import com.google.android.exoplayer2.text.TextRenderer
|
import com.google.android.exoplayer2.text.TextRenderer
|
||||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
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.trackselection.TrackSelector
|
||||||
import com.google.android.exoplayer2.ui.SubtitleView
|
import com.google.android.exoplayer2.ui.SubtitleView
|
||||||
import com.google.android.exoplayer2.upstream.*
|
import com.google.android.exoplayer2.upstream.*
|
||||||
|
@ -218,7 +219,43 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
var currentSubtitles: SubtitleData? = null
|
var currentSubtitles: SubtitleData? = null
|
||||||
|
|
||||||
override fun setMaxVideoSize(width: Int, height: Int) {
|
private fun List<Tracks.Group>.getTrack(id: String?): Pair<TrackGroup, Int>? {
|
||||||
|
if (id == null) return null
|
||||||
|
// This beast of an expression does:
|
||||||
|
// 1. Filter all audio tracks
|
||||||
|
// 2. Get all formats in said audio tacks
|
||||||
|
// 3. Gets all ids of the formats
|
||||||
|
// 4. Filters to find the first audio track with the same id as the audio track we are looking for
|
||||||
|
// 5. Returns the media group and the index of the audio track in the group
|
||||||
|
return this.firstNotNullOfOrNull { group ->
|
||||||
|
(0 until group.mediaTrackGroup.length).map {
|
||||||
|
group.getTrackFormat(it) to it
|
||||||
|
}.firstOrNull { it.first.id == id }
|
||||||
|
?.let { group.mediaTrackGroup to it.second }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setMaxVideoSize(width: Int, height: Int, id: String?) {
|
||||||
|
if (id != null) {
|
||||||
|
val videoTrack =
|
||||||
|
exoPlayer?.currentTracks?.groups?.filter { it.type == TRACK_TYPE_VIDEO }
|
||||||
|
?.getTrack(id)
|
||||||
|
|
||||||
|
if (videoTrack != null) {
|
||||||
|
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
|
||||||
|
?.buildUpon()
|
||||||
|
?.setOverrideForType(
|
||||||
|
TrackSelectionOverride(
|
||||||
|
videoTrack.first,
|
||||||
|
videoTrack.second
|
||||||
|
)
|
||||||
|
)
|
||||||
|
?.build()
|
||||||
|
?: return
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
|
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
|
||||||
?.buildUpon()
|
?.buildUpon()
|
||||||
?.setMaxVideoSize(width, height)
|
?.setMaxVideoSize(width, height)
|
||||||
|
@ -226,8 +263,29 @@ class CS3IPlayer : IPlayer {
|
||||||
?: return
|
?: return
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setPreferredAudioTrack(trackLanguage: String?) {
|
override fun setPreferredAudioTrack(trackLanguage: String?, id: String?) {
|
||||||
preferredAudioTrackLanguage = trackLanguage
|
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
|
exoPlayer?.trackSelectionParameters = exoPlayer?.trackSelectionParameters
|
||||||
?.buildUpon()
|
?.buildUpon()
|
||||||
?.setPreferredAudioLanguage(trackLanguage)
|
?.setPreferredAudioLanguage(trackLanguage)
|
||||||
|
@ -239,11 +297,11 @@ class CS3IPlayer : IPlayer {
|
||||||
/**
|
/**
|
||||||
* Gets all supported formats in a list
|
* Gets all supported formats in a list
|
||||||
* */
|
* */
|
||||||
private fun List<TracksInfo.TrackGroupInfo>.getFormats(): List<Format> {
|
private fun List<Tracks.Group>.getFormats(): List<Pair<Format, Int>> {
|
||||||
return this.map {
|
return this.map {
|
||||||
(0 until it.trackGroup.length).mapNotNull { i ->
|
(0 until it.mediaTrackGroup.length).mapNotNull { i ->
|
||||||
if (it.isSupported)
|
if (it.isSupported)
|
||||||
it.trackGroup.getFormat(i) // to it.isSelected
|
it.mediaTrackGroup.getFormat(i) to i
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
}.flatten()
|
}.flatten()
|
||||||
|
@ -270,11 +328,12 @@ class CS3IPlayer : IPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getVideoTracks(): CurrentTracks {
|
override fun getVideoTracks(): CurrentTracks {
|
||||||
val allTracks = exoPlayer?.currentTracksInfo?.trackGroupInfos ?: emptyList()
|
val allTracks = exoPlayer?.currentTracks?.groups ?: emptyList()
|
||||||
val videoTracks = allTracks.filter { it.trackType == TRACK_TYPE_VIDEO }.getFormats()
|
val videoTracks = allTracks.filter { it.type == TRACK_TYPE_VIDEO }
|
||||||
.map { it.toVideoTrack() }
|
.getFormats()
|
||||||
val audioTracks = allTracks.filter { it.trackType == TRACK_TYPE_AUDIO }.getFormats()
|
.map { it.first.toVideoTrack() }
|
||||||
.map { it.toAudioTrack() }
|
val audioTracks = allTracks.filter { it.type == TRACK_TYPE_AUDIO }.getFormats()
|
||||||
|
.map { it.first.toAudioTrack() }
|
||||||
|
|
||||||
return CurrentTracks(
|
return CurrentTracks(
|
||||||
exoPlayer?.videoFormat?.toVideoTrack(),
|
exoPlayer?.videoFormat?.toVideoTrack(),
|
||||||
|
@ -611,7 +670,12 @@ class CS3IPlayer : IPlayer {
|
||||||
} else it
|
} else it
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
}
|
}
|
||||||
.setTrackSelector(trackSelector ?: getTrackSelector(context, maxVideoHeight))
|
.setTrackSelector(
|
||||||
|
trackSelector ?: getTrackSelector(
|
||||||
|
context,
|
||||||
|
maxVideoHeight
|
||||||
|
)
|
||||||
|
)
|
||||||
.setLoadControl(
|
.setLoadControl(
|
||||||
DefaultLoadControl.Builder()
|
DefaultLoadControl.Builder()
|
||||||
.setTargetBufferBytes(
|
.setTargetBufferBytes(
|
||||||
|
@ -781,10 +845,7 @@ class CS3IPlayer : IPlayer {
|
||||||
isPlaying = exo.isPlaying
|
isPlaying = exo.isPlaying
|
||||||
}
|
}
|
||||||
exoPlayer?.addListener(object : Player.Listener {
|
exoPlayer?.addListener(object : Player.Listener {
|
||||||
/**
|
override fun onTracksChanged(tracks: Tracks) {
|
||||||
* Records the current used subtitle/track. Needed as exoplayer seems to have loose track language selection.
|
|
||||||
* */
|
|
||||||
override fun onTracksInfoChanged(tracksInfo: TracksInfo) {
|
|
||||||
fun Format.isSubtitle(): Boolean {
|
fun Format.isSubtitle(): Boolean {
|
||||||
return this.sampleMimeType?.contains("video/") == false &&
|
return this.sampleMimeType?.contains("video/") == false &&
|
||||||
this.sampleMimeType?.contains("audio/") == false
|
this.sampleMimeType?.contains("audio/") == false
|
||||||
|
@ -792,17 +853,17 @@ class CS3IPlayer : IPlayer {
|
||||||
|
|
||||||
normalSafeApiCall {
|
normalSafeApiCall {
|
||||||
exoPlayerSelectedTracks =
|
exoPlayerSelectedTracks =
|
||||||
tracksInfo.trackGroupInfos.mapNotNull {
|
tracks.groups.mapNotNull {
|
||||||
val format = it.trackGroup.getFormat(0)
|
val format = it.mediaTrackGroup.getFormat(0)
|
||||||
if (format.isSubtitle())
|
if (format.isSubtitle())
|
||||||
format.language?.let { lang -> lang to it.isSelected }
|
format.language?.let { lang -> lang to it.isSelected }
|
||||||
else null
|
else null
|
||||||
}
|
}
|
||||||
|
|
||||||
val exoPlayerReportedTracks = tracksInfo.trackGroupInfos.mapNotNull {
|
val exoPlayerReportedTracks = tracks.groups.mapNotNull {
|
||||||
// Filter out unsupported tracks
|
// Filter out unsupported tracks
|
||||||
if (it.isSupported)
|
if (it.isSupported)
|
||||||
it.trackGroup.getFormat(0)
|
it.mediaTrackGroup.getFormat(0)
|
||||||
else
|
else
|
||||||
null
|
null
|
||||||
}.mapNotNull {
|
}.mapNotNull {
|
||||||
|
@ -827,7 +888,6 @@ class CS3IPlayer : IPlayer {
|
||||||
onTracksInfoChanged?.invoke()
|
onTracksInfoChanged?.invoke()
|
||||||
subtitlesUpdates?.invoke()
|
subtitlesUpdates?.invoke()
|
||||||
}
|
}
|
||||||
super.onTracksInfoChanged(tracksInfo)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||||
|
|
|
@ -796,15 +796,16 @@ class GeneratorPlayer : FullScreenPlayer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
tracksDialog.apply_btt?.setOnClickListener {
|
tracksDialog.apply_btt?.setOnClickListener {
|
||||||
|
val currentTrack = currentAudioTracks.getOrNull(audioIndexStart)
|
||||||
player.setPreferredAudioTrack(
|
player.setPreferredAudioTrack(
|
||||||
currentAudioTracks.getOrNull(audioIndexStart)?.language
|
currentTrack?.language, currentTrack?.id
|
||||||
)
|
)
|
||||||
|
|
||||||
val currentVideo = currentVideoTracks.getOrNull(videoIndex)
|
val currentVideo = currentVideoTracks.getOrNull(videoIndex)
|
||||||
val width = currentVideo?.width ?: NO_VALUE
|
val width = currentVideo?.width ?: NO_VALUE
|
||||||
val height = currentVideo?.height ?: NO_VALUE
|
val height = currentVideo?.height ?: NO_VALUE
|
||||||
if (width != NO_VALUE && height != NO_VALUE) {
|
if (width != NO_VALUE && height != NO_VALUE) {
|
||||||
player.setMaxVideoSize(width, height)
|
player.setMaxVideoSize(width, height, currentVideo?.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
tracksDialog.dismissSafe(activity)
|
tracksDialog.dismissSafe(activity)
|
||||||
|
|
|
@ -161,9 +161,9 @@ interface IPlayer {
|
||||||
|
|
||||||
fun getVideoTracks(): CurrentTracks
|
fun getVideoTracks(): CurrentTracks
|
||||||
|
|
||||||
/** If no parameters are set it'll default to no set size */
|
/** 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)
|
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 */
|
/** 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?)
|
fun setPreferredAudioTrack(trackLanguage: String?, id: String? = null)
|
||||||
}
|
}
|
|
@ -0,0 +1,423 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2016 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.lagradost.cloudstream3.ui.player;
|
||||||
|
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkNotNull;
|
||||||
|
import static com.google.android.exoplayer2.util.Assertions.checkState;
|
||||||
|
import static java.lang.annotation.ElementType.TYPE_USE;
|
||||||
|
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Handler.Callback;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.os.Message;
|
||||||
|
import androidx.annotation.IntDef;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.google.android.exoplayer2.BaseRenderer;
|
||||||
|
import com.google.android.exoplayer2.C;
|
||||||
|
import com.google.android.exoplayer2.Format;
|
||||||
|
import com.google.android.exoplayer2.FormatHolder;
|
||||||
|
import com.google.android.exoplayer2.RendererCapabilities;
|
||||||
|
import com.google.android.exoplayer2.source.SampleStream.ReadDataResult;
|
||||||
|
import com.google.android.exoplayer2.text.Cue;
|
||||||
|
import com.google.android.exoplayer2.text.CueGroup;
|
||||||
|
import com.google.android.exoplayer2.text.Subtitle;
|
||||||
|
import com.google.android.exoplayer2.text.SubtitleDecoder;
|
||||||
|
import com.google.android.exoplayer2.text.SubtitleDecoderException;
|
||||||
|
import com.google.android.exoplayer2.text.SubtitleDecoderFactory;
|
||||||
|
import com.google.android.exoplayer2.text.SubtitleInputBuffer;
|
||||||
|
import com.google.android.exoplayer2.text.SubtitleOutputBuffer;
|
||||||
|
import com.google.android.exoplayer2.text.TextOutput;
|
||||||
|
import com.google.android.exoplayer2.util.Log;
|
||||||
|
import com.google.android.exoplayer2.util.MimeTypes;
|
||||||
|
import com.google.android.exoplayer2.util.Util;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
// DO NOT CONVERT TO KOTLIN AUTOMATICALLY, IT FUCKS UP AND DOES NOT DISPLAY SUBS FOR SOME REASON
|
||||||
|
/**
|
||||||
|
* A renderer for text.
|
||||||
|
*
|
||||||
|
* <p>{@link Subtitle}s are decoded from sample data using {@link SubtitleDecoder} instances
|
||||||
|
* obtained from a {@link SubtitleDecoderFactory}. The actual rendering of the subtitle {@link Cue}s
|
||||||
|
* is delegated to a {@link TextOutput}.
|
||||||
|
*/
|
||||||
|
public class NonFinalTextRenderer extends BaseRenderer implements Callback {
|
||||||
|
|
||||||
|
private static final String TAG = "TextRenderer";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param trackType The track type that the renderer handles. One of the {@link C} {@code
|
||||||
|
* TRACK_TYPE_*} constants.
|
||||||
|
* @param outputHandler
|
||||||
|
*/
|
||||||
|
public NonFinalTextRenderer(int trackType, @Nullable Handler outputHandler) {
|
||||||
|
super(trackType);
|
||||||
|
this.outputHandler = outputHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
@Target(TYPE_USE)
|
||||||
|
@IntDef({
|
||||||
|
REPLACEMENT_STATE_NONE,
|
||||||
|
REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,
|
||||||
|
REPLACEMENT_STATE_WAIT_END_OF_STREAM
|
||||||
|
})
|
||||||
|
private @interface ReplacementState {}
|
||||||
|
/** The decoder does not need to be replaced. */
|
||||||
|
private static final int REPLACEMENT_STATE_NONE = 0;
|
||||||
|
/**
|
||||||
|
* The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing
|
||||||
|
* decoder. We need to do so in order to ensure that it outputs any remaining buffers before we
|
||||||
|
* release it.
|
||||||
|
*/
|
||||||
|
private static final int REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1;
|
||||||
|
/**
|
||||||
|
* The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder.
|
||||||
|
* We're waiting for the decoder to output an end of stream signal to indicate that it has output
|
||||||
|
* any remaining buffers before we release it.
|
||||||
|
*/
|
||||||
|
private static final int REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2;
|
||||||
|
|
||||||
|
private static final int MSG_UPDATE_OUTPUT = 0;
|
||||||
|
|
||||||
|
@Nullable private final Handler outputHandler;
|
||||||
|
private TextOutput output = null;
|
||||||
|
private SubtitleDecoderFactory decoderFactory = null;
|
||||||
|
private FormatHolder formatHolder = null;
|
||||||
|
|
||||||
|
private boolean inputStreamEnded;
|
||||||
|
private boolean outputStreamEnded;
|
||||||
|
private boolean waitingForKeyFrame;
|
||||||
|
private @ReplacementState int decoderReplacementState;
|
||||||
|
@Nullable private Format streamFormat;
|
||||||
|
@Nullable private SubtitleDecoder decoder;
|
||||||
|
@Nullable private SubtitleInputBuffer nextInputBuffer;
|
||||||
|
@Nullable private SubtitleOutputBuffer subtitle;
|
||||||
|
@Nullable private SubtitleOutputBuffer nextSubtitle;
|
||||||
|
private int nextSubtitleEventIndex;
|
||||||
|
private long finalStreamEndPositionUs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param output The output.
|
||||||
|
* @param outputLooper The looper associated with the thread on which the output should be called.
|
||||||
|
* If the output makes use of standard Android UI components, then this should normally be the
|
||||||
|
* looper associated with the application's main thread, which can be obtained using {@link
|
||||||
|
* android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
|
||||||
|
* directly on the player's internal rendering thread.
|
||||||
|
*/
|
||||||
|
public NonFinalTextRenderer(TextOutput output, @Nullable Looper outputLooper) {
|
||||||
|
this(output, outputLooper, SubtitleDecoderFactory.DEFAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param output The output.
|
||||||
|
* @param outputLooper The looper associated with the thread on which the output should be called.
|
||||||
|
* If the output makes use of standard Android UI components, then this should normally be the
|
||||||
|
* looper associated with the application's main thread, which can be obtained using {@link
|
||||||
|
* android.app.Activity#getMainLooper()}. Null may be passed if the output should be called
|
||||||
|
* directly on the player's internal rendering thread.
|
||||||
|
* @param decoderFactory A factory from which to obtain {@link SubtitleDecoder} instances.
|
||||||
|
*/
|
||||||
|
public NonFinalTextRenderer(
|
||||||
|
TextOutput output, @Nullable Looper outputLooper, SubtitleDecoderFactory decoderFactory) {
|
||||||
|
super(C.TRACK_TYPE_TEXT);
|
||||||
|
this.output = checkNotNull(output);
|
||||||
|
this.outputHandler =
|
||||||
|
outputLooper == null ? null : Util.createHandler(outputLooper, /* callback= */ this);
|
||||||
|
this.decoderFactory = decoderFactory;
|
||||||
|
formatHolder = new FormatHolder();
|
||||||
|
finalStreamEndPositionUs = C.TIME_UNSET;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return TAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Capabilities int supportsFormat(Format format) {
|
||||||
|
if (decoderFactory.supportsFormat(format)) {
|
||||||
|
return RendererCapabilities.create(
|
||||||
|
format.cryptoType == C.CRYPTO_TYPE_NONE ? C.FORMAT_HANDLED : C.FORMAT_UNSUPPORTED_DRM);
|
||||||
|
} else if (MimeTypes.isText(format.sampleMimeType)) {
|
||||||
|
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE);
|
||||||
|
} else {
|
||||||
|
return RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the position at which to stop rendering the current stream.
|
||||||
|
*
|
||||||
|
* <p>Must be called after {@link #setCurrentStreamFinal()}.
|
||||||
|
*
|
||||||
|
* @param streamEndPositionUs The position to stop rendering at or {@link C#LENGTH_UNSET} to
|
||||||
|
* render until the end of the current stream.
|
||||||
|
*/
|
||||||
|
// TODO(internal b/181312195): Remove this when it's no longer needed once subtitles are decoded
|
||||||
|
// on the loading side of SampleQueue.
|
||||||
|
public void setFinalStreamEndPositionUs(long streamEndPositionUs) {
|
||||||
|
checkState(isCurrentStreamFinal());
|
||||||
|
this.finalStreamEndPositionUs = streamEndPositionUs;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStreamChanged(Format[] formats, long startPositionUs, long offsetUs) {
|
||||||
|
streamFormat = formats[0];
|
||||||
|
if (decoder != null) {
|
||||||
|
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM;
|
||||||
|
} else {
|
||||||
|
initDecoder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPositionReset(long positionUs, boolean joining) {
|
||||||
|
clearOutput();
|
||||||
|
inputStreamEnded = false;
|
||||||
|
outputStreamEnded = false;
|
||||||
|
finalStreamEndPositionUs = C.TIME_UNSET;
|
||||||
|
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
|
||||||
|
replaceDecoder();
|
||||||
|
} else {
|
||||||
|
releaseBuffers();
|
||||||
|
checkNotNull(decoder).flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(long positionUs, long elapsedRealtimeUs) {
|
||||||
|
if (isCurrentStreamFinal()
|
||||||
|
&& finalStreamEndPositionUs != C.TIME_UNSET
|
||||||
|
&& positionUs >= finalStreamEndPositionUs) {
|
||||||
|
releaseBuffers();
|
||||||
|
outputStreamEnded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputStreamEnded) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextSubtitle == null) {
|
||||||
|
checkNotNull(decoder).setPositionUs(positionUs);
|
||||||
|
try {
|
||||||
|
nextSubtitle = checkNotNull(decoder).dequeueOutputBuffer();
|
||||||
|
} catch (SubtitleDecoderException e) {
|
||||||
|
handleDecoderError(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getState() != STATE_STARTED) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean textRendererNeedsUpdate = false;
|
||||||
|
if (subtitle != null) {
|
||||||
|
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
|
||||||
|
// advance to the next event.
|
||||||
|
long subtitleNextEventTimeUs = getNextEventTime();
|
||||||
|
while (subtitleNextEventTimeUs <= positionUs) {
|
||||||
|
nextSubtitleEventIndex++;
|
||||||
|
subtitleNextEventTimeUs = getNextEventTime();
|
||||||
|
textRendererNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nextSubtitle != null) {
|
||||||
|
SubtitleOutputBuffer nextSubtitle = this.nextSubtitle;
|
||||||
|
if (nextSubtitle.isEndOfStream()) {
|
||||||
|
if (!textRendererNeedsUpdate && getNextEventTime() == Long.MAX_VALUE) {
|
||||||
|
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
|
||||||
|
replaceDecoder();
|
||||||
|
} else {
|
||||||
|
releaseBuffers();
|
||||||
|
outputStreamEnded = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (nextSubtitle.timeUs <= positionUs) {
|
||||||
|
// Advance to the next subtitle. Sync the next event index and trigger an update.
|
||||||
|
if (subtitle != null) {
|
||||||
|
subtitle.release();
|
||||||
|
}
|
||||||
|
nextSubtitleEventIndex = nextSubtitle.getNextEventTimeIndex(positionUs);
|
||||||
|
subtitle = nextSubtitle;
|
||||||
|
this.nextSubtitle = null;
|
||||||
|
textRendererNeedsUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (textRendererNeedsUpdate) {
|
||||||
|
// If textRendererNeedsUpdate then subtitle must be non-null.
|
||||||
|
checkNotNull(subtitle);
|
||||||
|
// textRendererNeedsUpdate is set and we're playing. Update the renderer.
|
||||||
|
updateOutput(subtitle.getCues(positionUs));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (!inputStreamEnded) {
|
||||||
|
@Nullable SubtitleInputBuffer nextInputBuffer = this.nextInputBuffer;
|
||||||
|
if (nextInputBuffer == null) {
|
||||||
|
nextInputBuffer = checkNotNull(decoder).dequeueInputBuffer();
|
||||||
|
if (nextInputBuffer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.nextInputBuffer = nextInputBuffer;
|
||||||
|
}
|
||||||
|
if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {
|
||||||
|
nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM);
|
||||||
|
checkNotNull(decoder).queueInputBuffer(nextInputBuffer);
|
||||||
|
this.nextInputBuffer = null;
|
||||||
|
decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Try and read the next subtitle from the source.
|
||||||
|
@ReadDataResult int result = readSource(formatHolder, nextInputBuffer, /* readFlags= */ 0);
|
||||||
|
if (result == C.RESULT_BUFFER_READ) {
|
||||||
|
if (nextInputBuffer.isEndOfStream()) {
|
||||||
|
inputStreamEnded = true;
|
||||||
|
waitingForKeyFrame = false;
|
||||||
|
} else {
|
||||||
|
@Nullable Format format = formatHolder.format;
|
||||||
|
if (format == null) {
|
||||||
|
// We haven't received a format yet.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nextInputBuffer.subsampleOffsetUs = format.subsampleOffsetUs;
|
||||||
|
nextInputBuffer.flip();
|
||||||
|
waitingForKeyFrame &= !nextInputBuffer.isKeyFrame();
|
||||||
|
}
|
||||||
|
if (!waitingForKeyFrame) {
|
||||||
|
checkNotNull(decoder).queueInputBuffer(nextInputBuffer);
|
||||||
|
this.nextInputBuffer = null;
|
||||||
|
}
|
||||||
|
} else if (result == C.RESULT_NOTHING_READ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SubtitleDecoderException e) {
|
||||||
|
handleDecoderError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDisabled() {
|
||||||
|
streamFormat = null;
|
||||||
|
finalStreamEndPositionUs = C.TIME_UNSET;
|
||||||
|
clearOutput();
|
||||||
|
releaseDecoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnded() {
|
||||||
|
return outputStreamEnded;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
// Don't block playback whilst subtitles are loading.
|
||||||
|
// Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseBuffers() {
|
||||||
|
nextInputBuffer = null;
|
||||||
|
nextSubtitleEventIndex = C.INDEX_UNSET;
|
||||||
|
if (subtitle != null) {
|
||||||
|
subtitle.release();
|
||||||
|
subtitle = null;
|
||||||
|
}
|
||||||
|
if (nextSubtitle != null) {
|
||||||
|
nextSubtitle.release();
|
||||||
|
nextSubtitle = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseDecoder() {
|
||||||
|
releaseBuffers();
|
||||||
|
checkNotNull(decoder).release();
|
||||||
|
decoder = null;
|
||||||
|
decoderReplacementState = REPLACEMENT_STATE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initDecoder() {
|
||||||
|
waitingForKeyFrame = true;
|
||||||
|
decoder = decoderFactory.createDecoder(checkNotNull(streamFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaceDecoder() {
|
||||||
|
releaseDecoder();
|
||||||
|
initDecoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getNextEventTime() {
|
||||||
|
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
|
||||||
|
return Long.MAX_VALUE;
|
||||||
|
}
|
||||||
|
checkNotNull(subtitle);
|
||||||
|
return nextSubtitleEventIndex >= subtitle.getEventTimeCount()
|
||||||
|
? Long.MAX_VALUE
|
||||||
|
: subtitle.getEventTime(nextSubtitleEventIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateOutput(List<Cue> cues) {
|
||||||
|
if (outputHandler != null) {
|
||||||
|
outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget();
|
||||||
|
} else {
|
||||||
|
invokeUpdateOutputInternal(cues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearOutput() {
|
||||||
|
updateOutput(Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public boolean handleMessage(Message msg) {
|
||||||
|
switch (msg.what) {
|
||||||
|
case MSG_UPDATE_OUTPUT:
|
||||||
|
invokeUpdateOutputInternal((List<Cue>) msg.obj);
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void invokeUpdateOutputInternal(List<Cue> cues) {
|
||||||
|
output.onCues(cues);
|
||||||
|
output.onCues(new CueGroup(cues));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when {@link #decoder} throws an exception, so it can be logged and playback can
|
||||||
|
* continue.
|
||||||
|
*
|
||||||
|
* <p>Logs {@code e} and resets state to allow decoding the next sample.
|
||||||
|
*/
|
||||||
|
private void handleDecoderError(SubtitleDecoderException e) {
|
||||||
|
Log.e(TAG, "Subtitle decoding failed. streamFormat=" + streamFormat, e);
|
||||||
|
clearOutput();
|
||||||
|
replaceDecoder();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,382 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (C) 2016 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
package com.lagradost.cloudstream3.ui.player
|
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.os.Message
|
|
||||||
import androidx.annotation.IntDef
|
|
||||||
import com.google.android.exoplayer2.*
|
|
||||||
import com.google.android.exoplayer2.source.SampleStream.ReadDataResult
|
|
||||||
import com.google.android.exoplayer2.text.*
|
|
||||||
import com.google.android.exoplayer2.text.Cue.DIMEN_UNSET
|
|
||||||
import com.google.android.exoplayer2.text.Cue.LINE_TYPE_NUMBER
|
|
||||||
import com.google.android.exoplayer2.util.Assertions
|
|
||||||
import com.google.android.exoplayer2.util.Log
|
|
||||||
import com.google.android.exoplayer2.util.MimeTypes
|
|
||||||
import com.google.android.exoplayer2.util.Util
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A renderer for text.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* [Subtitle]s are decoded from sample data using [SubtitleDecoder] instances
|
|
||||||
* obtained from a [SubtitleDecoderFactory]. The actual rendering of the subtitle [Cue]s
|
|
||||||
* is delegated to a [TextOutput].
|
|
||||||
*/
|
|
||||||
open class NonFinalTextRenderer @JvmOverloads constructor(
|
|
||||||
output: TextOutput?,
|
|
||||||
outputLooper: Looper?,
|
|
||||||
private val decoderFactory: SubtitleDecoderFactory = SubtitleDecoderFactory.DEFAULT
|
|
||||||
) :
|
|
||||||
BaseRenderer(C.TRACK_TYPE_TEXT), Handler.Callback {
|
|
||||||
@MustBeDocumented
|
|
||||||
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
|
|
||||||
@IntDef(
|
|
||||||
REPLACEMENT_STATE_NONE,
|
|
||||||
REPLACEMENT_STATE_SIGNAL_END_OF_STREAM,
|
|
||||||
REPLACEMENT_STATE_WAIT_END_OF_STREAM
|
|
||||||
)
|
|
||||||
private annotation class ReplacementState
|
|
||||||
|
|
||||||
private val outputHandler: Handler? = if (outputLooper == null) null else Util.createHandler(
|
|
||||||
outputLooper, /* callback= */
|
|
||||||
this
|
|
||||||
)
|
|
||||||
private val output: TextOutput = Assertions.checkNotNull(output)
|
|
||||||
private val formatHold: FormatHolder = FormatHolder()
|
|
||||||
private var inputStreamEnded = false
|
|
||||||
private var outputStreamEnded = false
|
|
||||||
private var waitingForKeyFrame = false
|
|
||||||
|
|
||||||
@ReplacementState
|
|
||||||
private var decoderReplacementState = 0
|
|
||||||
private var streamFormat: Format? = null
|
|
||||||
private var decoder: SubtitleDecoder? = null
|
|
||||||
private var nextInputBuffer: SubtitleInputBuffer? = null
|
|
||||||
private var subtitle: SubtitleOutputBuffer? = null
|
|
||||||
private var nextSubtitle: SubtitleOutputBuffer? = null
|
|
||||||
private var nextSubtitleEventIndex = 0
|
|
||||||
private var finalStreamEndPositionUs: Long
|
|
||||||
override fun getName(): String {
|
|
||||||
return TAG
|
|
||||||
}
|
|
||||||
|
|
||||||
@RendererCapabilities.Capabilities
|
|
||||||
override fun supportsFormat(format: Format): Int {
|
|
||||||
return if (decoderFactory.supportsFormat(format)) {
|
|
||||||
RendererCapabilities.create(
|
|
||||||
if (format.cryptoType == C.CRYPTO_TYPE_NONE) C.FORMAT_HANDLED else C.FORMAT_UNSUPPORTED_DRM
|
|
||||||
)
|
|
||||||
} else if (MimeTypes.isText(format.sampleMimeType)) {
|
|
||||||
RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE)
|
|
||||||
} else {
|
|
||||||
RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the position at which to stop rendering the current stream.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Must be called after [.setCurrentStreamFinal].
|
|
||||||
*
|
|
||||||
* @param streamEndPositionUs The position to stop rendering at or [C.LENGTH_UNSET] to
|
|
||||||
* render until the end of the current stream.
|
|
||||||
*/
|
|
||||||
|
|
||||||
override fun onStreamChanged(formats: Array<Format>, startPositionUs: Long, offsetUs: Long) {
|
|
||||||
streamFormat = formats[0]
|
|
||||||
if (decoder != null) {
|
|
||||||
decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM
|
|
||||||
} else {
|
|
||||||
initDecoder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPositionReset(positionUs: Long, joining: Boolean) {
|
|
||||||
clearOutput()
|
|
||||||
inputStreamEnded = false
|
|
||||||
outputStreamEnded = false
|
|
||||||
finalStreamEndPositionUs = C.TIME_UNSET
|
|
||||||
if (decoderReplacementState != REPLACEMENT_STATE_NONE) {
|
|
||||||
replaceDecoder()
|
|
||||||
} else {
|
|
||||||
releaseBuffers()
|
|
||||||
Assertions.checkNotNull(decoder).flush()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun render(positionUs: Long, elapsedRealtimeUs: Long) {
|
|
||||||
if (isCurrentStreamFinal
|
|
||||||
&& finalStreamEndPositionUs != C.TIME_UNSET && positionUs >= finalStreamEndPositionUs
|
|
||||||
) {
|
|
||||||
releaseBuffers()
|
|
||||||
outputStreamEnded = true
|
|
||||||
}
|
|
||||||
if (outputStreamEnded) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (nextSubtitle == null) {
|
|
||||||
Assertions.checkNotNull(decoder).setPositionUs(positionUs)
|
|
||||||
nextSubtitle = try {
|
|
||||||
Assertions.checkNotNull(decoder).dequeueOutputBuffer()
|
|
||||||
} catch (e: SubtitleDecoderException) {
|
|
||||||
handleDecoderError(e)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (state != STATE_STARTED) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var textRendererNeedsUpdate = false
|
|
||||||
if (subtitle != null) {
|
|
||||||
// We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we
|
|
||||||
// advance to the next event.
|
|
||||||
var subtitleNextEventTimeUs = nextEventTime
|
|
||||||
while (subtitleNextEventTimeUs <= positionUs) {
|
|
||||||
nextSubtitleEventIndex++
|
|
||||||
subtitleNextEventTimeUs = nextEventTime
|
|
||||||
textRendererNeedsUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (nextSubtitle != null) {
|
|
||||||
val nextSubtitle = nextSubtitle
|
|
||||||
if (nextSubtitle!!.isEndOfStream) {
|
|
||||||
if (!textRendererNeedsUpdate && nextEventTime == Long.MAX_VALUE) {
|
|
||||||
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
|
|
||||||
replaceDecoder()
|
|
||||||
} else {
|
|
||||||
releaseBuffers()
|
|
||||||
outputStreamEnded = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (nextSubtitle.timeUs <= positionUs) {
|
|
||||||
// Advance to the next subtitle. Sync the next event index and trigger an update.
|
|
||||||
if (subtitle != null) {
|
|
||||||
subtitle!!.release()
|
|
||||||
}
|
|
||||||
nextSubtitleEventIndex = nextSubtitle.getNextEventTimeIndex(positionUs)
|
|
||||||
subtitle = nextSubtitle
|
|
||||||
this.nextSubtitle = null
|
|
||||||
textRendererNeedsUpdate = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (textRendererNeedsUpdate) {
|
|
||||||
// If textRendererNeedsUpdate then subtitle must be non-null.
|
|
||||||
Assertions.checkNotNull(subtitle)
|
|
||||||
// textRendererNeedsUpdate is set and we're playing. Update the renderer.
|
|
||||||
updateOutput(subtitle!!.getCues(positionUs))
|
|
||||||
}
|
|
||||||
if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
while (!inputStreamEnded) {
|
|
||||||
var nextInputBuffer = nextInputBuffer
|
|
||||||
if (nextInputBuffer == null) {
|
|
||||||
nextInputBuffer = Assertions.checkNotNull(decoder).dequeueInputBuffer()
|
|
||||||
if (nextInputBuffer == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.nextInputBuffer = nextInputBuffer
|
|
||||||
}
|
|
||||||
if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) {
|
|
||||||
nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM)
|
|
||||||
Assertions.checkNotNull(decoder).queueInputBuffer(nextInputBuffer)
|
|
||||||
this.nextInputBuffer = null
|
|
||||||
decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Try and read the next subtitle from the source.
|
|
||||||
@ReadDataResult val result =
|
|
||||||
readSource(formatHold, nextInputBuffer, /* readFlags= */0)
|
|
||||||
if (result == C.RESULT_BUFFER_READ) {
|
|
||||||
if (nextInputBuffer.isEndOfStream) {
|
|
||||||
inputStreamEnded = true
|
|
||||||
waitingForKeyFrame = false
|
|
||||||
} else {
|
|
||||||
val format = formatHold.format
|
|
||||||
?: // We haven't received a format yet.
|
|
||||||
return
|
|
||||||
nextInputBuffer.subsampleOffsetUs = format.subsampleOffsetUs
|
|
||||||
nextInputBuffer.flip()
|
|
||||||
waitingForKeyFrame = waitingForKeyFrame and !nextInputBuffer.isKeyFrame
|
|
||||||
}
|
|
||||||
if (!waitingForKeyFrame) {
|
|
||||||
Assertions.checkNotNull(decoder).queueInputBuffer(nextInputBuffer)
|
|
||||||
this.nextInputBuffer = null
|
|
||||||
}
|
|
||||||
} else if (result == C.RESULT_NOTHING_READ) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: SubtitleDecoderException) {
|
|
||||||
handleDecoderError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDisabled() {
|
|
||||||
streamFormat = null
|
|
||||||
finalStreamEndPositionUs = C.TIME_UNSET
|
|
||||||
clearOutput()
|
|
||||||
releaseDecoder()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isEnded(): Boolean {
|
|
||||||
return outputStreamEnded
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isReady(): Boolean {
|
|
||||||
// Don't block playback whilst subtitles are loading.
|
|
||||||
// Note: To change this behavior, it will be necessary to consider [Internal: b/12949941].
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun releaseBuffers() {
|
|
||||||
nextInputBuffer = null
|
|
||||||
nextSubtitleEventIndex = C.INDEX_UNSET
|
|
||||||
if (subtitle != null) {
|
|
||||||
subtitle!!.release()
|
|
||||||
subtitle = null
|
|
||||||
}
|
|
||||||
if (nextSubtitle != null) {
|
|
||||||
nextSubtitle!!.release()
|
|
||||||
nextSubtitle = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun releaseDecoder() {
|
|
||||||
releaseBuffers()
|
|
||||||
Assertions.checkNotNull(decoder).release()
|
|
||||||
decoder = null
|
|
||||||
decoderReplacementState = REPLACEMENT_STATE_NONE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initDecoder() {
|
|
||||||
waitingForKeyFrame = true
|
|
||||||
decoder = decoderFactory.createDecoder(Assertions.checkNotNull(streamFormat))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun replaceDecoder() {
|
|
||||||
releaseDecoder()
|
|
||||||
initDecoder()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val nextEventTime: Long
|
|
||||||
get() {
|
|
||||||
if (nextSubtitleEventIndex == C.INDEX_UNSET) {
|
|
||||||
return Long.MAX_VALUE
|
|
||||||
}
|
|
||||||
Assertions.checkNotNull(subtitle)
|
|
||||||
return if (nextSubtitleEventIndex >= subtitle!!.eventTimeCount) Long.MAX_VALUE else subtitle!!.getEventTime(
|
|
||||||
nextSubtitleEventIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateOutput(cues: List<Cue>) {
|
|
||||||
if (outputHandler != null) {
|
|
||||||
outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget()
|
|
||||||
} else {
|
|
||||||
invokeUpdateOutputInternal(cues)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearOutput() {
|
|
||||||
updateOutput(emptyList())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleMessage(msg: Message): Boolean {
|
|
||||||
return when (msg.what) {
|
|
||||||
MSG_UPDATE_OUTPUT -> {
|
|
||||||
invokeUpdateOutputInternal(msg.obj as List<Cue>)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun invokeUpdateOutputInternal(cues: List<Cue>) {
|
|
||||||
output.onCues(cues.map { cue ->
|
|
||||||
val builder = cue.buildUpon()
|
|
||||||
|
|
||||||
// See https://github.com/google/ExoPlayer/issues/7934
|
|
||||||
// SubripDecoder texts tend to be DIMEN_UNSET which pushes up the
|
|
||||||
// subs unlike WEBVTT which creates an inconsistency
|
|
||||||
if (cue.line == DIMEN_UNSET)
|
|
||||||
builder.setLine(-1f, LINE_TYPE_NUMBER)
|
|
||||||
|
|
||||||
// this fixes https://github.com/LagradOst/CloudStream-3/issues/717
|
|
||||||
builder.setSize(DIMEN_UNSET).build()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when [.decoder] throws an exception, so it can be logged and playback can
|
|
||||||
* continue.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* Logs `e` and resets state to allow decoding the next sample.
|
|
||||||
*/
|
|
||||||
private fun handleDecoderError(e: SubtitleDecoderException) {
|
|
||||||
Log.e(
|
|
||||||
TAG,
|
|
||||||
"Subtitle decoding failed. streamFormat=$streamFormat", e
|
|
||||||
)
|
|
||||||
clearOutput()
|
|
||||||
replaceDecoder()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "TextRenderer"
|
|
||||||
|
|
||||||
/** The decoder does not need to be replaced. */
|
|
||||||
private const val REPLACEMENT_STATE_NONE = 0
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing
|
|
||||||
* decoder. We need to do so in order to ensure that it outputs any remaining buffers before we
|
|
||||||
* release it.
|
|
||||||
*/
|
|
||||||
private const val REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder.
|
|
||||||
* We're waiting for the decoder to output an end of stream signal to indicate that it has output
|
|
||||||
* any remaining buffers before we release it.
|
|
||||||
*/
|
|
||||||
private const val REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2
|
|
||||||
private const val MSG_UPDATE_OUTPUT = 0
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @param output The output.
|
|
||||||
* @param outputLooper The looper associated with the thread on which the output should be called.
|
|
||||||
* If the output makes use of standard Android UI components, then this should normally be the
|
|
||||||
* looper associated with the application's main thread, which can be obtained using [ ][android.app.Activity.getMainLooper]. Null may be passed if the output should be called
|
|
||||||
* directly on the player's internal rendering thread.
|
|
||||||
* @param decoderFactory A factory from which to obtain [SubtitleDecoder] instances.
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* @param output The output.
|
|
||||||
* @param outputLooper The looper associated with the thread on which the output should be called.
|
|
||||||
* If the output makes use of standard Android UI components, then this should normally be the
|
|
||||||
* looper associated with the application's main thread, which can be obtained using [ ][android.app.Activity.getMainLooper]. Null may be passed if the output should be called
|
|
||||||
* directly on the player's internal rendering thread.
|
|
||||||
*/
|
|
||||||
init {
|
|
||||||
finalStreamEndPositionUs = C.TIME_UNSET
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -23,6 +23,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.discord.panels.OverlappingPanelsLayout
|
import com.discord.panels.OverlappingPanelsLayout
|
||||||
import com.google.android.material.chip.Chip
|
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.getApiDubstatusSettings
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
||||||
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
|
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_swipe.*
|
||||||
import kotlinx.android.synthetic.main.fragment_result_tv.*
|
import kotlinx.android.synthetic.main.fragment_result_tv.*
|
||||||
import kotlinx.android.synthetic.main.result_sync.*
|
import kotlinx.android.synthetic.main.result_sync.*
|
||||||
|
import kotlinx.android.synthetic.main.trailer_custom_layout.*
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
import com.google.android.material.chip.ChipDrawable
|
|
||||||
|
|
||||||
const val START_ACTION_RESUME_LATEST = 1
|
const val START_ACTION_RESUME_LATEST = 1
|
||||||
const val START_ACTION_LOAD_EP = 2
|
const val START_ACTION_LOAD_EP = 2
|
||||||
|
|
||||||
|
@ -839,6 +839,8 @@ open class ResultFragment : ResultTrailerPlayer() {
|
||||||
result_next_airing.setText(d.nextAiringEpisode)
|
result_next_airing.setText(d.nextAiringEpisode)
|
||||||
result_next_airing_time.setText(d.nextAiringDate)
|
result_next_airing_time.setText(d.nextAiringDate)
|
||||||
result_poster.setImage(d.posterImage)
|
result_poster.setImage(d.posterImage)
|
||||||
|
result_poster_background.setImage(d.posterBackgroundImage)
|
||||||
|
//result_trailer_thumbnail.setImage(d.posterBackgroundImage, fadeIn = false)
|
||||||
|
|
||||||
if (d.posterImage != null && !isTrueTvSettings())
|
if (d.posterImage != null && !isTrueTvSettings())
|
||||||
result_poster_holder?.setOnClickListener {
|
result_poster_holder?.setOnClickListener {
|
||||||
|
|
|
@ -5,6 +5,9 @@ import android.graphics.Rect
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import android.view.animation.AlphaAnimation
|
||||||
|
import android.view.animation.Animation
|
||||||
|
import android.view.animation.DecelerateInterpolator
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
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.Some
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.observe
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
import com.lagradost.cloudstream3.ui.WatchType
|
|
||||||
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
|
import com.lagradost.cloudstream3.ui.player.CSPlayerEvent
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
import com.lagradost.cloudstream3.ui.search.SearchAdapter
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
import com.lagradost.cloudstream3.ui.search.SearchHelper
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
|
||||||
import com.lagradost.cloudstream3.utils.ExtractorLink
|
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.showBottomDialog
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
|
||||||
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
import com.lagradost.cloudstream3.utils.UIHelper.popCurrentPage
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
|
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
|
||||||
import kotlinx.android.synthetic.main.fragment_result.*
|
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.*
|
||||||
|
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.fragment_trailer.*
|
||||||
import kotlinx.android.synthetic.main.result_recommendations.*
|
import kotlinx.android.synthetic.main.result_recommendations.*
|
||||||
|
import kotlinx.android.synthetic.main.result_recommendations.result_recommendations
|
||||||
import kotlinx.android.synthetic.main.trailer_custom_layout.*
|
import kotlinx.android.synthetic.main.trailer_custom_layout.*
|
||||||
|
|
||||||
|
|
||||||
class ResultFragmentPhone : ResultFragment() {
|
class ResultFragmentPhone : ResultFragment() {
|
||||||
var currentTrailers: List<ExtractorLink> = emptyList()
|
var currentTrailers: List<ExtractorLink> = emptyList()
|
||||||
var currentTrailerIndex = 0
|
var currentTrailerIndex = 0
|
||||||
|
@ -84,8 +93,36 @@ class ResultFragmentPhone : ResultFragment() {
|
||||||
} ?: run {
|
} ?: run {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
//result_trailer_thumbnail?.setImageBitmap(result_poster_background?.drawable?.toBitmap())
|
||||||
|
|
||||||
|
|
||||||
result_trailer_loading?.isVisible = isSuccess
|
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
|
// We don't want the trailer to be focusable if it's not visible
|
||||||
result_smallscreen_holder?.descendantFocusability = if (isSuccess) {
|
result_smallscreen_holder?.descendantFocusability = if (isSuccess) {
|
||||||
|
@ -129,7 +166,7 @@ class ResultFragmentPhone : ResultFragment() {
|
||||||
down.nextFocusUpId = upper.id
|
down.nextFocusUpId = upper.id
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectSeason : String? = null
|
var selectSeason: String? = null
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
val apiName = arguments?.getString(API_NAME_BUNDLE) ?: return
|
val apiName = arguments?.getString(API_NAME_BUNDLE) ?: return
|
||||||
|
@ -301,7 +338,8 @@ class ResultFragmentPhone : ResultFragment() {
|
||||||
observe(viewModel.selectedSeason) { text ->
|
observe(viewModel.selectedSeason) { text ->
|
||||||
result_season_button.setText(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 the season button is visible the result season button will be next focus down
|
||||||
if (result_season_button?.isVisible == true)
|
if (result_season_button?.isVisible == true)
|
||||||
if (result_resume_parent?.isVisible == true)
|
if (result_resume_parent?.isVisible == true)
|
||||||
|
@ -378,12 +416,16 @@ class ResultFragmentPhone : ResultFragment() {
|
||||||
r to (text?.asStringNull(ctx) ?: return@mapNotNull null)
|
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)
|
viewModel.changeSeason(names[itemId].first)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
//view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) ->
|
//view.popupMenuNoIconsAndNoStringRes(names.mapIndexed { index, (_, name) ->
|
||||||
// index to name
|
// index to name
|
||||||
//}) {
|
//}) {
|
||||||
|
|
|
@ -13,7 +13,9 @@ import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
import com.lagradost.cloudstream3.ui.player.SubtitleData
|
||||||
import com.lagradost.cloudstream3.utils.IOnBackPressed
|
import com.lagradost.cloudstream3.utils.IOnBackPressed
|
||||||
import kotlinx.android.synthetic.main.fragment_result.*
|
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_swipe.*
|
||||||
|
import kotlinx.android.synthetic.main.fragment_result_tv.*
|
||||||
import kotlinx.android.synthetic.main.fragment_trailer.*
|
import kotlinx.android.synthetic.main.fragment_trailer.*
|
||||||
import kotlinx.android.synthetic.main.trailer_custom_layout.*
|
import kotlinx.android.synthetic.main.trailer_custom_layout.*
|
||||||
|
|
||||||
|
|
|
@ -88,6 +88,7 @@ data class ResultData(
|
||||||
var syncData: Map<String, String>,
|
var syncData: Map<String, String>,
|
||||||
|
|
||||||
val posterImage: UiImage?,
|
val posterImage: UiImage?,
|
||||||
|
val posterBackgroundImage: UiImage?,
|
||||||
val plotText: UiText,
|
val plotText: UiText,
|
||||||
val apiName: UiText,
|
val apiName: UiText,
|
||||||
val ratingText: UiText?,
|
val ratingText: UiText?,
|
||||||
|
@ -170,6 +171,9 @@ fun LoadResponse.toResultData(repo: APIRepository): ResultData {
|
||||||
posterImage = img(
|
posterImage = img(
|
||||||
posterUrl, posterHeaders
|
posterUrl, posterHeaders
|
||||||
) ?: img(R.drawable.default_cover),
|
) ?: img(R.drawable.default_cover),
|
||||||
|
posterBackgroundImage = img(
|
||||||
|
backgroundPosterUrl ?: posterUrl, posterHeaders
|
||||||
|
) ?: img(R.drawable.default_cover),
|
||||||
titleText = txt(name),
|
titleText = txt(name),
|
||||||
url = url,
|
url = url,
|
||||||
tags = tags ?: emptyList(),
|
tags = tags ?: emptyList(),
|
||||||
|
|
|
@ -70,9 +70,9 @@ sealed class UiImage {
|
||||||
data class Drawable(@DrawableRes val resId: Int) : UiImage()
|
data class Drawable(@DrawableRes val resId: Int) : UiImage()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ImageView?.setImage(value: UiImage?) {
|
fun ImageView?.setImage(value: UiImage?, fadeIn: Boolean = true) {
|
||||||
when (value) {
|
when (value) {
|
||||||
is UiImage.Image -> setImageImage(value)
|
is UiImage.Image -> setImageImage(value,fadeIn)
|
||||||
is UiImage.Drawable -> setImageDrawable(value)
|
is UiImage.Drawable -> setImageDrawable(value)
|
||||||
null -> {
|
null -> {
|
||||||
this?.isVisible = false
|
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
|
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) {
|
fun ImageView?.setImageDrawable(value: UiImage.Drawable) {
|
||||||
|
|
|
@ -221,7 +221,9 @@ class SearchFragment : Fragment() {
|
||||||
builder.setContentView(R.layout.home_select_mainpage)
|
builder.setContentView(R.layout.home_select_mainpage)
|
||||||
builder.show()
|
builder.show()
|
||||||
builder.let { dialog ->
|
builder.let { dialog ->
|
||||||
val isMultiLang = ctx.getApiProviderLangSettings().size > 1
|
val isMultiLang = ctx.getApiProviderLangSettings().let { set ->
|
||||||
|
set.size > 1 || set.contains(AllLanguagesName)
|
||||||
|
}
|
||||||
|
|
||||||
val cancelBtt = dialog.findViewById<MaterialButton>(R.id.cancel_btt)
|
val cancelBtt = dialog.findViewById<MaterialButton>(R.id.cancel_btt)
|
||||||
val applyBtt = dialog.findViewById<MaterialButton>(R.id.apply_btt)
|
val applyBtt = dialog.findViewById<MaterialButton>(R.id.apply_btt)
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package com.lagradost.cloudstream3.ui.settings
|
package com.lagradost.cloudstream3.ui.settings
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
|
@ -9,17 +8,15 @@ import com.lagradost.cloudstream3.*
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||||
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
|
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.setPaddingBottom
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
|
||||||
import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API
|
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.SingleSelectionHelper.showMultiDialog
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
|
||||||
|
import kotlin.reflect.jvm.internal.impl.descriptors.deserialization.PlatformDependentDeclarationFilter.All
|
||||||
|
|
||||||
|
|
||||||
class SettingsProviders : PreferenceFragmentCompat() {
|
class SettingsProviders : PreferenceFragmentCompat() {
|
||||||
|
@ -63,13 +60,17 @@ class SettingsProviders : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener {
|
getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener {
|
||||||
val names = enumValues<TvType>().sorted().map { it.name }
|
val names = enumValues<TvType>().sorted().map { it.name }
|
||||||
val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }.map { it.ordinal }
|
val default =
|
||||||
|
enumValues<TvType>().sorted().filter { it != TvType.NSFW }.map { it.ordinal }
|
||||||
val defaultSet = default.map { it.toString() }.toSet()
|
val defaultSet = default.map { it.toString() }.toSet()
|
||||||
val currentList = try {
|
val currentList = try {
|
||||||
settingsManager.getStringSet(getString(R.string.prefer_media_type_key), defaultSet)?.map {
|
settingsManager.getStringSet(getString(R.string.prefer_media_type_key), defaultSet)
|
||||||
|
?.map {
|
||||||
it.toInt()
|
it.toInt()
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) { null } ?: default
|
} catch (e: Throwable) {
|
||||||
|
null
|
||||||
|
} ?: default
|
||||||
|
|
||||||
activity?.showMultiDialog(
|
activity?.showMultiDialog(
|
||||||
names,
|
names,
|
||||||
|
@ -89,20 +90,23 @@ class SettingsProviders : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
|
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
|
||||||
activity?.getApiProviderLangSettings()?.let { current ->
|
activity?.getApiProviderLangSettings()?.let { current ->
|
||||||
val langs = APIHolder.apis.map { it.lang }.toSet()
|
val languages = APIHolder.apis.map { it.lang }.toSet()
|
||||||
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) }
|
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
|
||||||
|
|
||||||
val currentList = ArrayList<Int>()
|
val currentList = current.map {
|
||||||
for (i in current) {
|
languages.indexOf(it)
|
||||||
currentList.add(langs.indexOf(i))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val names = langs.map {
|
val names = languages.map {
|
||||||
|
if (it == AllLanguagesName) {
|
||||||
|
Pair(it, getString(R.string.all_languages_preference))
|
||||||
|
} else {
|
||||||
val emoji = SubtitleHelper.getFlagFromIso(it)
|
val emoji = SubtitleHelper.getFlagFromIso(it)
|
||||||
val name = SubtitleHelper.fromTwoLettersToLanguage(it)
|
val name = SubtitleHelper.fromTwoLettersToLanguage(it)
|
||||||
val fullName = "$emoji $name"
|
val fullName = "$emoji $name"
|
||||||
Pair(it, fullName)
|
Pair(it, fullName)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
activity?.showMultiDialog(
|
activity?.showMultiDialog(
|
||||||
names.map { it.second },
|
names.map { it.second },
|
||||||
|
|
|
@ -33,8 +33,6 @@ import com.lagradost.cloudstream3.widget.LinearRecycleViewLayoutManager
|
||||||
import kotlinx.android.synthetic.main.add_repo_input.*
|
import kotlinx.android.synthetic.main.add_repo_input.*
|
||||||
import kotlinx.android.synthetic.main.fragment_extensions.*
|
import kotlinx.android.synthetic.main.fragment_extensions.*
|
||||||
|
|
||||||
const val PUBLIC_REPOSITORIES_LIST = "https://recloudstream.github.io/repos/"
|
|
||||||
|
|
||||||
class ExtensionsFragment : Fragment() {
|
class ExtensionsFragment : Fragment() {
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -186,15 +184,7 @@ class ExtensionsFragment : Fragment() {
|
||||||
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt(
|
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)?.primaryClip?.getItemAt(
|
||||||
0
|
0
|
||||||
)?.text?.toString()?.let { copy ->
|
)?.text?.toString()?.let { copy ->
|
||||||
// Fix our own repo links and only paste the text if it's a link.
|
dialog.repo_url_input?.setText(copy)
|
||||||
if (copy.startsWith("http")) {
|
|
||||||
val fixedUrl = if (copy.startsWith("https://cs.repo")) {
|
|
||||||
"https://" + copy.substringAfter("?")
|
|
||||||
} else {
|
|
||||||
copy
|
|
||||||
}
|
|
||||||
dialog.repo_url_input?.setText(fixedUrl)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dialog.list_repositories?.setOnClickListener {
|
// dialog.list_repositories?.setOnClickListener {
|
||||||
|
@ -206,13 +196,12 @@ class ExtensionsFragment : Fragment() {
|
||||||
// dialog.text2?.text = provider.name
|
// dialog.text2?.text = provider.name
|
||||||
dialog.apply_btt?.setOnClickListener secondListener@{
|
dialog.apply_btt?.setOnClickListener secondListener@{
|
||||||
val name = dialog.repo_name_input?.text?.toString()
|
val name = dialog.repo_name_input?.text?.toString()
|
||||||
|
ioSafe {
|
||||||
val url = dialog.repo_url_input?.text?.toString()
|
val url = dialog.repo_url_input?.text?.toString()
|
||||||
|
?.let { it1 -> RepositoryManager.parseRepoUrl(it1) }
|
||||||
if (url.isNullOrBlank()) {
|
if (url.isNullOrBlank()) {
|
||||||
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
|
showToast(activity, R.string.error_invalid_data, Toast.LENGTH_SHORT)
|
||||||
return@secondListener
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
ioSafe {
|
|
||||||
val fixedName = if (!name.isNullOrBlank()) name
|
val fixedName = if (!name.isNullOrBlank()) name
|
||||||
else RepositoryManager.parseRepository(url)?.name ?: "No name"
|
else RepositoryManager.parseRepository(url)?.name ?: "No name"
|
||||||
|
|
||||||
|
@ -222,6 +211,7 @@ class ExtensionsFragment : Fragment() {
|
||||||
extensionViewModel.loadRepositories()
|
extensionViewModel.loadRepositories()
|
||||||
this@ExtensionsFragment.activity?.downloadAllPluginsDialog(url, fixedName)
|
this@ExtensionsFragment.activity?.downloadAllPluginsDialog(url, fixedName)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
dialog.dismissSafe(activity)
|
dialog.dismissSafe(activity)
|
||||||
}
|
}
|
||||||
dialog.cancel_btt?.setOnClickListener {
|
dialog.cancel_btt?.setOnClickListener {
|
||||||
|
|
|
@ -7,6 +7,9 @@ import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.navigation.fragment.findNavController
|
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.MainActivity.Companion.afterRepositoryLoadedEvent
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
import com.lagradost.cloudstream3.plugins.RepositoryManager
|
||||||
|
@ -96,7 +99,14 @@ class SetupFragmentExtensions : Fragment() {
|
||||||
next_btt?.setOnClickListener {
|
next_btt?.setOnClickListener {
|
||||||
// Continue setup
|
// Continue setup
|
||||||
if (isSetup)
|
if (isSetup)
|
||||||
|
if (
|
||||||
|
// If any available languages
|
||||||
|
apis.distinctBy { it.lang }.size > 1
|
||||||
|
) {
|
||||||
|
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
|
||||||
|
} else {
|
||||||
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media)
|
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_media)
|
||||||
|
}
|
||||||
else
|
else
|
||||||
findNavController().navigate(R.id.navigation_home)
|
findNavController().navigate(R.id.navigation_home)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ import com.lagradost.cloudstream3.CommonActivity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
||||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
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.appLanguages
|
||||||
import com.lagradost.cloudstream3.ui.settings.getCurrentLocale
|
import com.lagradost.cloudstream3.ui.settings.getCurrentLocale
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
|
@ -85,7 +84,7 @@ class SetupFragmentLanguage : Fragment() {
|
||||||
&& PluginManager.getPluginsLocal().isEmpty()
|
&& PluginManager.getPluginsLocal().isEmpty()
|
||||||
//&& PREBUILT_REPOSITORIES.isNotEmpty()
|
//&& PREBUILT_REPOSITORIES.isNotEmpty()
|
||||||
) R.id.action_navigation_global_to_navigation_setup_extensions
|
) 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(
|
findNavController().navigate(
|
||||||
nextDestination,
|
nextDestination,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import androidx.navigation.fragment.findNavController
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.lagradost.cloudstream3.APIHolder
|
import com.lagradost.cloudstream3.APIHolder
|
||||||
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings
|
||||||
|
import com.lagradost.cloudstream3.AllLanguagesName
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
import com.lagradost.cloudstream3.utils.SubtitleHelper
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
|
||||||
|
@ -39,14 +40,21 @@ class SetupFragmentProviderLanguage : Fragment() {
|
||||||
|
|
||||||
val current = this.getApiProviderLangSettings()
|
val current = this.getApiProviderLangSettings()
|
||||||
val langs = APIHolder.apis.map { it.lang }.toSet()
|
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 languageNames = langs.map {
|
||||||
|
if (it == AllLanguagesName) {
|
||||||
|
getString(R.string.all_languages_preference)
|
||||||
|
} else {
|
||||||
val emoji = SubtitleHelper.getFlagFromIso(it)
|
val emoji = SubtitleHelper.getFlagFromIso(it)
|
||||||
val name = SubtitleHelper.fromTwoLettersToLanguage(it)
|
val name = SubtitleHelper.fromTwoLettersToLanguage(it)
|
||||||
"$emoji $name"
|
"$emoji $name"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
arrayAdapter.addAll(languageNames)
|
arrayAdapter.addAll(languageNames)
|
||||||
|
|
||||||
listview1?.adapter = arrayAdapter
|
listview1?.adapter = arrayAdapter
|
||||||
|
|
|
@ -49,8 +49,7 @@ data class SaveCaptionStyle(
|
||||||
@JsonProperty("foregroundColor") var foregroundColor: Int,
|
@JsonProperty("foregroundColor") var foregroundColor: Int,
|
||||||
@JsonProperty("backgroundColor") var backgroundColor: Int,
|
@JsonProperty("backgroundColor") var backgroundColor: Int,
|
||||||
@JsonProperty("windowColor") var windowColor: Int,
|
@JsonProperty("windowColor") var windowColor: Int,
|
||||||
@CaptionStyleCompat.EdgeType
|
@JsonProperty("edgeType") var edgeType: @CaptionStyleCompat.EdgeType Int,
|
||||||
@JsonProperty("edgeType") var edgeType: Int,
|
|
||||||
@JsonProperty("edgeColor") var edgeColor: Int,
|
@JsonProperty("edgeColor") var edgeColor: Int,
|
||||||
@FontRes
|
@FontRes
|
||||||
@JsonProperty("typeface") var typeface: Int?,
|
@JsonProperty("typeface") var typeface: Int?,
|
||||||
|
|
|
@ -20,10 +20,13 @@ class CastOptionsProvider : OptionsProvider {
|
||||||
MediaIntentReceiver.ACTION_FORWARD,
|
MediaIntentReceiver.ACTION_FORWARD,
|
||||||
MediaIntentReceiver.ACTION_STOP_CASTING
|
MediaIntentReceiver.ACTION_STOP_CASTING
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val name = ControllerActivity::class.qualifiedName!!
|
||||||
|
|
||||||
val compatButtonAction = intArrayOf(1, 3)
|
val compatButtonAction = intArrayOf(1, 3)
|
||||||
val notificationOptions =
|
val notificationOptions =
|
||||||
NotificationOptions.Builder()
|
NotificationOptions.Builder()
|
||||||
.setTargetActivityClassName(ControllerActivity::class.qualifiedName)
|
.setTargetActivityClassName(name)
|
||||||
.setActions(buttonActions, compatButtonAction)
|
.setActions(buttonActions, compatButtonAction)
|
||||||
.setForward30DrawableResId(R.drawable.go_forward_30)
|
.setForward30DrawableResId(R.drawable.go_forward_30)
|
||||||
.setRewind30DrawableResId(R.drawable.go_back_30)
|
.setRewind30DrawableResId(R.drawable.go_back_30)
|
||||||
|
@ -32,7 +35,7 @@ class CastOptionsProvider : OptionsProvider {
|
||||||
|
|
||||||
val mediaOptions = CastMediaOptions.Builder()
|
val mediaOptions = CastMediaOptions.Builder()
|
||||||
.setNotificationOptions(notificationOptions)
|
.setNotificationOptions(notificationOptions)
|
||||||
.setExpandedControllerActivityClassName(ControllerActivity::class.qualifiedName)
|
.setExpandedControllerActivityClassName(name)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return CastOptions.Builder()
|
return CastOptions.Builder()
|
||||||
|
@ -44,7 +47,7 @@ class CastOptionsProvider : OptionsProvider {
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getAdditionalSessionProviders(p0: Context?): MutableList<SessionProvider> {
|
override fun getAdditionalSessionProviders(p0: Context): MutableList<SessionProvider> {
|
||||||
return Collections.emptyList()
|
return Collections.emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -329,6 +329,8 @@ val extractorApis: MutableList<ExtractorApi> = arrayListOf(
|
||||||
Vidmolyme(),
|
Vidmolyme(),
|
||||||
Voe(),
|
Voe(),
|
||||||
Moviehab(),
|
Moviehab(),
|
||||||
|
MoviehabNet(),
|
||||||
|
Jeniusplay(),
|
||||||
|
|
||||||
Gdriveplayerapi(),
|
Gdriveplayerapi(),
|
||||||
Gdriveplayerapp(),
|
Gdriveplayerapp(),
|
||||||
|
|
|
@ -136,7 +136,7 @@ object UIHelper {
|
||||||
navigation, arguments
|
navigation, arguments
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (t : Throwable) {
|
} catch (t: Throwable) {
|
||||||
logError(t)
|
logError(t)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -159,17 +159,20 @@ object UIHelper {
|
||||||
url: String?,
|
url: String?,
|
||||||
headers: Map<String, String>? = null,
|
headers: Map<String, String>? = null,
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
errorImageDrawable: Int? = null
|
errorImageDrawable: Int? = null,
|
||||||
|
fadeIn: Boolean = true
|
||||||
): Boolean {
|
): Boolean {
|
||||||
if (this == null || url.isNullOrBlank()) return false
|
if (this == null || url.isNullOrBlank()) return false
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val builder = GlideApp.with(this)
|
val builder = GlideApp.with(this)
|
||||||
.load(GlideUrl(url) { headers ?: emptyMap() }).transition(
|
.load(GlideUrl(url) { headers ?: emptyMap() })
|
||||||
DrawableTransitionOptions.withCrossFade()
|
|
||||||
)
|
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
.diskCacheStrategy(DiskCacheStrategy.ALL).let { req ->
|
||||||
|
if (fadeIn)
|
||||||
|
req.transition(DrawableTransitionOptions.withCrossFade())
|
||||||
|
else req
|
||||||
|
}
|
||||||
|
|
||||||
val res = if (errorImageDrawable != null)
|
val res = if (errorImageDrawable != null)
|
||||||
builder.error(errorImageDrawable).into(this)
|
builder.error(errorImageDrawable).into(this)
|
||||||
|
|
|
@ -287,39 +287,23 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
<androidx.viewpager.widget.ViewPager
|
<androidx.viewpager2.widget.ViewPager2
|
||||||
android:id="@+id/home_preview_viewpager"
|
android:id="@+id/home_preview_viewpager"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
</androidx.viewpager.widget.ViewPager>
|
</androidx.viewpager2.widget.ViewPager2>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:visibility="gone"
|
|
||||||
android:id="@+id/home_preview_image"
|
android:id="@+id/home_preview_image"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:alpha="0.8"
|
android:alpha="0.8"
|
||||||
android:scaleType="centerCrop"
|
android:scaleType="centerCrop"
|
||||||
|
android:visibility="gone"
|
||||||
tools:src="@drawable/example_poster" />
|
tools:src="@drawable/example_poster" />
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/title_shadow_top"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="150dp"
|
|
||||||
android:layout_gravity="top"
|
|
||||||
android:alpha="1"
|
|
||||||
android:background="@drawable/background_shadow"
|
|
||||||
android:rotation="180"
|
|
||||||
android:visibility="visible" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/title_shadow"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="300dp"
|
|
||||||
android:layout_gravity="bottom"
|
|
||||||
android:background="@drawable/background_shadow" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/home_padding"
|
android:id="@+id/home_padding"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -363,43 +347,14 @@
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
||||||
|
android:id="@+id/home_preview_title_holder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="100dp"
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
|
android:gravity="center"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/home_preview_title"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:paddingStart="30dp"
|
|
||||||
android:paddingEnd="30dp"
|
|
||||||
android:paddingBottom="10dp"
|
|
||||||
android:textColor="?attr/white"
|
|
||||||
android:textSize="17sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
tools:text="The Perfect Run" />
|
|
||||||
<!--<TextView
|
|
||||||
android:paddingStart="30dp"
|
|
||||||
android:paddingEnd="30dp"
|
|
||||||
android:id="@+id/home_season_tags"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="14sp"
|
|
||||||
tools:text="5 seasons 50 episodes" />-->
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/home_preview_tags"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:paddingStart="30dp"
|
|
||||||
android:paddingEnd="30dp"
|
|
||||||
android:textColor="?attr/white"
|
|
||||||
android:textSize="14sp"
|
|
||||||
tools:text="Hello • World • Tags" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:keepScreenOn="true"
|
|
||||||
android:id="@+id/player_background"
|
android:id="@+id/player_background"
|
||||||
app:backgroundTint="@android:color/black"
|
app:backgroundTint="@android:color/black"
|
||||||
android:background="@android:color/black"
|
android:background="@android:color/black"
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@android:color/black"
|
android:background="@android:color/black"
|
||||||
android:keepScreenOn="true"
|
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:screenOrientation="sensorLandscape"
|
android:screenOrientation="sensorLandscape"
|
||||||
app:backgroundTint="@android:color/black"
|
app:backgroundTint="@android:color/black"
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/result_root"
|
android:id="@+id/result_root"
|
||||||
|
style="@style/DarkFragment"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
style="@style/DarkFragment"
|
|
||||||
android:background="?attr/primaryBlackBackground"
|
android:background="?attr/primaryBlackBackground"
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true">
|
android:focusable="true">
|
||||||
|
@ -122,9 +122,9 @@
|
||||||
android:id="@+id/result_finish_loading"
|
android:id="@+id/result_finish_loading"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible"
|
tools:visibility="visible">
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.core.widget.NestedScrollView
|
||||||
android:id="@+id/result_scroll"
|
android:id="@+id/result_scroll"
|
||||||
|
@ -139,18 +139,19 @@
|
||||||
android:background="?attr/primaryBlackBackground"
|
android:background="?attr/primaryBlackBackground"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!--
|
||||||
<com.facebook.shimmer.ShimmerFrameLayout
|
<com.facebook.shimmer.ShimmerFrameLayout
|
||||||
tools:visibility="gone"
|
|
||||||
android:visibility="gone"
|
|
||||||
android:id="@+id/result_trailer_loading"
|
android:id="@+id/result_trailer_loading"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
|
android:visibility="gone"
|
||||||
app:shimmer_auto_start="true"
|
app:shimmer_auto_start="true"
|
||||||
app:shimmer_base_alpha="0.2"
|
app:shimmer_base_alpha="0.2"
|
||||||
app:shimmer_duration="@integer/loading_time"
|
app:shimmer_duration="@integer/loading_time"
|
||||||
app:shimmer_highlight_alpha="0.3">
|
app:shimmer_highlight_alpha="0.3"
|
||||||
|
tools:visibility="gone">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -159,22 +160,15 @@
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:background="@color/grayShimmer"
|
|
||||||
app:cardCornerRadius="@dimen/loading_radius"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="150dp"
|
android:layout_height="150dp"
|
||||||
android:foreground="@drawable/outline_drawable" />
|
android:background="@color/grayShimmer"
|
||||||
|
android:foreground="@drawable/outline_drawable"
|
||||||
|
app:cardCornerRadius="@dimen/loading_radius" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.facebook.shimmer.ShimmerFrameLayout>
|
</com.facebook.shimmer.ShimmerFrameLayout>
|
||||||
|
-->
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:descendantFocusability="blocksDescendants"
|
|
||||||
android:id="@+id/result_smallscreen_holder"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<include layout="@layout/fragment_trailer" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
|
@ -272,6 +266,36 @@
|
||||||
app:tint="?attr/textColor" />
|
app:tint="?attr/textColor" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</FrameLayout>-->
|
</FrameLayout>-->
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="200dp">
|
||||||
|
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/result_smallscreen_holder"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:descendantFocusability="blocksDescendants">
|
||||||
|
|
||||||
|
<include layout="@layout/fragment_trailer" />
|
||||||
|
</FrameLayout>
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/result_poster_background_holder"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/result_poster_background"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="@drawable/background_shadow" />
|
||||||
|
</FrameLayout>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -281,6 +305,7 @@
|
||||||
android:paddingStart="@dimen/result_padding"
|
android:paddingStart="@dimen/result_padding"
|
||||||
android:paddingEnd="@dimen/result_padding">
|
android:paddingEnd="@dimen/result_padding">
|
||||||
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -289,10 +314,12 @@
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:visibility="visible">
|
android:visibility="visible">
|
||||||
|
|
||||||
|
|
||||||
<androidx.cardview.widget.CardView
|
<androidx.cardview.widget.CardView
|
||||||
android:id="@+id/result_poster_holder"
|
android:id="@+id/result_poster_holder"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
app:cardCornerRadius="@dimen/rounded_image_radius">
|
app:cardCornerRadius="@dimen/rounded_image_radius">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
@ -312,7 +339,6 @@
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_marginHorizontal="10dp"
|
|
||||||
android:id="@+id/result_title"
|
android:id="@+id/result_title"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -324,7 +350,6 @@
|
||||||
tools:text="The Perfect Run The Perfect Run" />
|
tools:text="The Perfect Run The Perfect Run" />
|
||||||
|
|
||||||
<com.lagradost.cloudstream3.widget.FlowLayout
|
<com.lagradost.cloudstream3.widget.FlowLayout
|
||||||
android:layout_marginHorizontal="10dp"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:itemSpacing="10dp">
|
app:itemSpacing="10dp">
|
||||||
|
@ -366,20 +391,19 @@
|
||||||
The focus outline now settles between the poster and text.
|
The focus outline now settles between the poster and text.
|
||||||
-->
|
-->
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_marginHorizontal="5dp"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:padding="5dp"
|
|
||||||
android:maxLength="1000"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:id="@+id/result_description"
|
android:id="@+id/result_description"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
android:foreground="@drawable/outline_drawable"
|
android:foreground="@drawable/outline_drawable"
|
||||||
|
android:maxLength="1000"
|
||||||
android:nextFocusUp="@id/result_back"
|
android:nextFocusUp="@id/result_back"
|
||||||
android:nextFocusDown="@id/result_bookmark_button"
|
android:nextFocusDown="@id/result_bookmark_button"
|
||||||
|
android:paddingTop="5dp"
|
||||||
android:textColor="?attr/textColor"
|
android:textColor="?attr/textColor"
|
||||||
android:textSize="15sp"
|
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. " />
|
tools:text="Ryan Quicksave Romano is an eccentric adventurer with a strange power: he can create a save-point in time and redo his life whenever he dies. Arriving in New Rome, the glitzy capital of sin of a rebuilding Europe, he finds the city torn between mega-corporations, sponsored heroes, superpowered criminals, and true monsters. It's a time of chaos, where potions can grant the power to rule the world and dangers lurk everywhere. " />
|
||||||
|
@ -389,12 +413,13 @@
|
||||||
android:layout_height="30dp"
|
android:layout_height="30dp"
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
android:src="@drawable/background_shadow"
|
android:src="@drawable/background_shadow"
|
||||||
|
android:visibility="gone"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/result_bookmark_button"
|
android:id="@+id/result_bookmark_button"
|
||||||
style="@style/BlackButton"
|
style="@style/BlackButton"
|
||||||
|
@ -417,7 +442,7 @@
|
||||||
app:icon="@drawable/ic_baseline_bookmark_24"
|
app:icon="@drawable/ic_baseline_bookmark_24"
|
||||||
tools:text="Bookmark"
|
tools:text="Bookmark"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/result_cast_text"
|
android:id="@+id/result_cast_text"
|
||||||
|
@ -431,23 +456,23 @@
|
||||||
tools:text="Cast: Joe Ligma" />
|
tools:text="Cast: Joe Ligma" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
tools:visibility="gone"
|
|
||||||
android:nextFocusUp="@id/result_bookmark_button"
|
|
||||||
android:nextFocusDown="@id/result_play_movie"
|
|
||||||
|
|
||||||
android:id="@+id/result_cast_items"
|
android:id="@+id/result_cast_items"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:descendantFocusability="afterDescendants"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
|
android:descendantFocusability="afterDescendants"
|
||||||
android:fadingEdge="horizontal"
|
android:fadingEdge="horizontal"
|
||||||
android:focusableInTouchMode="false"
|
|
||||||
android:focusable="false"
|
android:focusable="false"
|
||||||
|
android:focusableInTouchMode="false"
|
||||||
|
android:nextFocusUp="@id/result_bookmark_button"
|
||||||
|
android:nextFocusDown="@id/result_play_movie"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingTop="5dp"
|
android:paddingTop="5dp"
|
||||||
android:requiresFadingEdge="horizontal"
|
android:requiresFadingEdge="horizontal"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:itemCount="2"
|
tools:itemCount="2"
|
||||||
tools:listitem="@layout/cast_item" />
|
tools:listitem="@layout/cast_item"
|
||||||
|
tools:visibility="gone" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/result_vpn"
|
android:id="@+id/result_vpn"
|
||||||
|
@ -847,29 +872,29 @@
|
||||||
<!--TODO add next airing-->
|
<!--TODO add next airing-->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/result_next_airing_holder"
|
android:id="@+id/result_next_airing_holder"
|
||||||
android:layout_gravity="start"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:gravity="center"
|
|
||||||
|
|
||||||
android:id="@+id/result_next_airing"
|
android:id="@+id/result_next_airing"
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
android:textColor="?attr/grayTextColor"
|
android:textColor="?attr/grayTextColor"
|
||||||
android:textSize="17sp"
|
android:textSize="17sp"
|
||||||
android:textStyle="normal"
|
android:textStyle="normal"
|
||||||
tools:text="Episode 1022 will be released in" />
|
tools:text="Episode 1022 will be released in" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:paddingEnd="5dp"
|
|
||||||
android:paddingStart="5dp"
|
|
||||||
android:gravity="center"
|
|
||||||
android:id="@+id/result_next_airing_time"
|
android:id="@+id/result_next_airing_time"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingStart="5dp"
|
||||||
|
android:paddingEnd="5dp"
|
||||||
android:textColor="?attr/textColor"
|
android:textColor="?attr/textColor"
|
||||||
android:textSize="17sp"
|
android:textSize="17sp"
|
||||||
android:textStyle="normal"
|
android:textStyle="normal"
|
||||||
|
@ -917,9 +942,9 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="0dp"
|
android:layout_marginTop="0dp"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
|
||||||
android:descendantFocusability="afterDescendants"
|
android:descendantFocusability="afterDescendants"
|
||||||
android:paddingBottom="100dp"
|
android:paddingBottom="100dp"
|
||||||
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||||
tools:listitem="@layout/result_episode_both_tv" />
|
tools:listitem="@layout/result_episode_both_tv" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:keepScreenOn="true"
|
|
||||||
android:id="@+id/player_background"
|
android:id="@+id/player_background"
|
||||||
app:backgroundTint="@android:color/black"
|
app:backgroundTint="@android:color/black"
|
||||||
android:background="@android:color/black"
|
android:background="@android:color/black"
|
||||||
|
|
74
app/src/main/res/layout/home_scroll_view.xml
Normal file
74
app/src/main/res/layout/home_scroll_view.xml
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:background="?attr/primaryGrayBackground"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/home_scroll_preview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
tools:src="@drawable/example_poster" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/title_shadow_top"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="150dp"
|
||||||
|
android:layout_gravity="top"
|
||||||
|
android:alpha="1"
|
||||||
|
android:background="@drawable/background_shadow"
|
||||||
|
android:rotation="180"
|
||||||
|
android:visibility="visible" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/title_shadow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="300dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="@drawable/background_shadow" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal|bottom"
|
||||||
|
android:layout_marginBottom="100dp"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/home_scroll_preview_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingHorizontal="30dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:textColor="?attr/white"
|
||||||
|
android:textSize="17sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="The Perfect Run" />
|
||||||
|
<!--<TextView
|
||||||
|
android:paddingStart="30dp"
|
||||||
|
android:paddingEnd="30dp"
|
||||||
|
android:id="@+id/home_season_tags"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:textColor="@color/white"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="5 seasons 50 episodes" />-->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/home_scroll_preview_tags"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:gravity="center"
|
||||||
|
android:paddingStart="30dp"
|
||||||
|
android:paddingEnd="30dp"
|
||||||
|
android:textColor="?attr/white"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="Hello • World • Tags" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -38,26 +38,41 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
<View
|
<!-- <View
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@drawable/player_gradient_tv" />
|
android:background="@drawable/player_gradient_tv" />
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/result_trailer_thumbnail"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:scaleType="centerCrop" />-->
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="60dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="@drawable/background_shadow" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:textSize="20sp"
|
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="@android:color/white"
|
|
||||||
android:text="@string/trailer"
|
|
||||||
android:padding="10dp"
|
|
||||||
android:layout_gravity="start|bottom"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start|bottom"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:text="@string/trailer"
|
||||||
|
android:textColor="@android:color/white"
|
||||||
|
android:textSize="20sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:layout_gravity="center"
|
|
||||||
android:src="@drawable/play_button"
|
|
||||||
android:layout_width="60dp"
|
android:layout_width="60dp"
|
||||||
android:layout_height="60dp" />
|
android:layout_height="60dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:src="@drawable/play_button" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
@ -67,54 +82,54 @@
|
||||||
|
|
||||||
<!--use for thinner app:trackThickness="3dp" com.google.android.material.progressindicator.CircularProgressIndicator-->
|
<!--use for thinner app:trackThickness="3dp" com.google.android.material.progressindicator.CircularProgressIndicator-->
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
android:id="@+id/player_buffering"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
android:layout_width="wrap_content"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
android:layout_gravity="center"
|
||||||
|
|
||||||
android:focusable="false"
|
|
||||||
android:clickable="false"
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
android:focusableInTouchMode="false"
|
android:focusableInTouchMode="false"
|
||||||
|
|
||||||
android:indeterminate="true"
|
android:indeterminate="true"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible"
|
|
||||||
android:id="@+id/player_buffering"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_width="wrap_content" />
|
|
||||||
|
|
||||||
<!-- This nested layout is necessary because of buffering and clicking-->
|
|
||||||
<FrameLayout
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<!-- This nested layout is necessary because of buffering and clicking-->
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/player_pause_play_holder_holder"
|
||||||
android:layout_width="100dp"
|
android:layout_width="100dp"
|
||||||
android:layout_height="100dp"
|
android:layout_height="100dp"
|
||||||
android:id="@+id/player_pause_play_holder_holder">
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
tools:ignore="uselessParent"
|
|
||||||
android:id="@+id/player_pause_play_holder"
|
android:id="@+id/player_pause_play_holder"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
tools:ignore="uselessParent">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
app:tint="@color/white"
|
|
||||||
android:id="@+id/player_pause_play"
|
android:id="@+id/player_pause_play"
|
||||||
|
android:layout_width="70dp"
|
||||||
|
android:layout_height="70dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:background="@drawable/video_tap_button"
|
||||||
android:nextFocusLeft="@id/exo_rew"
|
android:nextFocusLeft="@id/exo_rew"
|
||||||
|
|
||||||
android:nextFocusRight="@id/exo_ffwd"
|
android:nextFocusRight="@id/exo_ffwd"
|
||||||
|
|
||||||
android:nextFocusUp="@id/player_go_back"
|
android:nextFocusUp="@id/player_go_back"
|
||||||
android:nextFocusDown="@id/player_lock"
|
android:nextFocusDown="@id/player_lock"
|
||||||
|
|
||||||
android:layout_gravity="center"
|
|
||||||
|
|
||||||
android:src="@drawable/netflix_pause"
|
android:src="@drawable/netflix_pause"
|
||||||
android:background="@drawable/video_tap_button"
|
app:tint="@color/white"
|
||||||
|
|
||||||
android:layout_width="70dp"
|
|
||||||
android:layout_height="70dp"
|
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
@ -135,12 +150,12 @@
|
||||||
android:id="@+id/player_rew_holder"
|
android:id="@+id/player_rew_holder"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_constraintWidth_percent="0.5"
|
|
||||||
android:layout_gravity="center_vertical|start"
|
android:layout_gravity="center_vertical|start"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
app:layout_constraintRight_toLeftOf="@id/player_ffwd_holder"
|
app:layout_constraintRight_toLeftOf="@id/player_ffwd_holder"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.5">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/exo_rew_text"
|
android:id="@+id/exo_rew_text"
|
||||||
|
@ -169,21 +184,21 @@
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:scaleX="-1"
|
android:scaleX="-1"
|
||||||
android:src="@drawable/netflix_skip_forward"
|
android:src="@drawable/netflix_skip_forward"
|
||||||
app:tint="@color/white"
|
|
||||||
android:tintMode="src_in"
|
android:tintMode="src_in"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/player_ffwd_holder"
|
android:id="@+id/player_ffwd_holder"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
app:layout_constraintWidth_percent="0.5"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical|end"
|
android:layout_gravity="center_vertical|end"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintLeft_toRightOf="@id/player_rew_holder"
|
app:layout_constraintLeft_toRightOf="@id/player_rew_holder"
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintWidth_percent="0.5">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/exo_ffwd_text"
|
android:id="@+id/exo_ffwd_text"
|
||||||
|
@ -209,8 +224,8 @@
|
||||||
android:padding="10dp"
|
android:padding="10dp"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:src="@drawable/netflix_skip_forward"
|
android:src="@drawable/netflix_skip_forward"
|
||||||
app:tint="@color/white"
|
|
||||||
android:tintMode="src_in"
|
android:tintMode="src_in"
|
||||||
|
app:tint="@color/white"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
@ -230,60 +245,60 @@
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@id/exo_prev"
|
android:id="@id/exo_prev"
|
||||||
style="@style/ExoMediaButton.Previous"
|
style="@style/ExoMediaButton.Previous"
|
||||||
app:tint="?attr/colorPrimaryDark"
|
|
||||||
android:tintMode="src_in"
|
android:tintMode="src_in"
|
||||||
|
app:tint="?attr/colorPrimaryDark"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@id/exo_repeat_toggle"
|
android:id="@id/exo_repeat_toggle"
|
||||||
style="@style/ExoMediaButton"
|
style="@style/ExoMediaButton"
|
||||||
app:tint="?attr/colorPrimaryDark"
|
|
||||||
android:tintMode="src_in"
|
android:tintMode="src_in"
|
||||||
|
app:tint="?attr/colorPrimaryDark"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@id/exo_next"
|
android:id="@id/exo_next"
|
||||||
style="@style/ExoMediaButton.Next"
|
style="@style/ExoMediaButton.Next"
|
||||||
app:tint="?attr/colorPrimaryDark"
|
|
||||||
android:tintMode="src_in"
|
android:tintMode="src_in"
|
||||||
|
app:tint="?attr/colorPrimaryDark"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@id/exo_vr"
|
android:id="@id/exo_vr"
|
||||||
style="@style/ExoMediaButton.VR"
|
style="@style/ExoMediaButton.VR"
|
||||||
app:tint="?attr/colorPrimaryDark"
|
|
||||||
android:tintMode="src_in"
|
android:tintMode="src_in"
|
||||||
|
app:tint="?attr/colorPrimaryDark"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@id/exo_play"
|
android:id="@id/exo_play"
|
||||||
app:tint="?attr/colorPrimaryDark"
|
android:layout_width="0dp"
|
||||||
android:tintMode="src_in"
|
|
||||||
tools:ignore="ContentDescription"
|
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_width="0dp" />
|
android:tintMode="src_in"
|
||||||
|
app:tint="?attr/colorPrimaryDark"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@id/exo_pause"
|
android:id="@id/exo_pause"
|
||||||
app:tint="?attr/colorPrimaryDark"
|
android:layout_width="0dp"
|
||||||
android:tintMode="src_in"
|
|
||||||
tools:ignore="ContentDescription"
|
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_width="0dp" />
|
android:tintMode="src_in"
|
||||||
|
app:tint="?attr/colorPrimaryDark"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
|
||||||
android:id="@+id/player_open_source"
|
android:id="@+id/player_open_source"
|
||||||
app:tint="@color/white"
|
|
||||||
android:src="@drawable/ic_baseline_public_24"
|
|
||||||
android:layout_margin="20dp"
|
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="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_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="@color/white" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/bottom_player_bar"
|
android:id="@+id/bottom_player_bar"
|
||||||
|
@ -296,10 +311,10 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent">
|
app:layout_constraintEnd_toEndOf="parent">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_weight="1"
|
|
||||||
android:id="@+id/player_video_bar"
|
android:id="@+id/player_video_bar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -350,14 +365,14 @@
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:background="?android:attr/selectableItemBackgroundBorderless"
|
|
||||||
android:id="@+id/player_fullscreen"
|
android:id="@+id/player_fullscreen"
|
||||||
app:tint="@color/white"
|
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:src="@drawable/baseline_fullscreen_24"
|
|
||||||
android:layout_marginEnd="20dp"
|
|
||||||
android:layout_width="30dp"
|
android:layout_width="30dp"
|
||||||
android:layout_height="30dp" />
|
android:layout_height="30dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:src="@drawable/baseline_fullscreen_24"
|
||||||
|
app:tint="@color/white" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
@ -397,14 +412,14 @@
|
||||||
style="@android:style/Widget.Material.ProgressBar.Horizontal"
|
style="@android:style/Widget.Material.ProgressBar.Horizontal"
|
||||||
android:layout_width="4dp"
|
android:layout_width="4dp"
|
||||||
android:layout_height="150dp"
|
android:layout_height="150dp"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
android:layout_gravity="end|center_vertical"
|
android:layout_gravity="end|center_vertical"
|
||||||
android:layout_marginStart="40dp"
|
android:layout_marginStart="40dp"
|
||||||
android:indeterminate="false"
|
android:indeterminate="false"
|
||||||
android:max="100"
|
android:max="100"
|
||||||
android:progress="100"
|
android:progress="100"
|
||||||
android:progressDrawable="@drawable/progress_drawable_vertical"
|
android:progressDrawable="@drawable/progress_drawable_vertical"
|
||||||
tools:progress="30"
|
tools:progress="30" />
|
||||||
android:layout_centerInParent="true" />
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
|
|
|
@ -522,13 +522,6 @@
|
||||||
app:exitAnim="@anim/exit_anim"
|
app:exitAnim="@anim/exit_anim"
|
||||||
app:popEnterAnim="@anim/enter_anim"
|
app:popEnterAnim="@anim/enter_anim"
|
||||||
app:popExitAnim="@anim/exit_anim" />
|
app:popExitAnim="@anim/exit_anim" />
|
||||||
<action
|
|
||||||
android:id="@+id/action_navigation_setup_language_to_navigation_setup_media"
|
|
||||||
app:destination="@id/navigation_setup_media"
|
|
||||||
app:enterAnim="@anim/enter_anim"
|
|
||||||
app:exitAnim="@anim/exit_anim"
|
|
||||||
app:popEnterAnim="@anim/enter_anim"
|
|
||||||
app:popExitAnim="@anim/exit_anim" />
|
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<action
|
<action
|
||||||
|
|
|
@ -643,4 +643,5 @@
|
||||||
<string name="player_settings_play_in_web">Web Video Cast</string>
|
<string name="player_settings_play_in_web">Web Video Cast</string>
|
||||||
<string name="player_settings_play_in_browser">Browser</string>
|
<string name="player_settings_play_in_browser">Browser</string>
|
||||||
<string name="app_not_found_error">App not found</string>
|
<string name="app_not_found_error">App not found</string>
|
||||||
|
<string name="all_languages_preference">All Languages</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -81,6 +81,9 @@
|
||||||
<item name="chipStrokeColor">@color/transparent</item>
|
<item name="chipStrokeColor">@color/transparent</item>
|
||||||
<item name="textColor">@color/chip_color_text</item>
|
<item name="textColor">@color/chip_color_text</item>
|
||||||
<item name="android:textColor">@color/chip_color_text</item>
|
<item name="android:textColor">@color/chip_color_text</item>
|
||||||
|
<item name="checkedIconTint">@color/chip_color_text</item>
|
||||||
|
<item name="fontFamily">@font/google_sans</item>
|
||||||
|
<item name="android:fontFamily">@font/google_sans</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AmoledMode">
|
<style name="AmoledMode">
|
||||||
|
@ -137,13 +140,13 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="OverlayPrimaryColorMonetTwo">
|
<style name="OverlayPrimaryColorMonetTwo">
|
||||||
<item name="colorPrimary">@color/material_dynamic_tertiary80</item>
|
<item name="colorPrimary">@color/material_dynamic_secondary80</item>
|
||||||
<item name="android:colorPrimary">@color/material_dynamic_tertiary80</item>
|
<item name="android:colorPrimary">@color/material_dynamic_secondary80</item>
|
||||||
<item name="colorPrimaryDark">@color/material_dynamic_tertiary30</item>
|
<item name="colorPrimaryDark">@color/material_dynamic_secondary30</item>
|
||||||
<item name="colorAccent">@color/material_dynamic_tertiary80</item>
|
<item name="colorAccent">@color/material_dynamic_secondary80</item>
|
||||||
<item name="colorOnPrimary">@color/material_dynamic_tertiary20</item>
|
<item name="colorOnPrimary">@color/material_dynamic_secondary20</item>
|
||||||
<!-- Needed for leanback fuckery -->
|
<!-- Needed for leanback fuckery -->
|
||||||
<item name="android:colorAccent">@color/material_dynamic_tertiary30</item>
|
<item name="android:colorAccent">@color/material_dynamic_secondary30</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="OverlayPrimaryColorBlue">
|
<style name="OverlayPrimaryColorBlue">
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
<Preference
|
<Preference
|
||||||
app:isPreferenceVisible="false"
|
|
||||||
android:icon="@drawable/ic_baseline_language_24"
|
android:icon="@drawable/ic_baseline_language_24"
|
||||||
android:key="@string/provider_lang_key"
|
android:key="@string/provider_lang_key"
|
||||||
android:title="@string/provider_lang_settings" />
|
android:title="@string/provider_lang_settings" />
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue