diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f4886258..27bd1e48 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -52,7 +52,7 @@ android { targetSdk = 33 versionCode = 59 - versionName = "4.0.1" + versionName = "4.1.1" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index 509ea4b9..df41ef91 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -46,9 +46,9 @@ class TestApplication : Activity() { @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - private fun getAllProviders(): List { + private fun getAllProviders(): Array { 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) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 684e2269..9c7c319e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -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(id)?.let { return it } + currentLook = (currentLook.parent as? View) ?: break + } + return null + } + /*var currentLook: View = view + while (true) { + val tmpNext = currentLook.findViewById(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(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(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(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 } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 86252b40..51d218bf 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -50,8 +50,10 @@ object APIHolder { val allProviders = threadSafeListOf() 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? = 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() 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() diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 3dd5a495..1083ad49 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -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>(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>(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 += "\n" } - println(providersAndroidManifestString) - - } catch (t: Throwable) { - logError(t) } - + println(providersAndroidManifestString) } handleAppIntent(intent) diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt index 07aa904e..5bbb4538 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/CrossTmdbProvider.kt @@ -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() // TODO REMOVE + this.recommendations = + this.recommendations?.filterIsInstance() // 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") } diff --git a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt index e8ac1876..8cfe1e9a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/metaproviders/MultiAnimeProvider.kt @@ -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, "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt index 242baf59..6b7dc90b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/Plugin.kt @@ -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 } /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt index 0dee57eb..49b5a752 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -163,7 +163,8 @@ object PluginManager { private val classLoaders: MutableMap = HashMap() - 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 } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt index eb12c411..a6e1b5e6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt @@ -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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt index ce7b8447..fd2412da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapterPreview.kt @@ -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) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt index 563a326e..a2dc9821 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeViewModel.kt @@ -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 = 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) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt index a20cd5c6..04ef3d96 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryFragment.kt @@ -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, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt index 88f55444..011d133d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultViewModel2.kt @@ -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>, EPISODE_RANGE_SIZE : Int): Map> { + private fun getRanges( + allEpisodes: Map>, + EPISODE_RANGE_SIZE: Int + ): Map> { 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() - 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)) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt index aceda644..320687f8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchViewModel.kt @@ -37,7 +37,7 @@ class SearchViewModel : ViewModel() { private val _currentHistory: MutableLiveData> = MutableLiveData() val currentHistory: LiveData> 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( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt index 070389b0..e53fa91a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsFragment.kt @@ -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() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index b308efc7..85dd9540 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -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) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt index 42a864a6..0bef5e9a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsProviders.kt @@ -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) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt index 2e05baff..4fd24afe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestViewModel.kt @@ -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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt index 138a31a3..4369b22f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentExtensions.kt @@ -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 { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt index 8637fc99..59dcc402 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentProviderLanguage.kt @@ -51,8 +51,8 @@ class SetupFragmentProviderLanguage : Fragment() { ArrayAdapter(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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index e1cedd39..dd2b40a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -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() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt index 817e9235..71d3a1ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SyncUtil.kt @@ -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 diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt index 66e1e504..dd973538 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/TestingUtils.kt @@ -211,7 +211,7 @@ object TestingUtils { fun getDeferredProviderTests( scope: CoroutineScope, - providers: List, + providers: Array, logger: (String) -> Unit, callback: (MainAPI, TestResultProvider) -> Unit ) { diff --git a/app/src/main/res/layout/fragment_home_head_tv.xml b/app/src/main/res/layout/fragment_home_head_tv.xml index 0a2c52b2..8592daea 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -1,13 +1,13 @@ - + android:orientation="vertical"> @@ -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" /> + android:minWidth="150dp" + android:nextFocusLeft="@id/nav_rail_view" + android:nextFocusDown="@id/home_watch_child_recyclerview" /> @null - @drawable/outline_drawable_less + @drawable/outline_drawable_forced false @@ -572,6 +572,7 @@ @drawable/ic_baseline_check_24_listview +