mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
alert fix + synchronized + bump + homepage load fix + small focus change
This commit is contained in:
parent
c987f7581e
commit
3bdbb35754
26 changed files with 265 additions and 156 deletions
|
@ -52,7 +52,7 @@ android {
|
|||
targetSdk = 33
|
||||
|
||||
versionCode = 59
|
||||
versionName = "4.0.1"
|
||||
versionName = "4.1.1"
|
||||
|
||||
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
|
||||
|
||||
|
|
|
@ -46,9 +46,9 @@ class TestApplication : Activity() {
|
|||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
private fun getAllProviders(): List<MainAPI> {
|
||||
private fun getAllProviders(): Array<MainAPI> {
|
||||
println("Providers: ${APIHolder.allProviders.size}")
|
||||
return APIHolder.allProviders //.filter { !it.usesWebView }
|
||||
return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -147,7 +147,7 @@ class ExampleInstrumentedTest {
|
|||
@Test
|
||||
fun providerCorrectHomepage() {
|
||||
runBlocking {
|
||||
getAllProviders().amap { api ->
|
||||
getAllProviders().toList().amap { api ->
|
||||
TestingUtils.testHomepage(api, ::println)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.content.res.Resources
|
|||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.View.NO_ID
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.ComponentActivity
|
||||
|
@ -295,6 +296,30 @@ object CommonActivity {
|
|||
) // THEME IS SET BEFORE VIEW IS CREATED TO APPLY THE THEME TO THE MAIN VIEW
|
||||
}
|
||||
|
||||
/** because we want closes find, aka when multiple have the same id, we go to parent
|
||||
until the correct one is found */
|
||||
private fun localLook(from: View, id: Int): View? {
|
||||
if (id == NO_ID) return null
|
||||
var currentLook: View = from
|
||||
while (true) {
|
||||
currentLook.findViewById<View?>(id)?.let { return it }
|
||||
currentLook = (currentLook.parent as? View) ?: break
|
||||
}
|
||||
return null
|
||||
}
|
||||
/*var currentLook: View = view
|
||||
while (true) {
|
||||
val tmpNext = currentLook.findViewById<View?>(nextId)
|
||||
if (tmpNext != null) {
|
||||
next = tmpNext
|
||||
break
|
||||
}
|
||||
currentLook = currentLook.parent as? View ?: break
|
||||
}*/
|
||||
|
||||
/** recursively looks for a next focus up to a depth of 10,
|
||||
* this is used to override the normal shit focus system
|
||||
* because this application has a lot of invisible views that messes with some tv devices*/
|
||||
private fun getNextFocus(
|
||||
act: Activity?,
|
||||
view: View?,
|
||||
|
@ -306,7 +331,7 @@ object CommonActivity {
|
|||
return null
|
||||
}
|
||||
|
||||
val nextId = when (direction) {
|
||||
var nextId = when (direction) {
|
||||
FocusDirection.Start -> {
|
||||
if (view.isRtl())
|
||||
view.nextFocusRightId
|
||||
|
@ -330,22 +355,16 @@ object CommonActivity {
|
|||
}
|
||||
}
|
||||
|
||||
// if view not found then return
|
||||
if (nextId == -1) return null
|
||||
if (nextId == NO_ID) {
|
||||
// if not specified then use forward id
|
||||
nextId = view.nextFocusForwardId
|
||||
// if view is still not found to next focus then return and let android decide
|
||||
if (nextId == NO_ID) return null
|
||||
}
|
||||
|
||||
var next = act.findViewById<View?>(nextId) ?: return null
|
||||
|
||||
// because we want closes find, aka when multiple have the same id, we go to parent
|
||||
// until the correct one is found
|
||||
/*var currentLook: View = view
|
||||
while (true) {
|
||||
val tmpNext = currentLook.findViewById<View?>(nextId)
|
||||
if (tmpNext != null) {
|
||||
next = tmpNext
|
||||
break
|
||||
}
|
||||
|
||||
currentLook = currentLook.parent as? View ?: break
|
||||
}*/
|
||||
next = localLook(view, nextId) ?: next
|
||||
|
||||
var currentLook: View = view
|
||||
while (currentLook.findViewById<View?>(nextId)?.also { next = it } == null) {
|
||||
|
@ -362,7 +381,7 @@ object CommonActivity {
|
|||
return next
|
||||
}
|
||||
|
||||
enum class FocusDirection {
|
||||
private enum class FocusDirection {
|
||||
Start,
|
||||
End,
|
||||
Up,
|
||||
|
@ -463,6 +482,7 @@ object CommonActivity {
|
|||
//}
|
||||
}
|
||||
|
||||
/** overrides focus and custom key events */
|
||||
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
|
||||
if (act == null) return null
|
||||
val currentFocus = act.currentFocus
|
||||
|
@ -503,7 +523,9 @@ object CommonActivity {
|
|||
return true
|
||||
}
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER && (act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)) {
|
||||
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER &&
|
||||
(act.currentFocus is SearchView || act.currentFocus is SearchView.SearchAutoComplete)
|
||||
) {
|
||||
UIHelper.showInputMethod(act.currentFocus?.findFocus())
|
||||
}
|
||||
|
||||
|
@ -516,6 +538,8 @@ object CommonActivity {
|
|||
|
||||
}
|
||||
|
||||
// if someone else want to override the focus then don't handle the event as it is already
|
||||
// consumed. used in video player
|
||||
if (keyEventListener?.invoke(Pair(event, false)) == true) {
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -50,8 +50,10 @@ object APIHolder {
|
|||
val allProviders = threadSafeListOf<MainAPI>()
|
||||
|
||||
fun initAll() {
|
||||
for (api in allProviders) {
|
||||
api.init()
|
||||
synchronized(allProviders) {
|
||||
for (api in allProviders) {
|
||||
api.init()
|
||||
}
|
||||
}
|
||||
apiMap = null
|
||||
}
|
||||
|
@ -64,27 +66,35 @@ object APIHolder {
|
|||
var apiMap: Map<String, Int>? = null
|
||||
|
||||
fun addPluginMapping(plugin: MainAPI) {
|
||||
apis = apis + plugin
|
||||
synchronized(apis) {
|
||||
apis = apis + plugin
|
||||
}
|
||||
initMap(true)
|
||||
}
|
||||
|
||||
fun removePluginMapping(plugin: MainAPI) {
|
||||
apis = apis.filter { it != plugin }
|
||||
synchronized(apis) {
|
||||
apis = apis.filter { it != plugin }
|
||||
}
|
||||
initMap(true)
|
||||
}
|
||||
|
||||
private fun initMap(forcedUpdate: Boolean = false) {
|
||||
if (apiMap == null || forcedUpdate)
|
||||
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
|
||||
synchronized(apis) {
|
||||
if (apiMap == null || forcedUpdate)
|
||||
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
|
||||
}
|
||||
}
|
||||
|
||||
fun getApiFromNameNull(apiName: String?): MainAPI? {
|
||||
if (apiName == null) return null
|
||||
synchronized(allProviders) {
|
||||
initMap()
|
||||
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
||||
// Leave the ?. null check, it can crash regardless
|
||||
?: allProviders.firstOrNull { it.name == apiName }
|
||||
synchronized(apis) {
|
||||
return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
|
||||
// Leave the ?. null check, it can crash regardless
|
||||
?: allProviders.firstOrNull { it.name == apiName }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -215,7 +225,7 @@ object APIHolder {
|
|||
val hashSet = HashSet<String>()
|
||||
val activeLangs = getApiProviderLangSettings()
|
||||
val hasUniversal = activeLangs.contains(AllLanguagesName)
|
||||
hashSet.addAll(apis.filter { hasUniversal || activeLangs.contains(it.lang) }
|
||||
hashSet.addAll(synchronized(apis) { apis.filter { hasUniversal || activeLangs.contains(it.lang) } }
|
||||
.map { it.name })
|
||||
|
||||
/*val set = settingsManager.getStringSet(
|
||||
|
@ -314,8 +324,9 @@ object APIHolder {
|
|||
} ?: default
|
||||
val langs = this.getApiProviderLangSettings()
|
||||
val hasUniversal = langs.contains(AllLanguagesName)
|
||||
val allApis = apis.filter { hasUniversal || langs.contains(it.lang) }
|
||||
.filter { api -> api.hasMainPage || !hasHomePageIsRequired }
|
||||
val allApis = synchronized(apis) {
|
||||
apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
|
||||
}
|
||||
return if (currentPrefMedia.isEmpty()) {
|
||||
allApis
|
||||
} else {
|
||||
|
@ -736,6 +747,7 @@ fun fixTitle(str: String): String {
|
|||
.replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rhino context in a safe way as it needs to be initialized on the main thread.
|
||||
* Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()
|
||||
|
|
|
@ -382,10 +382,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
this.navigate(R.id.navigation_downloads)
|
||||
return true
|
||||
} else {
|
||||
for (api in apis) {
|
||||
if (str.startsWith(api.mainUrl)) {
|
||||
loadResult(str, api.name)
|
||||
return true
|
||||
synchronized(apis) {
|
||||
for (api in apis) {
|
||||
if (str.startsWith(api.mainUrl)) {
|
||||
loadResult(str, api.name)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -464,9 +466,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
binding?.navHostFragment?.apply {
|
||||
val params = layoutParams as ConstraintLayout.LayoutParams
|
||||
val push = if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
|
||||
val push =
|
||||
if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0
|
||||
|
||||
if(!this.isLtr()) {
|
||||
if (!this.isLtr()) {
|
||||
params.setMargins(
|
||||
params.leftMargin,
|
||||
params.topMargin,
|
||||
|
@ -695,27 +698,29 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
private fun onAllPluginsLoaded(success: Boolean = false) {
|
||||
ioSafe {
|
||||
pluginsLock.withLock {
|
||||
// Load cloned sites after plugins have been loaded since clones depend on plugins.
|
||||
try {
|
||||
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
|
||||
list.forEach { custom ->
|
||||
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
|
||||
?.let {
|
||||
allProviders.add(it.javaClass.newInstance().apply {
|
||||
name = custom.name
|
||||
lang = custom.lang
|
||||
mainUrl = custom.url.trimEnd('/')
|
||||
canBeOverridden = false
|
||||
})
|
||||
}
|
||||
synchronized(allProviders) {
|
||||
// Load cloned sites after plugins have been loaded since clones depend on plugins.
|
||||
try {
|
||||
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
|
||||
list.forEach { custom ->
|
||||
allProviders.firstOrNull { it.javaClass.simpleName == custom.parentJavaClass }
|
||||
?.let {
|
||||
allProviders.add(it.javaClass.newInstance().apply {
|
||||
name = custom.name
|
||||
lang = custom.lang
|
||||
mainUrl = custom.url.trimEnd('/')
|
||||
canBeOverridden = false
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
// it.hashCode() is not enough to make sure they are distinct
|
||||
apis =
|
||||
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
|
||||
APIHolder.apiMap = null
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
// it.hashCode() is not enough to make sure they are distinct
|
||||
apis =
|
||||
allProviders.distinctBy { it.lang + it.name + it.mainUrl + it.javaClass.name }
|
||||
APIHolder.apiMap = null
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -814,6 +819,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
|
||||
translationX = target.x
|
||||
translationY = target.y
|
||||
bringToFront()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -839,10 +845,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
val out = IntArray(2)
|
||||
newFocus.getLocationInWindow(out)
|
||||
val (screenX, screenY) = out
|
||||
var (x,y) = screenX.toFloat() to screenY.toFloat()
|
||||
var (x, y) = screenX.toFloat() to screenY.toFloat()
|
||||
val (currentX, currentY) = focusOutline.translationX to focusOutline.translationY
|
||||
// println(">><<< $x $y $currentX $currentY")
|
||||
if(!newFocus.isLtr()) {
|
||||
// println(">><<< $x $y $currentX $currentY")
|
||||
if (!newFocus.isLtr()) {
|
||||
x = x - focusOutline.rootView.width + newFocus.measuredWidth
|
||||
}
|
||||
|
||||
|
@ -1195,7 +1201,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
ioSafe {
|
||||
initAll()
|
||||
// No duplicates (which can happen by registerMainAPI)
|
||||
apis = allProviders.distinctBy { it }
|
||||
apis = synchronized(allProviders) {
|
||||
allProviders.distinctBy { it }
|
||||
}
|
||||
}
|
||||
|
||||
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
|
||||
|
@ -1347,8 +1355,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
}*/
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
try {
|
||||
var providersAndroidManifestString = "Current androidmanifest should be:\n"
|
||||
var providersAndroidManifestString = "Current androidmanifest should be:\n"
|
||||
synchronized(allProviders) {
|
||||
for (api in allProviders) {
|
||||
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
|
||||
api.mainUrl.removePrefix(
|
||||
|
@ -1356,12 +1364,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
)
|
||||
}\" android:pathPrefix=\"/\"/>\n"
|
||||
}
|
||||
println(providersAndroidManifestString)
|
||||
|
||||
} catch (t: Throwable) {
|
||||
logError(t)
|
||||
}
|
||||
|
||||
println(providersAndroidManifestString)
|
||||
}
|
||||
|
||||
handleAppIntent(intent)
|
||||
|
|
|
@ -21,10 +21,11 @@ class CrossTmdbProvider : TmdbProvider() {
|
|||
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
||||
}
|
||||
|
||||
private val validApis by lazy {
|
||||
apis.filter { it.lang == this.lang && it::class.java != this::class.java }
|
||||
//.distinctBy { it.uniqueId }
|
||||
}
|
||||
private val validApis
|
||||
get() =
|
||||
synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
|
||||
//.distinctBy { it.uniqueId }
|
||||
|
||||
|
||||
data class CrossMetaData(
|
||||
@JsonProperty("isSuccess") val isSuccess: Boolean,
|
||||
|
@ -60,7 +61,8 @@ class CrossTmdbProvider : TmdbProvider() {
|
|||
|
||||
override suspend fun load(url: String): LoadResponse? {
|
||||
val base = super.load(url)?.apply {
|
||||
this.recommendations = this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
||||
this.recommendations =
|
||||
this.recommendations?.filterIsInstance<MovieSearchResponse>() // TODO REMOVE
|
||||
val matchName = filterName(this.name)
|
||||
when (this) {
|
||||
is MovieLoadResponse -> {
|
||||
|
@ -98,6 +100,7 @@ class CrossTmdbProvider : TmdbProvider() {
|
|||
this.dataUrl =
|
||||
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
|
||||
}
|
||||
|
|
|
@ -25,13 +25,16 @@ class MultiAnimeProvider : MainAPI() {
|
|||
}
|
||||
}
|
||||
|
||||
private val validApis by lazy {
|
||||
APIHolder.apis.filter {
|
||||
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
|
||||
TvType.Anime
|
||||
)
|
||||
}
|
||||
}
|
||||
private val validApis
|
||||
get() =
|
||||
synchronized(APIHolder.apis) {
|
||||
APIHolder.apis.filter {
|
||||
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
|
||||
TvType.Anime
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun filterName(name: String): String {
|
||||
return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
|
||||
|
|
|
@ -36,7 +36,9 @@ abstract class Plugin {
|
|||
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
|
||||
element.sourcePlugin = this.__filename
|
||||
// Race condition causing which would case duplicates if not for distinctBy
|
||||
APIHolder.allProviders.add(element)
|
||||
synchronized(APIHolder.allProviders) {
|
||||
APIHolder.allProviders.add(element)
|
||||
}
|
||||
APIHolder.addPluginMapping(element)
|
||||
}
|
||||
|
||||
|
@ -51,10 +53,14 @@ abstract class Plugin {
|
|||
}
|
||||
|
||||
class Manifest {
|
||||
@JsonProperty("name") var name: String? = null
|
||||
@JsonProperty("pluginClassName") var pluginClassName: String? = null
|
||||
@JsonProperty("version") var version: Int? = null
|
||||
@JsonProperty("requiresResources") var requiresResources: Boolean = false
|
||||
@JsonProperty("name")
|
||||
var name: String? = null
|
||||
@JsonProperty("pluginClassName")
|
||||
var pluginClassName: String? = null
|
||||
@JsonProperty("version")
|
||||
var version: Int? = null
|
||||
@JsonProperty("requiresResources")
|
||||
var requiresResources: Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -163,7 +163,8 @@ object PluginManager {
|
|||
private val classLoaders: MutableMap<PathClassLoader, Plugin> =
|
||||
HashMap<PathClassLoader, Plugin>()
|
||||
|
||||
private var loadedLocalPlugins = false
|
||||
var loadedLocalPlugins = false
|
||||
private set
|
||||
private val gson = Gson()
|
||||
|
||||
private suspend fun maybeLoadPlugin(context: Context, file: File) {
|
||||
|
@ -531,10 +532,14 @@ object PluginManager {
|
|||
}
|
||||
|
||||
// remove all registered apis
|
||||
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
|
||||
removePluginMapping(it)
|
||||
synchronized(APIHolder.apis) {
|
||||
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
|
||||
removePluginMapping(it)
|
||||
}
|
||||
}
|
||||
synchronized(APIHolder.allProviders) {
|
||||
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
|
||||
}
|
||||
APIHolder.allProviders.removeIf { provider: MainAPI -> provider.sourcePlugin == plugin.__filename }
|
||||
extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
|
||||
|
||||
classLoaders.values.removeIf { v -> v == plugin }
|
||||
|
|
|
@ -462,7 +462,7 @@ class HomeFragment : Fragment() {
|
|||
|
||||
private val apiChangeClickListener = View.OnClickListener { view ->
|
||||
view.context.selectHomepage(currentApiName) { api ->
|
||||
homeViewModel.loadAndCancel(api)
|
||||
homeViewModel.loadAndCancel(api, forceReload = true,fromUI = true)
|
||||
}
|
||||
/*val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf()
|
||||
|
||||
|
@ -652,6 +652,7 @@ class HomeFragment : Fragment() {
|
|||
}
|
||||
|
||||
homeViewModel.reloadStored()
|
||||
homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false)
|
||||
//loadHomePage(false)
|
||||
|
||||
// nice profile pic on homepage
|
||||
|
|
|
@ -447,12 +447,12 @@ class HomeParentItemAdapterPreview(
|
|||
(binding as? FragmentHomeHeadTvBinding)?.apply {
|
||||
homePreviewChangeApi.setOnClickListener { view ->
|
||||
view.context.selectHomepage(viewModel.repo?.name) { api ->
|
||||
viewModel.loadAndCancel(api)
|
||||
viewModel.loadAndCancel(api, forceReload = true, fromUI = true)
|
||||
}
|
||||
}
|
||||
homePreviewChangeApi2.setOnClickListener { view ->
|
||||
view.context.selectHomepage(viewModel.repo?.name) { api ->
|
||||
viewModel.loadAndCancel(api)
|
||||
viewModel.loadAndCancel(api, forceReload = true, fromUI = true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.lagradost.cloudstream3.*
|
||||
import com.lagradost.cloudstream3.APIHolder.apis
|
||||
import com.lagradost.cloudstream3.APIHolder.filterHomePageListByFilmQuality
|
||||
import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia
|
||||
|
@ -15,12 +14,22 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.context
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
||||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity.activity
|
||||
import com.lagradost.cloudstream3.mvvm.*
|
||||
import com.lagradost.cloudstream3.HomePageList
|
||||
import com.lagradost.cloudstream3.LoadResponse
|
||||
import com.lagradost.cloudstream3.MainAPI
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.SearchResponse
|
||||
import com.lagradost.cloudstream3.amap
|
||||
import com.lagradost.cloudstream3.mvvm.Resource
|
||||
import com.lagradost.cloudstream3.mvvm.debugAssert
|
||||
import com.lagradost.cloudstream3.mvvm.debugWarning
|
||||
import com.lagradost.cloudstream3.mvvm.launchSafe
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.plugins.PluginManager
|
||||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.noneApi
|
||||
import com.lagradost.cloudstream3.ui.APIRepository.Companion.randomApi
|
||||
import com.lagradost.cloudstream3.ui.WatchType
|
||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.loadHomepageList
|
||||
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
|
||||
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
|
||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||
|
@ -30,8 +39,6 @@ import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching
|
|||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
||||
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
|
||||
|
@ -44,7 +51,7 @@ import com.lagradost.cloudstream3.utils.VideoDownloadHelper
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
import java.util.EnumSet
|
||||
import kotlin.collections.set
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
|
@ -95,7 +102,7 @@ class HomeViewModel : ViewModel() {
|
|||
private var currentShuffledList: List<SearchResponse> = listOf()
|
||||
|
||||
private fun autoloadRepo(): APIRepository {
|
||||
return APIRepository(apis.first { it.hasMainPage })
|
||||
return APIRepository(synchronized(apis) { apis.first { it.hasMainPage }})
|
||||
}
|
||||
|
||||
private val _availableWatchStatusTypes =
|
||||
|
@ -177,8 +184,10 @@ class HomeViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
private var onGoingLoad: Job? = null
|
||||
private fun loadAndCancel(api: MainAPI?) {
|
||||
private var isCurrentlyLoadingName : String? = null
|
||||
private fun loadAndCancel(api: MainAPI) {
|
||||
onGoingLoad?.cancel()
|
||||
isCurrentlyLoadingName = api.name
|
||||
onGoingLoad = load(api)
|
||||
}
|
||||
|
||||
|
@ -280,12 +289,12 @@ class HomeViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun load(api: MainAPI?) = ioSafe {
|
||||
repo = if (api != null) {
|
||||
private fun load(api: MainAPI) : Job = ioSafe {
|
||||
repo = //if (api != null) {
|
||||
APIRepository(api)
|
||||
} else {
|
||||
autoloadRepo()
|
||||
}
|
||||
//} else {
|
||||
// autoloadRepo()
|
||||
//}
|
||||
|
||||
_apiName.postValue(repo?.name)
|
||||
_randomItems.postValue(listOf())
|
||||
|
@ -299,6 +308,7 @@ class HomeViewModel : ViewModel() {
|
|||
|
||||
_page.postValue(Resource.Loading())
|
||||
_preview.postValue(Resource.Loading())
|
||||
// cancel the current preview expand as that is no longer relevant
|
||||
addJob?.cancel()
|
||||
|
||||
when (val data = repo?.getMainPage(1, null)) {
|
||||
|
@ -370,7 +380,7 @@ class HomeViewModel : ViewModel() {
|
|||
|
||||
else -> Unit
|
||||
}
|
||||
onGoingLoad = null
|
||||
isCurrentlyLoadingName = null
|
||||
}
|
||||
|
||||
fun click(callback: SearchClickCallback) {
|
||||
|
@ -437,33 +447,51 @@ class HomeViewModel : ViewModel() {
|
|||
loadResult(load.response.url, load.response.apiName, load.action)
|
||||
}
|
||||
|
||||
fun loadAndCancel(preferredApiName: String?, forceReload: Boolean = true) =
|
||||
viewModelScope.launchSafe {
|
||||
// only save the key if it is from UI, as we don't want internal functions changing the setting
|
||||
fun loadAndCancel(
|
||||
preferredApiName: String?,
|
||||
forceReload: Boolean = true,
|
||||
fromUI: Boolean = false
|
||||
) =
|
||||
ioSafe {
|
||||
// Since plugins are loaded in stages this function can get called multiple times.
|
||||
// The issue with this is that the homepage may be fetched multiple times while the first request is loading
|
||||
val api = getApiFromNameNull(preferredApiName)
|
||||
if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) {
|
||||
return@launchSafe
|
||||
// api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true
|
||||
val currentPage = page.value
|
||||
|
||||
// if we don't need to reload and we have a valid homepage or currently loading the same thing then return
|
||||
val currentLoading = isCurrentlyLoadingName
|
||||
if (!forceReload && (currentPage is Resource.Success && currentPage.value.isNotEmpty() || (currentLoading != null && currentLoading == preferredApiName))) {
|
||||
return@ioSafe
|
||||
}
|
||||
|
||||
if (preferredApiName == noneApi.name) {
|
||||
setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
|
||||
// just set to random
|
||||
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name)
|
||||
loadAndCancel(noneApi)
|
||||
} else if (preferredApiName == randomApi.name) {
|
||||
// randomize the api, if none exist like if not loaded or not installed
|
||||
// then use nothing
|
||||
val validAPIs = context?.filterProviderByPreferredMedia()
|
||||
if (validAPIs.isNullOrEmpty()) {
|
||||
// Do not set USER_SELECTED_HOMEPAGE_API when there is no plugins loaded
|
||||
loadAndCancel(noneApi)
|
||||
} else {
|
||||
val apiRandom = validAPIs.random()
|
||||
loadAndCancel(apiRandom)
|
||||
setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name)
|
||||
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name)
|
||||
}
|
||||
// If the plugin isn't loaded yet. (Does not set the key)
|
||||
} else if (api == null) {
|
||||
loadAndCancel(noneApi)
|
||||
// API is not found aka not loaded or removed, post the loading
|
||||
// progress if waiting for plugins, otherwise nothing
|
||||
if(PluginManager.loadedLocalPlugins) {
|
||||
loadAndCancel(noneApi)
|
||||
} else {
|
||||
_page.postValue(Resource.Loading())
|
||||
}
|
||||
} else {
|
||||
setKey(USER_SELECTED_HOMEPAGE_API, api.name)
|
||||
// if the api is found, then set it to it and save key
|
||||
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name)
|
||||
loadAndCancel(api)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,12 +163,14 @@ class LibraryFragment : Fragment() {
|
|||
syncId: SyncIdName,
|
||||
apiName: String? = null,
|
||||
) {
|
||||
val availableProviders = allProviders.filter {
|
||||
it.supportedSyncNames.contains(syncId)
|
||||
}.map { it.name } +
|
||||
// Add the api if it exists
|
||||
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } ?: emptyList())
|
||||
|
||||
val availableProviders = synchronized(allProviders) {
|
||||
allProviders.filter {
|
||||
it.supportedSyncNames.contains(syncId)
|
||||
}.map { it.name } +
|
||||
// Add the api if it exists
|
||||
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
|
||||
?: emptyList())
|
||||
}
|
||||
val baseOptions = listOf(
|
||||
LibraryOpenerType.Default,
|
||||
LibraryOpenerType.None,
|
||||
|
|
|
@ -321,7 +321,7 @@ data class ExtractedTrailerData(
|
|||
|
||||
class ResultViewModel2 : ViewModel() {
|
||||
private var currentResponse: LoadResponse? = null
|
||||
var EPISODE_RANGE_SIZE : Int = 20
|
||||
var EPISODE_RANGE_SIZE: Int = 20
|
||||
fun clear() {
|
||||
currentResponse = null
|
||||
_page.postValue(null)
|
||||
|
@ -456,7 +456,7 @@ class ResultViewModel2 : ViewModel() {
|
|||
currentResponse.year
|
||||
)
|
||||
)
|
||||
if(currentWatchType != status) {
|
||||
if (currentWatchType != status) {
|
||||
MainActivity.bookmarksUpdatedEvent(true)
|
||||
}
|
||||
}
|
||||
|
@ -477,7 +477,10 @@ class ResultViewModel2 : ViewModel() {
|
|||
)
|
||||
)
|
||||
|
||||
private fun getRanges(allEpisodes: Map<EpisodeIndexer, List<ResultEpisode>>, EPISODE_RANGE_SIZE : Int): Map<EpisodeIndexer, List<EpisodeRange>> {
|
||||
private fun getRanges(
|
||||
allEpisodes: Map<EpisodeIndexer, List<ResultEpisode>>,
|
||||
EPISODE_RANGE_SIZE: Int
|
||||
): Map<EpisodeIndexer, List<EpisodeRange>> {
|
||||
return allEpisodes.keys.mapNotNull { index ->
|
||||
val episodes =
|
||||
allEpisodes[index] ?: return@mapNotNull null // this should never happened
|
||||
|
@ -1505,13 +1508,14 @@ class ResultViewModel2 : ViewModel() {
|
|||
}
|
||||
|
||||
val realRecommendations = ArrayList<SearchResponse>()
|
||||
val apiNames = apis.filter {
|
||||
it.name.contains("gogoanime", true) ||
|
||||
it.name.contains("9anime", true)
|
||||
}.map {
|
||||
it.name
|
||||
val apiNames = synchronized(apis) {
|
||||
apis.filter {
|
||||
it.name.contains("gogoanime", true) ||
|
||||
it.name.contains("9anime", true)
|
||||
}.map {
|
||||
it.name
|
||||
}
|
||||
}
|
||||
|
||||
meta.recommendations?.forEach { rec ->
|
||||
apiNames.forEach { name ->
|
||||
realRecommendations.add(rec.copy(apiName = name))
|
||||
|
|
|
@ -37,7 +37,7 @@ class SearchViewModel : ViewModel() {
|
|||
private val _currentHistory: MutableLiveData<List<SearchHistoryItem>> = MutableLiveData()
|
||||
val currentHistory: LiveData<List<SearchHistoryItem>> get() = _currentHistory
|
||||
|
||||
private var repos = apis.map { APIRepository(it) }
|
||||
private var repos = synchronized(apis) { apis.map { APIRepository(it) } }
|
||||
|
||||
fun clearSearch() {
|
||||
_searchResponse.postValue(Resource.Success(ArrayList()))
|
||||
|
@ -48,7 +48,7 @@ class SearchViewModel : ViewModel() {
|
|||
private var onGoingSearch: Job? = null
|
||||
|
||||
fun reloadRepos() {
|
||||
repos = apis.map { APIRepository(it) }
|
||||
repos = synchronized(apis) { apis.map { APIRepository(it) } }
|
||||
}
|
||||
|
||||
fun searchAndCancel(
|
||||
|
|
|
@ -8,7 +8,9 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.core.view.children
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.preference.Preference
|
||||
|
@ -74,6 +76,7 @@ class SettingsFragment : Fragment() {
|
|||
settingsToolbar.apply {
|
||||
setTitle(title)
|
||||
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
|
||||
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
|
||||
setNavigationOnClickListener {
|
||||
activity?.onBackPressed()
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
|
|||
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
||||
import com.lagradost.cloudstream3.CommonActivity
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.MainActivity
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.app
|
||||
import com.lagradost.cloudstream3.databinding.AddRemoveSitesBinding
|
||||
|
@ -188,7 +189,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
|
||||
|
||||
fun showAdd() {
|
||||
val providers = allProviders.distinctBy { it.javaClass }.sortedBy { it.name }
|
||||
val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
|
||||
activity?.showDialog(
|
||||
providers.map { "${it.name} (${it.mainUrl})" },
|
||||
-1,
|
||||
|
@ -221,6 +222,8 @@ class SettingsGeneral : PreferenceFragmentCompat() {
|
|||
val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang)
|
||||
current.add(newSite)
|
||||
setKey(USER_PROVIDER_API, current.toTypedArray())
|
||||
// reload apis
|
||||
MainActivity.afterPluginsLoadedEvent.invoke(false)
|
||||
|
||||
dialog.dismissSafe(activity)
|
||||
}
|
||||
|
|
|
@ -105,8 +105,10 @@ class SettingsProviders : PreferenceFragmentCompat() {
|
|||
|
||||
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
|
||||
activity?.getApiProviderLangSettings()?.let { current ->
|
||||
val languages = APIHolder.apis.map { it.lang }.toSet()
|
||||
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
|
||||
val languages = synchronized(APIHolder.apis) {
|
||||
APIHolder.apis.map { it.lang }.toSet()
|
||||
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
|
||||
}
|
||||
|
||||
val currentList = current.map {
|
||||
languages.indexOf(it)
|
||||
|
|
|
@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.utils.TestingUtils
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import okhttp3.internal.toImmutableList
|
||||
|
||||
class TestViewModel : ViewModel() {
|
||||
data class TestProgress(
|
||||
|
@ -81,15 +82,14 @@ class TestViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
fun init() {
|
||||
val apis = APIHolder.allProviders
|
||||
total = apis.size
|
||||
total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size }
|
||||
updateProgress()
|
||||
}
|
||||
|
||||
fun startTest() {
|
||||
scope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
val apis = APIHolder.allProviders
|
||||
val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() }
|
||||
total = apis.size
|
||||
failed = 0
|
||||
passed = 0
|
||||
|
|
|
@ -107,7 +107,7 @@ class SetupFragmentExtensions : Fragment() {
|
|||
if (isSetup)
|
||||
if (
|
||||
// If any available languages
|
||||
apis.distinctBy { it.lang }.size > 1
|
||||
synchronized(apis) { apis.distinctBy { it.lang }.size > 1 }
|
||||
) {
|
||||
findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
|
||||
} else {
|
||||
|
|
|
@ -51,8 +51,8 @@ class SetupFragmentProviderLanguage : Fragment() {
|
|||
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
|
||||
|
||||
val current = ctx.getApiProviderLangSettings()
|
||||
val langs = APIHolder.apis.map { it.lang }.toSet()
|
||||
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
|
||||
val langs = synchronized(APIHolder.apis) { APIHolder.apis.map { it.lang }.toSet()
|
||||
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName}
|
||||
|
||||
val currentList =
|
||||
current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO
|
||||
|
|
|
@ -18,6 +18,8 @@ const val USER_PROVIDER_API = "user_custom_sites"
|
|||
|
||||
const val PREFERENCES_NAME = "rebuild_preference"
|
||||
|
||||
// TODO degelgate by value for get & set
|
||||
|
||||
object DataStore {
|
||||
val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
|
||||
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
|
||||
|
|
|
@ -96,8 +96,10 @@ object SyncUtil {
|
|||
.mapNotNull { it.url }.toMutableList()
|
||||
|
||||
if (type == "anilist") { // TODO MAKE BETTER
|
||||
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
|
||||
current.add("${it.mainUrl}/anime/$id")
|
||||
synchronized(apis) {
|
||||
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
|
||||
current.add("${it.mainUrl}/anime/$id")
|
||||
}
|
||||
}
|
||||
}
|
||||
return current
|
||||
|
|
|
@ -211,7 +211,7 @@ object TestingUtils {
|
|||
|
||||
fun getDeferredProviderTests(
|
||||
scope: CoroutineScope,
|
||||
providers: List<MainAPI>,
|
||||
providers: Array<MainAPI>,
|
||||
logger: (String) -> Unit,
|
||||
callback: (MainAPI, TestResultProvider) -> Unit
|
||||
) {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout android:orientation="vertical"
|
||||
android:id="@+id/home_header"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/home_header"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/primaryBlackBackground"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/home_none_padding"
|
||||
|
@ -20,10 +20,10 @@
|
|||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:id="@+id/home_preview_viewpager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="400dp"
|
||||
android:descendantFocusability="blocksDescendants"
|
||||
android:orientation="horizontal">
|
||||
|
||||
</androidx.viewpager2.widget.ViewPager2>
|
||||
|
@ -39,7 +39,9 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="top|start"
|
||||
android:layout_marginStart="@dimen/navbar_width"
|
||||
android:minWidth="150dp" />
|
||||
android:minWidth="150dp"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusDown="@id/home_preview_play_btt" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
@ -131,12 +133,14 @@
|
|||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/home_preview_change_api2"
|
||||
style="@style/BlackButton"
|
||||
style="@style/RegularButtonTV"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_gravity="top|start"
|
||||
android:layout_marginStart="@dimen/navbar_width"
|
||||
android:backgroundTint="@color/semiWhite"
|
||||
android:minWidth="150dp" />
|
||||
android:minWidth="150dp"
|
||||
android:nextFocusLeft="@id/nav_rail_view"
|
||||
android:nextFocusDown="@id/home_watch_child_recyclerview" />
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
|
||||
<style name="ListViewStyle" parent="Widget.AppCompat.ListView">
|
||||
<item name="android:divider">@null</item>
|
||||
<item name="android:listSelector">@drawable/outline_drawable_less</item>
|
||||
<item name="android:listSelector">@drawable/outline_drawable_forced</item>
|
||||
<item name="android:drawSelectorOnTop">false</item>
|
||||
</style>
|
||||
|
||||
|
@ -572,6 +572,7 @@
|
|||
<item name="drawableStartCompat">@drawable/ic_baseline_check_24_listview</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="NoCheckLabel" parent="@style/AppTextViewStyle">
|
||||
<item name="android:layout_width">match_parent</item>
|
||||
<item name="android:layout_height">wrap_content</item>
|
||||
|
|
Loading…
Reference in a new issue