alert fix + synchronized + bump + homepage load fix + small focus change

This commit is contained in:
LagradOst 2023-07-30 05:05:13 +02:00
parent c987f7581e
commit 3bdbb35754
26 changed files with 265 additions and 156 deletions

View file

@ -52,7 +52,7 @@ android {
targetSdk = 33 targetSdk = 33
versionCode = 59 versionCode = 59
versionName = "4.0.1" versionName = "4.1.1"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")

View file

@ -46,9 +46,9 @@ class TestApplication : Activity() {
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest { class ExampleInstrumentedTest {
private fun getAllProviders(): List<MainAPI> { private fun getAllProviders(): Array<MainAPI> {
println("Providers: ${APIHolder.allProviders.size}") println("Providers: ${APIHolder.allProviders.size}")
return APIHolder.allProviders //.filter { !it.usesWebView } return APIHolder.allProviders.toTypedArray() //.filter { !it.usesWebView }
} }
@Test @Test
@ -147,7 +147,7 @@ class ExampleInstrumentedTest {
@Test @Test
fun providerCorrectHomepage() { fun providerCorrectHomepage() {
runBlocking { runBlocking {
getAllProviders().amap { api -> getAllProviders().toList().amap { api ->
TestingUtils.testHomepage(api, ::println) TestingUtils.testHomepage(api, ::println)
} }
} }

View file

@ -9,6 +9,7 @@ import android.content.res.Resources
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.view.View.NO_ID
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity 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 ) // 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( private fun getNextFocus(
act: Activity?, act: Activity?,
view: View?, view: View?,
@ -306,7 +331,7 @@ object CommonActivity {
return null return null
} }
val nextId = when (direction) { var nextId = when (direction) {
FocusDirection.Start -> { FocusDirection.Start -> {
if (view.isRtl()) if (view.isRtl())
view.nextFocusRightId view.nextFocusRightId
@ -330,22 +355,16 @@ object CommonActivity {
} }
} }
// if view not found then return if (nextId == NO_ID) {
if (nextId == -1) return null // if not specified then use forward id
var next = act.findViewById<View?>(nextId) ?: return null nextId = view.nextFocusForwardId
// if view is still not found to next focus then return and let android decide
// because we want closes find, aka when multiple have the same id, we go to parent if (nextId == NO_ID) return null
// 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 var next = act.findViewById<View?>(nextId) ?: return null
}*/
next = localLook(view, nextId) ?: next
var currentLook: View = view var currentLook: View = view
while (currentLook.findViewById<View?>(nextId)?.also { next = it } == null) { while (currentLook.findViewById<View?>(nextId)?.also { next = it } == null) {
@ -362,7 +381,7 @@ object CommonActivity {
return next return next
} }
enum class FocusDirection { private enum class FocusDirection {
Start, Start,
End, End,
Up, Up,
@ -463,6 +482,7 @@ object CommonActivity {
//} //}
} }
/** overrides focus and custom key events */
fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? { fun dispatchKeyEvent(act: Activity?, event: KeyEvent?): Boolean? {
if (act == null) return null if (act == null) return null
val currentFocus = act.currentFocus val currentFocus = act.currentFocus
@ -503,7 +523,9 @@ object CommonActivity {
return true 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()) 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) { if (keyEventListener?.invoke(Pair(event, false)) == true) {
return true return true
} }

View file

@ -50,9 +50,11 @@ object APIHolder {
val allProviders = threadSafeListOf<MainAPI>() val allProviders = threadSafeListOf<MainAPI>()
fun initAll() { fun initAll() {
synchronized(allProviders) {
for (api in allProviders) { for (api in allProviders) {
api.init() api.init()
} }
}
apiMap = null apiMap = null
} }
@ -64,29 +66,37 @@ object APIHolder {
var apiMap: Map<String, Int>? = null var apiMap: Map<String, Int>? = null
fun addPluginMapping(plugin: MainAPI) { fun addPluginMapping(plugin: MainAPI) {
synchronized(apis) {
apis = apis + plugin apis = apis + plugin
}
initMap(true) initMap(true)
} }
fun removePluginMapping(plugin: MainAPI) { fun removePluginMapping(plugin: MainAPI) {
synchronized(apis) {
apis = apis.filter { it != plugin } apis = apis.filter { it != plugin }
}
initMap(true) initMap(true)
} }
private fun initMap(forcedUpdate: Boolean = false) { private fun initMap(forcedUpdate: Boolean = false) {
synchronized(apis) {
if (apiMap == null || forcedUpdate) if (apiMap == null || forcedUpdate)
apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap() apiMap = apis.mapIndexed { index, api -> api.name to index }.toMap()
} }
}
fun getApiFromNameNull(apiName: String?): MainAPI? { fun getApiFromNameNull(apiName: String?): MainAPI? {
if (apiName == null) return null if (apiName == null) return null
synchronized(allProviders) { synchronized(allProviders) {
initMap() initMap()
synchronized(apis) {
return apiMap?.get(apiName)?.let { apis.getOrNull(it) } return apiMap?.get(apiName)?.let { apis.getOrNull(it) }
// Leave the ?. null check, it can crash regardless // Leave the ?. null check, it can crash regardless
?: allProviders.firstOrNull { it.name == apiName } ?: allProviders.firstOrNull { it.name == apiName }
} }
} }
}
fun getApiFromUrlNull(url: String?): MainAPI? { fun getApiFromUrlNull(url: String?): MainAPI? {
if (url == null) return null if (url == null) return null
@ -215,7 +225,7 @@ object APIHolder {
val hashSet = HashSet<String>() val hashSet = HashSet<String>()
val activeLangs = getApiProviderLangSettings() val activeLangs = getApiProviderLangSettings()
val hasUniversal = activeLangs.contains(AllLanguagesName) 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 }) .map { it.name })
/*val set = settingsManager.getStringSet( /*val set = settingsManager.getStringSet(
@ -314,8 +324,9 @@ object APIHolder {
} ?: default } ?: default
val langs = this.getApiProviderLangSettings() val langs = this.getApiProviderLangSettings()
val hasUniversal = langs.contains(AllLanguagesName) val hasUniversal = langs.contains(AllLanguagesName)
val allApis = apis.filter { hasUniversal || langs.contains(it.lang) } val allApis = synchronized(apis) {
.filter { api -> api.hasMainPage || !hasHomePageIsRequired } apis.filter { api -> (hasUniversal || langs.contains(api.lang)) && (api.hasMainPage || !hasHomePageIsRequired) }
}
return if (currentPrefMedia.isEmpty()) { return if (currentPrefMedia.isEmpty()) {
allApis allApis
} else { } else {
@ -736,6 +747,7 @@ fun fixTitle(str: String): String {
.replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else it } .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. * 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() * Make sure you get the scope using: val scope: Scriptable = rhino.initSafeStandardObjects()

View file

@ -382,6 +382,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
this.navigate(R.id.navigation_downloads) this.navigate(R.id.navigation_downloads)
return true return true
} else { } else {
synchronized(apis) {
for (api in apis) { for (api in apis) {
if (str.startsWith(api.mainUrl)) { if (str.startsWith(api.mainUrl)) {
loadResult(str, api.name) loadResult(str, api.name)
@ -391,6 +392,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
} }
}
return false return false
} }
} }
@ -464,7 +466,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
binding?.navHostFragment?.apply { binding?.navHostFragment?.apply {
val params = layoutParams as ConstraintLayout.LayoutParams 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.setMargins(
@ -695,6 +698,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
private fun onAllPluginsLoaded(success: Boolean = false) { private fun onAllPluginsLoaded(success: Boolean = false) {
ioSafe { ioSafe {
pluginsLock.withLock { pluginsLock.withLock {
synchronized(allProviders) {
// Load cloned sites after plugins have been loaded since clones depend on plugins. // Load cloned sites after plugins have been loaded since clones depend on plugins.
try { try {
getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list -> getKey<Array<SettingsGeneral.CustomSite>>(USER_PROVIDER_API)?.let { list ->
@ -720,6 +724,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
} }
} }
} }
}
lateinit var viewModel: ResultViewModel2 lateinit var viewModel: ResultViewModel2
@ -814,6 +819,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
translationX = target.x translationX = target.x
translationY = target.y translationY = target.y
bringToFront()
} }
} }
@ -1195,7 +1201,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
ioSafe { ioSafe {
initAll() initAll()
// No duplicates (which can happen by registerMainAPI) // 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) // val navView: BottomNavigationView = findViewById(R.id.nav_view)
@ -1347,8 +1355,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
}*/ }*/
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
try {
var providersAndroidManifestString = "Current androidmanifest should be:\n" var providersAndroidManifestString = "Current androidmanifest should be:\n"
synchronized(allProviders) {
for (api in allProviders) { for (api in allProviders) {
providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${ providersAndroidManifestString += "<data android:scheme=\"https\" android:host=\"${
api.mainUrl.removePrefix( api.mainUrl.removePrefix(
@ -1356,12 +1364,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
) )
}\" android:pathPrefix=\"/\"/>\n" }\" android:pathPrefix=\"/\"/>\n"
} }
println(providersAndroidManifestString)
} catch (t: Throwable) {
logError(t)
} }
println(providersAndroidManifestString)
} }
handleAppIntent(intent) handleAppIntent(intent)

View file

@ -21,10 +21,11 @@ class CrossTmdbProvider : TmdbProvider() {
return Regex("""[^a-zA-Z0-9-]""").replace(name, "") return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
} }
private val validApis by lazy { private val validApis
apis.filter { it.lang == this.lang && it::class.java != this::class.java } get() =
synchronized(apis) { apis.filter { it.lang == this.lang && it::class.java != this::class.java } }
//.distinctBy { it.uniqueId } //.distinctBy { it.uniqueId }
}
data class CrossMetaData( data class CrossMetaData(
@JsonProperty("isSuccess") val isSuccess: Boolean, @JsonProperty("isSuccess") val isSuccess: Boolean,
@ -60,7 +61,8 @@ class CrossTmdbProvider : TmdbProvider() {
override suspend fun load(url: String): LoadResponse? { override suspend fun load(url: String): LoadResponse? {
val base = super.load(url)?.apply { 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) val matchName = filterName(this.name)
when (this) { when (this) {
is MovieLoadResponse -> { is MovieLoadResponse -> {
@ -98,6 +100,7 @@ class CrossTmdbProvider : TmdbProvider() {
this.dataUrl = this.dataUrl =
CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson() CrossMetaData(true, data.map { it.apiName to it.dataUrl }).toJson()
} }
else -> { else -> {
throw ErrorLoadingException("Nothing besides movies are implemented for this provider") throw ErrorLoadingException("Nothing besides movies are implemented for this provider")
} }

View file

@ -25,7 +25,9 @@ class MultiAnimeProvider : MainAPI() {
} }
} }
private val validApis by lazy { private val validApis
get() =
synchronized(APIHolder.apis) {
APIHolder.apis.filter { APIHolder.apis.filter {
it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains( it.lang == this.lang && it::class.java != this::class.java && it.supportedTypes.contains(
TvType.Anime TvType.Anime
@ -33,6 +35,7 @@ class MultiAnimeProvider : MainAPI() {
} }
} }
private fun filterName(name: String): String { private fun filterName(name: String): String {
return Regex("""[^a-zA-Z0-9-]""").replace(name, "") return Regex("""[^a-zA-Z0-9-]""").replace(name, "")
} }

View file

@ -36,7 +36,9 @@ abstract class Plugin {
Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI") Log.i(PLUGIN_TAG, "Adding ${element.name} (${element.mainUrl}) MainAPI")
element.sourcePlugin = this.__filename element.sourcePlugin = this.__filename
// Race condition causing which would case duplicates if not for distinctBy // Race condition causing which would case duplicates if not for distinctBy
synchronized(APIHolder.allProviders) {
APIHolder.allProviders.add(element) APIHolder.allProviders.add(element)
}
APIHolder.addPluginMapping(element) APIHolder.addPluginMapping(element)
} }
@ -51,10 +53,14 @@ abstract class Plugin {
} }
class Manifest { class Manifest {
@JsonProperty("name") var name: String? = null @JsonProperty("name")
@JsonProperty("pluginClassName") var pluginClassName: String? = null var name: String? = null
@JsonProperty("version") var version: Int? = null @JsonProperty("pluginClassName")
@JsonProperty("requiresResources") var requiresResources: Boolean = false var pluginClassName: String? = null
@JsonProperty("version")
var version: Int? = null
@JsonProperty("requiresResources")
var requiresResources: Boolean = false
} }
/** /**

View file

@ -163,7 +163,8 @@ object PluginManager {
private val classLoaders: MutableMap<PathClassLoader, Plugin> = private val classLoaders: MutableMap<PathClassLoader, Plugin> =
HashMap<PathClassLoader, Plugin>() HashMap<PathClassLoader, Plugin>()
private var loadedLocalPlugins = false var loadedLocalPlugins = false
private set
private val gson = Gson() private val gson = Gson()
private suspend fun maybeLoadPlugin(context: Context, file: File) { private suspend fun maybeLoadPlugin(context: Context, file: File) {
@ -531,10 +532,14 @@ object PluginManager {
} }
// remove all registered apis // remove all registered apis
synchronized(APIHolder.apis) {
APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach { APIHolder.apis.filter { api -> api.sourcePlugin == plugin.__filename }.forEach {
removePluginMapping(it) 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 } extractorApis.removeIf { provider: ExtractorApi -> provider.sourcePlugin == plugin.__filename }
classLoaders.values.removeIf { v -> v == plugin } classLoaders.values.removeIf { v -> v == plugin }

View file

@ -462,7 +462,7 @@ class HomeFragment : Fragment() {
private val apiChangeClickListener = View.OnClickListener { view -> private val apiChangeClickListener = View.OnClickListener { view ->
view.context.selectHomepage(currentApiName) { api -> view.context.selectHomepage(currentApiName) { api ->
homeViewModel.loadAndCancel(api) homeViewModel.loadAndCancel(api, forceReload = true,fromUI = true)
} }
/*val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf() /*val validAPIs = view.context?.filterProviderByPreferredMedia()?.toMutableList() ?: mutableListOf()
@ -652,6 +652,7 @@ class HomeFragment : Fragment() {
} }
homeViewModel.reloadStored() homeViewModel.reloadStored()
homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false)
//loadHomePage(false) //loadHomePage(false)
// nice profile pic on homepage // nice profile pic on homepage

View file

@ -447,12 +447,12 @@ class HomeParentItemAdapterPreview(
(binding as? FragmentHomeHeadTvBinding)?.apply { (binding as? FragmentHomeHeadTvBinding)?.apply {
homePreviewChangeApi.setOnClickListener { view -> homePreviewChangeApi.setOnClickListener { view ->
view.context.selectHomepage(viewModel.repo?.name) { api -> view.context.selectHomepage(viewModel.repo?.name) { api ->
viewModel.loadAndCancel(api) viewModel.loadAndCancel(api, forceReload = true, fromUI = true)
} }
} }
homePreviewChangeApi2.setOnClickListener { view -> homePreviewChangeApi2.setOnClickListener { view ->
view.context.selectHomepage(viewModel.repo?.name) { api -> view.context.selectHomepage(viewModel.repo?.name) { api ->
viewModel.loadAndCancel(api) viewModel.loadAndCancel(api, forceReload = true, fromUI = true)
} }
} }

View file

@ -5,7 +5,6 @@ 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
@ -15,12 +14,22 @@ 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.CommonActivity.activity 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
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.ui.home.HomeFragment.Companion.loadHomepageList
import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchClickCallback 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.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE 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
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllResumeStateIds
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllWatchStateIds
@ -44,7 +51,7 @@ 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.withContext import kotlinx.coroutines.withContext
import java.util.* import java.util.EnumSet
import kotlin.collections.set import kotlin.collections.set
class HomeViewModel : ViewModel() { class HomeViewModel : ViewModel() {
@ -95,7 +102,7 @@ class HomeViewModel : ViewModel() {
private var currentShuffledList: List<SearchResponse> = listOf() private var currentShuffledList: List<SearchResponse> = listOf()
private fun autoloadRepo(): APIRepository { private fun autoloadRepo(): APIRepository {
return APIRepository(apis.first { it.hasMainPage }) return APIRepository(synchronized(apis) { apis.first { it.hasMainPage }})
} }
private val _availableWatchStatusTypes = private val _availableWatchStatusTypes =
@ -177,8 +184,10 @@ class HomeViewModel : ViewModel() {
} }
private var onGoingLoad: Job? = null private var onGoingLoad: Job? = null
private fun loadAndCancel(api: MainAPI?) { private var isCurrentlyLoadingName : String? = null
private fun loadAndCancel(api: MainAPI) {
onGoingLoad?.cancel() onGoingLoad?.cancel()
isCurrentlyLoadingName = api.name
onGoingLoad = load(api) onGoingLoad = load(api)
} }
@ -280,12 +289,12 @@ class HomeViewModel : ViewModel() {
} }
} }
private fun load(api: MainAPI?) = ioSafe { private fun load(api: MainAPI) : Job = ioSafe {
repo = if (api != null) { repo = //if (api != null) {
APIRepository(api) APIRepository(api)
} else { //} else {
autoloadRepo() // autoloadRepo()
} //}
_apiName.postValue(repo?.name) _apiName.postValue(repo?.name)
_randomItems.postValue(listOf()) _randomItems.postValue(listOf())
@ -299,6 +308,7 @@ class HomeViewModel : ViewModel() {
_page.postValue(Resource.Loading()) _page.postValue(Resource.Loading())
_preview.postValue(Resource.Loading()) _preview.postValue(Resource.Loading())
// cancel the current preview expand as that is no longer relevant
addJob?.cancel() addJob?.cancel()
when (val data = repo?.getMainPage(1, null)) { when (val data = repo?.getMainPage(1, null)) {
@ -370,7 +380,7 @@ class HomeViewModel : ViewModel() {
else -> Unit else -> Unit
} }
onGoingLoad = null isCurrentlyLoadingName = null
} }
fun click(callback: SearchClickCallback) { fun click(callback: SearchClickCallback) {
@ -437,33 +447,51 @@ class HomeViewModel : ViewModel() {
loadResult(load.response.url, load.response.apiName, load.action) loadResult(load.response.url, load.response.apiName, load.action)
} }
fun loadAndCancel(preferredApiName: String?, forceReload: Boolean = true) = // only save the key if it is from UI, as we don't want internal functions changing the setting
viewModelScope.launchSafe { fun loadAndCancel(
preferredApiName: String?,
forceReload: Boolean = true,
fromUI: Boolean = false
) =
ioSafe {
// Since plugins are loaded in stages this function can get called multiple times. // 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 // The issue with this is that the homepage may be fetched multiple times while the first request is loading
val api = getApiFromNameNull(preferredApiName) val api = getApiFromNameNull(preferredApiName)
if (!forceReload && api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true) { // api?.let { expandable[it.name]?.list?.list?.isNotEmpty() } == true
return@launchSafe 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) { 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) loadAndCancel(noneApi)
} else if (preferredApiName == randomApi.name) { } 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() val validAPIs = context?.filterProviderByPreferredMedia()
if (validAPIs.isNullOrEmpty()) { if (validAPIs.isNullOrEmpty()) {
// Do not set USER_SELECTED_HOMEPAGE_API when there is no plugins loaded
loadAndCancel(noneApi) loadAndCancel(noneApi)
} else { } else {
val apiRandom = validAPIs.random() val apiRandom = validAPIs.random()
loadAndCancel(apiRandom) 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) { } else if (api == null) {
// API is not found aka not loaded or removed, post the loading
// progress if waiting for plugins, otherwise nothing
if(PluginManager.loadedLocalPlugins) {
loadAndCancel(noneApi) loadAndCancel(noneApi)
} else { } else {
setKey(USER_SELECTED_HOMEPAGE_API, api.name) _page.postValue(Resource.Loading())
}
} else {
// if the api is found, then set it to it and save key
if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name)
loadAndCancel(api) loadAndCancel(api)
} }
} }

View file

@ -163,12 +163,14 @@ class LibraryFragment : Fragment() {
syncId: SyncIdName, syncId: SyncIdName,
apiName: String? = null, apiName: String? = null,
) { ) {
val availableProviders = allProviders.filter { val availableProviders = synchronized(allProviders) {
allProviders.filter {
it.supportedSyncNames.contains(syncId) it.supportedSyncNames.contains(syncId)
}.map { it.name } + }.map { it.name } +
// Add the api if it exists // Add the api if it exists
(APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) } ?: emptyList()) (APIHolder.getApiFromNameNull(apiName)?.let { listOf(it.name) }
?: emptyList())
}
val baseOptions = listOf( val baseOptions = listOf(
LibraryOpenerType.Default, LibraryOpenerType.Default,
LibraryOpenerType.None, LibraryOpenerType.None,

View file

@ -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 -> return allEpisodes.keys.mapNotNull { index ->
val episodes = val episodes =
allEpisodes[index] ?: return@mapNotNull null // this should never happened allEpisodes[index] ?: return@mapNotNull null // this should never happened
@ -1505,13 +1508,14 @@ class ResultViewModel2 : ViewModel() {
} }
val realRecommendations = ArrayList<SearchResponse>() val realRecommendations = ArrayList<SearchResponse>()
val apiNames = apis.filter { val apiNames = synchronized(apis) {
apis.filter {
it.name.contains("gogoanime", true) || it.name.contains("gogoanime", true) ||
it.name.contains("9anime", true) it.name.contains("9anime", true)
}.map { }.map {
it.name it.name
} }
}
meta.recommendations?.forEach { rec -> meta.recommendations?.forEach { rec ->
apiNames.forEach { name -> apiNames.forEach { name ->
realRecommendations.add(rec.copy(apiName = name)) realRecommendations.add(rec.copy(apiName = name))

View file

@ -37,7 +37,7 @@ class SearchViewModel : ViewModel() {
private val _currentHistory: MutableLiveData<List<SearchHistoryItem>> = MutableLiveData() private val _currentHistory: MutableLiveData<List<SearchHistoryItem>> = MutableLiveData()
val currentHistory: LiveData<List<SearchHistoryItem>> get() = _currentHistory 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() { fun clearSearch() {
_searchResponse.postValue(Resource.Success(ArrayList())) _searchResponse.postValue(Resource.Success(ArrayList()))
@ -48,7 +48,7 @@ class SearchViewModel : ViewModel() {
private var onGoingSearch: Job? = null private var onGoingSearch: Job? = null
fun reloadRepos() { fun reloadRepos() {
repos = apis.map { APIRepository(it) } repos = synchronized(apis) { apis.map { APIRepository(it) } }
} }
fun searchAndCancel( fun searchAndCancel(

View file

@ -8,7 +8,9 @@ import android.os.Bundle
import android.view.LayoutInflater 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.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.Preference import androidx.preference.Preference
@ -74,6 +76,7 @@ class SettingsFragment : Fragment() {
settingsToolbar.apply { settingsToolbar.apply {
setTitle(title) setTitle(title)
setNavigationIcon(R.drawable.ic_baseline_arrow_back_24) setNavigationIcon(R.drawable.ic_baseline_arrow_back_24)
children.firstOrNull { it is ImageView }?.tag = getString(R.string.tv_no_focus_tag)
setNavigationOnClickListener { setNavigationOnClickListener {
activity?.onBackPressed() activity?.onBackPressed()
} }

View file

@ -20,6 +20,7 @@ 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.CommonActivity import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.app
import com.lagradost.cloudstream3.databinding.AddRemoveSitesBinding import com.lagradost.cloudstream3.databinding.AddRemoveSitesBinding
@ -188,7 +189,7 @@ class SettingsGeneral : PreferenceFragmentCompat() {
fun showAdd() { fun showAdd() {
val providers = allProviders.distinctBy { it.javaClass }.sortedBy { it.name } val providers = synchronized(allProviders) { allProviders.distinctBy { it.javaClass }.sortedBy { it.name } }
activity?.showDialog( activity?.showDialog(
providers.map { "${it.name} (${it.mainUrl})" }, providers.map { "${it.name} (${it.mainUrl})" },
-1, -1,
@ -221,6 +222,8 @@ class SettingsGeneral : PreferenceFragmentCompat() {
val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang) val newSite = CustomSite(provider.javaClass.simpleName, name, url, realLang)
current.add(newSite) current.add(newSite)
setKey(USER_PROVIDER_API, current.toTypedArray()) setKey(USER_PROVIDER_API, current.toTypedArray())
// reload apis
MainActivity.afterPluginsLoadedEvent.invoke(false)
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }

View file

@ -105,8 +105,10 @@ 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 languages = APIHolder.apis.map { it.lang }.toSet() val languages = synchronized(APIHolder.apis) {
APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName
}
val currentList = current.map { val currentList = current.map {
languages.indexOf(it) languages.indexOf(it)

View file

@ -10,6 +10,7 @@ import com.lagradost.cloudstream3.utils.TestingUtils
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import okhttp3.internal.toImmutableList
class TestViewModel : ViewModel() { class TestViewModel : ViewModel() {
data class TestProgress( data class TestProgress(
@ -81,15 +82,14 @@ class TestViewModel : ViewModel() {
} }
fun init() { fun init() {
val apis = APIHolder.allProviders total = synchronized(APIHolder.allProviders) { APIHolder.allProviders.size }
total = apis.size
updateProgress() updateProgress()
} }
fun startTest() { fun startTest() {
scope = CoroutineScope(Dispatchers.Default) scope = CoroutineScope(Dispatchers.Default)
val apis = APIHolder.allProviders val apis = synchronized(APIHolder.allProviders) { APIHolder.allProviders.toTypedArray() }
total = apis.size total = apis.size
failed = 0 failed = 0
passed = 0 passed = 0

View file

@ -107,7 +107,7 @@ class SetupFragmentExtensions : Fragment() {
if (isSetup) if (isSetup)
if ( if (
// If any available languages // 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) findNavController().navigate(R.id.action_navigation_setup_extensions_to_navigation_setup_provider_languages)
} else { } else {

View file

@ -51,8 +51,8 @@ class SetupFragmentProviderLanguage : Fragment() {
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice) ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
val current = ctx.getApiProviderLangSettings() val current = ctx.getApiProviderLangSettings()
val langs = APIHolder.apis.map { it.lang }.toSet() val langs = synchronized(APIHolder.apis) { APIHolder.apis.map { it.lang }.toSet()
.sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName .sortedBy { SubtitleHelper.fromTwoLettersToLanguage(it) } + AllLanguagesName}
val currentList = val currentList =
current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO current.map { langs.indexOf(it) }.filter { it != -1 } // TODO LOOK INTO

View file

@ -18,6 +18,8 @@ const val USER_PROVIDER_API = "user_custom_sites"
const val PREFERENCES_NAME = "rebuild_preference" const val PREFERENCES_NAME = "rebuild_preference"
// TODO degelgate by value for get & set
object DataStore { object DataStore {
val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule()) val mapper: JsonMapper = JsonMapper.builder().addModule(KotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()

View file

@ -96,10 +96,12 @@ object SyncUtil {
.mapNotNull { it.url }.toMutableList() .mapNotNull { it.url }.toMutableList()
if (type == "anilist") { // TODO MAKE BETTER if (type == "anilist") { // TODO MAKE BETTER
synchronized(apis) {
apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach { apis.filter { it.name.contains("Aniflix", ignoreCase = true) }.forEach {
current.add("${it.mainUrl}/anime/$id") current.add("${it.mainUrl}/anime/$id")
} }
} }
}
return current return current
} }

View file

@ -211,7 +211,7 @@ object TestingUtils {
fun getDeferredProviderTests( fun getDeferredProviderTests(
scope: CoroutineScope, scope: CoroutineScope,
providers: List<MainAPI>, providers: Array<MainAPI>,
logger: (String) -> Unit, logger: (String) -> Unit,
callback: (MainAPI, TestResultProvider) -> Unit callback: (MainAPI, TestResultProvider) -> Unit
) { ) {

View file

@ -1,13 +1,13 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:orientation="vertical" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/home_header" 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_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/primaryBlackBackground" android:background="?attr/primaryBlackBackground"
xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical">
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<View <View
android:id="@+id/home_none_padding" android:id="@+id/home_none_padding"
@ -20,10 +20,10 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2
android:descendantFocusability="blocksDescendants"
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="400dp" android:layout_height="400dp"
android:descendantFocusability="blocksDescendants"
android:orientation="horizontal"> android:orientation="horizontal">
</androidx.viewpager2.widget.ViewPager2> </androidx.viewpager2.widget.ViewPager2>
@ -39,7 +39,9 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_gravity="top|start" android:layout_gravity="top|start"
android:layout_marginStart="@dimen/navbar_width" 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> </FrameLayout>
<LinearLayout <LinearLayout
@ -131,12 +133,14 @@
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/home_preview_change_api2" android:id="@+id/home_preview_change_api2"
style="@style/BlackButton" style="@style/RegularButtonTV"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_gravity="top|start" android:layout_gravity="top|start"
android:layout_marginStart="@dimen/navbar_width" android:layout_marginStart="@dimen/navbar_width"
android:backgroundTint="@color/semiWhite" android:backgroundTint="@color/semiWhite"
android:minWidth="150dp" /> android:minWidth="150dp"
android:nextFocusLeft="@id/nav_rail_view"
android:nextFocusDown="@id/home_watch_child_recyclerview" />
</FrameLayout> </FrameLayout>
<LinearLayout <LinearLayout

View file

@ -75,7 +75,7 @@
<style name="ListViewStyle" parent="Widget.AppCompat.ListView"> <style name="ListViewStyle" parent="Widget.AppCompat.ListView">
<item name="android:divider">@null</item> <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> <item name="android:drawSelectorOnTop">false</item>
</style> </style>
@ -572,6 +572,7 @@
<item name="drawableStartCompat">@drawable/ic_baseline_check_24_listview</item> <item name="drawableStartCompat">@drawable/ic_baseline_check_24_listview</item>
</style> </style>
<style name="NoCheckLabel" parent="@style/AppTextViewStyle"> <style name="NoCheckLabel" parent="@style/AppTextViewStyle">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item> <item name="android:layout_height">wrap_content</item>