From a5450e5da2990b26ac6f7a5c1a60394510ff95a6 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Thu, 14 Sep 2023 10:53:35 +0000 Subject: [PATCH 01/42] More robust player release (#601) --- .../ui/player/AbstractPlayerFragment.kt | 1 + .../cloudstream3/ui/player/CS3IPlayer.kt | 15 +++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 4316bbc6..8388e58f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -517,6 +517,7 @@ abstract class AbstractPlayerFragment( canEnterPipMode = false mMediaSession?.release() mMediaSession = null + playerView?.player = null SubtitlesFragment.applyStyleEvent -= ::onSubStyleChanged keepScreenOn(false) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index fe4e3423..331cfb73 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -51,6 +51,7 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.USER_AGENT import com.lagradost.cloudstream3.app +import com.lagradost.cloudstream3.mvvm.debugAssert import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle @@ -85,6 +86,12 @@ const val toleranceAfterUs = 300_000L class CS3IPlayer : IPlayer { private var isPlaying = false private var exoPlayer: ExoPlayer? = null + set(value) { + // If the old value is not null then the player has not been properly released. + debugAssert({ field != null && value != null }, { "Previous player instance should be released!" }) + field = value + } + var cacheSize = 0L var simpleCacheSize = 0L var videoBufferMs = 0L @@ -682,13 +689,13 @@ class CS3IPlayer : IPlayer { metadataRendererOutput ).map { if (it is TextRenderer) { - currentTextRenderer = CustomTextRenderer( + val currentTextRenderer = CustomTextRenderer( subtitleOffset, textRendererOutput, eventHandler.looper, CustomSubtitleDecoderFactory() - ) - currentTextRenderer!! + ).also { this.currentTextRenderer = it } + currentTextRenderer } else it }.toTypedArray() } @@ -1323,7 +1330,7 @@ class CS3IPlayer : IPlayer { override fun reloadPlayer(context: Context) { Log.i(TAG, "reloadPlayer") - exoPlayer?.release() + releasePlayer(false) currentLink?.let { loadOnlinePlayer(context, it) } ?: currentDownloadedFile?.let { From 2bed79b1f18e7b7ae145377865f427a781197e11 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Thu, 14 Sep 2023 17:53:54 +0700 Subject: [PATCH 02/42] Update Gofile.kt (#600) --- .../main/java/com/lagradost/cloudstream3/extractors/Gofile.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt index d76b0e11..eaf9c65f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Gofile.kt @@ -19,7 +19,7 @@ open class Gofile : ExtractorApi() { subtitleCallback: (SubtitleFile) -> Unit, callback: (ExtractorLink) -> Unit ) { - val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z]+)").find(url)?.groupValues?.get(1) + val id = Regex("/(?:\\?c=|d/)([\\da-zA-Z-]+)").find(url)?.groupValues?.get(1) val token = app.get("$mainApi/createAccount").parsedSafe()?.data?.get("token") val websiteToken = app.get("$mainUrl/dist/js/alljs.js").text.let { Regex("websiteToken\\s*=\\s*\"([^\"]+)").find(it)?.groupValues?.get(1) @@ -59,4 +59,4 @@ open class Gofile : ExtractorApi() { @JsonProperty("data") val data: Data? = null, ) -} \ No newline at end of file +} From 6957a8f95dc7144c5b6c454930950d533c276f95 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 14 Sep 2023 20:30:44 +0200 Subject: [PATCH 03/42] bump --- app/build.gradle.kts | 4 ++-- app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f52d6e5e..66ba16c6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 59 - versionName = "4.1.8" + versionCode = 60 + versionName = "4.1.9" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 0175e0d0..5b674c4c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -179,6 +179,13 @@ object APIHolder { private var trackerCache: HashMap = hashMapOf() + /** backwards compatibility, use getTracker4 instead */ + suspend fun getTracker( + titles: List, + types: Set?, + year: Int?, + ): Tracker? = getTracker(titles, types, year, false) + /** * Get anime tracker information based on title, year and type. * Both titles are attempted to be matched with both Romaji and English title. @@ -192,7 +199,7 @@ object APIHolder { titles: List, types: Set?, year: Int?, - lessAccurate: Boolean = false + lessAccurate: Boolean ): Tracker? { return try { require(titles.isNotEmpty()) { "titles must no be empty when calling getTracker" } From 24977a8d628a3418051e9bbbead8d4a88f7df035 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Fri, 15 Sep 2023 20:47:59 +0000 Subject: [PATCH 04/42] Potential fix for crash loops (#608) --- .../com/lagradost/cloudstream3/MainActivity.kt | 12 ++++++++---- .../cloudstream3/plugins/PluginManager.kt | 14 ++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index a07ae2c2..82a52a2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1092,15 +1092,19 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { updateTv() // backup when we update the app, I don't trust myself to not boot lock users, might want to make this a setting? - try { + normalSafeApiCall { val appVer = BuildConfig.VERSION_NAME val lastAppAutoBackup: String = getKey("VERSION_NAME") ?: "" if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) - backup() + normalSafeApiCall { + backup() + } + normalSafeApiCall { + // Recompile oat on new version + PluginManager.deleteAllOatFiles(this) + } } - } catch (t: Throwable) { - logError(t) } // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH 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 87b0ba3b..5bb96ed1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/plugins/PluginManager.kt @@ -137,6 +137,20 @@ object PluginManager { } } + /** + * Deletes all generated oat files which will force Android to recompile the dex extensions. + * This might fix unrecoverable SIGSEGV exceptions when old oat files are loaded in a new app update. + */ + fun deleteAllOatFiles(context: Context) { + File("${context.filesDir}/${ONLINE_PLUGINS_FOLDER}").listFiles()?.forEach { repo -> + repo.listFiles { file -> file.name == "oat" && file.isDirectory }?.forEach { file -> + val success = file.deleteRecursively() + Log.i(TAG, "Deleted oat directory: ${file.absolutePath} Success=$success") + } + } + } + + fun getPluginsOnline(): Array { return getKey(PLUGINS_KEY) ?: emptyArray() } From 627c1bb223a3cc4b8a917807e91100211f30a859 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Fri, 15 Sep 2023 22:30:34 +0000 Subject: [PATCH 05/42] Many UI fixes (#606) * Lower targetSdk to get all installed packages * Update sdk version * Let's not be too radical * Many fixes * Revert targetSdk * Make account homepage persistent --- .../lagradost/cloudstream3/MainActivity.kt | 25 +- ...RecyclerView.kt => CustomRecyclerViews.kt} | 39 ++- .../cloudstream3/ui/home/HomeFragment.kt | 3 +- .../cloudstream3/ui/home/HomeViewModel.kt | 17 +- .../ui/result/ResultFragmentTv.kt | 4 +- .../ui/result/ResultViewModel2.kt | 3 +- .../ui/settings/SettingsProviders.kt | 4 +- .../ui/setup/SetupFragmentMedia.kt | 4 +- .../cloudstream3/utils/DataStoreHelper.kt | 33 ++- app/src/main/res/drawable/episodes_shadow.xml | 6 +- .../main/res/layout/fragment_result_tv.xml | 239 ++++++++++-------- 11 files changed, 239 insertions(+), 138 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/ui/{AutofitRecyclerView.kt => CustomRecyclerViews.kt} (79%) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 82a52a2a..263a40f0 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -128,6 +128,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main 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.migrateResumeWatching import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.IOnBackPressed @@ -305,6 +306,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { // kinda shitty solution, but cant com main->home otherwise for popups val bookmarksUpdatedEvent = Event() + /** + * Used by data store helper to fully reload home when switching accounts + */ + val reloadHomeEvent = Event() /** @@ -539,6 +544,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val isTrueTv = isTrueTvSettings() navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + + // Hide downloads on TV + navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv } } @@ -1116,16 +1125,17 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> // println("refocus $oldFocus -> $newFocus") try { - val r = Rect(0,0,0,0) + val r = Rect(0, 0, 0, 0) newFocus.getDrawingRect(r) val x = r.centerX() val y = r.centerY() val dx = 0 //screenWidth / 2 val dy = screenHeight / 2 - val r2 = Rect(x-dx,y-dy,x+dx,y+dy) + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_ : Throwable) { } + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } TvFocus.updateFocusView(newFocus) /*var focus = newFocus @@ -1186,7 +1196,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } else if (lastError == null) { ioSafe { - getKey(USER_SELECTED_HOMEPAGE_API)?.let { homeApi -> + DataStoreHelper.currentHomePage?.let { homeApi -> mainPluginsLoadedEvent.invoke(loadSinglePlugin(this@MainActivity, homeApi)) } ?: run { mainPluginsLoadedEvent.invoke(false) @@ -1547,6 +1557,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { migrateResumeWatching() } + getKey(USER_SELECTED_HOMEPAGE_API)?.let { homepage -> + DataStoreHelper.currentHomePage = homepage + removeKey(USER_SELECTED_HOMEPAGE_API) + } + try { if (getKey(HAS_DONE_SETUP_KEY, false) != true) { navController.navigate(R.id.navigation_setup_language) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt similarity index 79% rename from app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt rename to app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt index 28ced48c..1a9549e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/AutofitRecyclerView.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/CustomRecyclerViews.kt @@ -3,6 +3,7 @@ package com.lagradost.cloudstream3.ui import android.content.Context import android.util.AttributeSet import android.view.View +import androidx.core.view.children import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import kotlin.math.abs @@ -70,8 +71,8 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : val orientation = this.orientation // fixes arabic by inverting left and right layout focus - val correctDirection = if(this.isLayoutRTL) { - when(direction) { + val correctDirection = if (this.isLayoutRTL) { + when (direction) { View.FOCUS_RIGHT -> View.FOCUS_LEFT View.FOCUS_LEFT -> View.FOCUS_RIGHT else -> direction @@ -83,12 +84,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return spanCount } + View.FOCUS_UP -> { return -spanCount } + View.FOCUS_RIGHT -> { return 1 } + View.FOCUS_LEFT -> { return -1 } @@ -98,12 +102,15 @@ class GrdLayoutManager(val context: Context, _spanCount: Int) : View.FOCUS_DOWN -> { return 1 } + View.FOCUS_UP -> { return -1 } + View.FOCUS_RIGHT -> { return spanCount } + View.FOCUS_LEFT -> { return -spanCount } @@ -155,4 +162,32 @@ class AutofitRecyclerView @JvmOverloads constructor(context: Context, attrs: Att layoutManager = manager } +} + +/** + * Recyclerview wherein the max item width or height is set by the biggest view to prevent inconsistent view sizes. + */ +class MaxRecyclerView(ctx: Context, attrs: AttributeSet) : RecyclerView(ctx, attrs) { + private var biggestObserved: Int = 0 + private val orientation = LayoutManager.getProperties(context, attrs, 0, 0).orientation + private val isHorizontal = orientation == HORIZONTAL + private fun View.updateMaxSize() { + if (isHorizontal) { + this.minimumHeight = biggestObserved + } else { + this.minimumWidth = biggestObserved + } + } + + override fun onChildAttachedToWindow(child: View) { + child.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + val observed = if (isHorizontal) child.measuredHeight else child.measuredWidth + if (observed > biggestObserved) { + biggestObserved = observed + children.forEach { it.updateMaxSize() } + } else { + child.updateMaxSize() + } + super.onChildAttachedToWindow(child) + } } \ No newline at end of file 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 b84c619e..0797e9a0 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 @@ -69,7 +69,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import java.util.* @@ -669,7 +668,7 @@ class HomeFragment : Fragment() { } homeViewModel.reloadStored() - homeViewModel.loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + homeViewModel.loadAndCancel(DataStoreHelper.currentHomePage, false) //loadHomePage(false) // nice profile pic on homepage 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 b27223ec..13d34b59 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 @@ -49,7 +49,6 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getBookmarkedData import com.lagradost.cloudstream3.utils.DataStoreHelper.getLastWatched import com.lagradost.cloudstream3.utils.DataStoreHelper.getResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API import com.lagradost.cloudstream3.utils.VideoDownloadHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -426,23 +425,29 @@ class HomeViewModel : ViewModel() { } private fun afterPluginsLoaded(forceReload: Boolean) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), forceReload) + loadAndCancel(DataStoreHelper.currentHomePage, forceReload) } private fun afterMainPluginsLoaded(unused: Boolean = false) { - loadAndCancel(getKey(USER_SELECTED_HOMEPAGE_API), false) + loadAndCancel(DataStoreHelper.currentHomePage, false) + } + + private fun reloadHome(unused: Boolean = false) { + loadAndCancel(DataStoreHelper.currentHomePage, true) } init { MainActivity.bookmarksUpdatedEvent += ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent += ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent += ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent += ::reloadHome } override fun onCleared() { MainActivity.bookmarksUpdatedEvent -= ::bookmarksUpdated MainActivity.afterPluginsLoadedEvent -= ::afterPluginsLoaded MainActivity.mainPluginsLoadedEvent -= ::afterMainPluginsLoaded + MainActivity.reloadHomeEvent -= ::reloadHome super.onCleared() } @@ -495,7 +500,7 @@ class HomeViewModel : ViewModel() { val api = getApiFromNameNull(preferredApiName) if (preferredApiName == noneApi.name) { // just set to random - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, noneApi.name) + if (fromUI) DataStoreHelper.currentHomePage = noneApi.name loadAndCancel(noneApi) } else if (preferredApiName == randomApi.name) { // randomize the api, if none exist like if not loaded or not installed @@ -506,7 +511,7 @@ class HomeViewModel : ViewModel() { } else { val apiRandom = validAPIs.random() loadAndCancel(apiRandom) - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, apiRandom.name) + if (fromUI) DataStoreHelper.currentHomePage = apiRandom.name } } else if (api == null) { // API is not found aka not loaded or removed, post the loading @@ -520,7 +525,7 @@ class HomeViewModel : ViewModel() { } } else { // if the api is found, then set it to it and save key - if (fromUI) setKey(USER_SELECTED_HOMEPAGE_API, api.name) + if (fromUI) DataStoreHelper.currentHomePage = api.name loadAndCancel(api) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index be3de52b..c40d995b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -177,7 +177,7 @@ class ResultFragmentTv : Fragment() { isVisible = true } - this.animate().alpha(if (turnVisible) 1.0f else 0.0f).apply { + this.animate().alpha(if (turnVisible) 0.97f else 0.0f).apply { duration = 200 interpolator = DecelerateInterpolator() setListener(object : Animator.AnimatorListener { @@ -294,9 +294,9 @@ class ResultFragmentTv : Fragment() { toggleEpisodes(true) binding?.apply { val views = listOf( + resultDubSelection, resultSeasonSelection, resultRangeSelection, - resultDubSelection, resultEpisodes, resultPlayTrailer, ) 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 b398b54e..6acf476a 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 @@ -518,7 +518,8 @@ class ResultViewModel2 : ViewModel() { val episodeNumber = episodes[currentIndex].episode if (episodeNumber < currentMin) { currentMin = episodeNumber - } else if (episodeNumber > currentMax) { + } + if (episodeNumber > currentMax) { currentMax = episodeNumber } ++currentIndex 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 0bef5e9a..7e57fc5b 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 @@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard @@ -96,7 +96,7 @@ class SettingsProviders : PreferenceFragmentCompat() { this.getString(R.string.prefer_media_type_key), selectedList.map { it.toString() }.toMutableSet() ).apply() - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null //(context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt index 6916cafe..f9197213 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/setup/SetupFragmentMedia.kt @@ -15,8 +15,8 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSetupMediaBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar -import com.lagradost.cloudstream3.utils.USER_SELECTED_HOMEPAGE_API class SetupFragmentMedia : Fragment() { @@ -77,7 +77,7 @@ class SetupFragmentMedia : Fragment() { .apply() // Regenerate set homepage - removeKey(USER_SELECTED_HOMEPAGE_API) + DataStoreHelper.currentHomePage = null } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 2eb2ab01..7bce1b6c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -77,10 +77,28 @@ object DataStoreHelper { var selectedKeyIndex by PreferenceDelegate("$TAG/account_key_index", 0) val currentAccount: String get() = selectedKeyIndex.toString() - private fun setAccount(account: Account) { + /** + * Get or set the current account homepage. + * Setting this does not automatically reload the homepage. + */ + var currentHomePage: String? + get() = getKey("$currentAccount/$USER_SELECTED_HOMEPAGE_API") + set(value) { + val key = "$currentAccount/$USER_SELECTED_HOMEPAGE_API" + if (value == null) { + removeKey(key) + } else { + setKey(key, value) + } + } + + private fun setAccount(account: Account, refreshHomePage: Boolean) { selectedKeyIndex = account.keyIndex showToast(account.name) MainActivity.bookmarksUpdatedEvent(true) + if (refreshHomePage) { + MainActivity.reloadHomeEvent(true) + } } private fun editAccount(context: Context, account: Account, isNewAccount: Boolean) { @@ -112,7 +130,7 @@ object DataStoreHelper { accounts = currentAccounts.toTypedArray() // update UI - setAccount(getDefaultAccount(context)) + setAccount(getDefaultAccount(context), true) MainActivity.bookmarksUpdatedEvent(true) dialog?.dismissSafe() } @@ -161,8 +179,13 @@ object DataStoreHelper { currentAccounts.add(currentEditAccount) } + // Save the current homepage for new accounts + val currentHomePage = DataStoreHelper.currentHomePage + // set the new default account as well as add the key for the new account - setAccount(currentEditAccount) + setAccount(currentEditAccount, false) + DataStoreHelper.currentHomePage = currentHomePage + accounts = currentAccounts.toTypedArray() dialog.dismissSafe() @@ -204,7 +227,7 @@ object DataStoreHelper { ) binding.profilesRecyclerview.adapter = WhoIsWatchingAdapter( selectCallBack = { account -> - setAccount(account) + setAccount(account, true) builder.dismissSafe() }, addAccountCallback = { @@ -353,7 +376,7 @@ object DataStoreHelper { removeKeys(folder2) } - fun deleteBookmarkedData(id : Int?) { + fun deleteBookmarkedData(id: Int?) { if (id == null) return removeKey("$currentAccount/$RESULT_WATCH_STATE", id.toString()) removeKey("$currentAccount/$RESULT_WATCH_STATE_DATA", id.toString()) diff --git a/app/src/main/res/drawable/episodes_shadow.xml b/app/src/main/res/drawable/episodes_shadow.xml index b4cdd382..a77cbf25 100644 --- a/app/src/main/res/drawable/episodes_shadow.xml +++ b/app/src/main/res/drawable/episodes_shadow.xml @@ -1,6 +1,8 @@ + android:centerColor="?attr/primaryBlackBackground" + android:centerX="0.2" + android:endColor="?attr/primaryBlackBackground" + android:startColor="@color/transparent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index 4d236d78..a143fbda 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -535,129 +535,150 @@ https://developer.android.com/design/ui/tv/samples/jet-fit - - - - - - - - - - - - - + tools:visibility="visible"> - + - style="@style/Widget.AppCompat.ProgressBar" - android:layout_gravity="center" - android:layout_width="50dp" - android:layout_height="50dp" />--> + + - - - - + android:focusableInTouchMode="false" + android:importantForAccessibility="no" + android:src="@drawable/episodes_shadow" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/shadow_space_2" + app:layout_constraintTop_toTopOf="parent" /> + + + + + + + + + + + + + + + + + + - Date: Sun, 17 Sep 2023 20:35:01 +0200 Subject: [PATCH 06/42] fix --- .../ui/player/PlayerGeneratorViewModel.kt | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index 42659f8d..3179cb9f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.safeApiCall import com.lagradost.cloudstream3.ui.result.ResultEpisode @@ -15,6 +16,7 @@ import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorUri import kotlinx.coroutines.Job +import kotlinx.coroutines.launch class PlayerGeneratorViewModel : ViewModel() { companion object { @@ -38,6 +40,11 @@ class PlayerGeneratorViewModel : ViewModel() { private val _currentSubtitleYear = MutableLiveData(null) val currentSubtitleYear: LiveData = _currentSubtitleYear + /** + * Save the Episode ID to prevent starting multiple link loading Jobs when preloading links. + */ + private var currentLoadingEpisodeId: Int? = null + fun setSubtitleYear(year: Int?) { _currentSubtitleYear.postValue(year) } @@ -72,18 +79,32 @@ class PlayerGeneratorViewModel : ViewModel() { } fun preLoadNextLinks() { + val id = getId() + // Do not preload if already loading + if (id == currentLoadingEpisodeId) return + Log.i(TAG, "preLoadNextLinks") currentJob?.cancel() - currentJob = viewModelScope.launchSafe { - if (generator?.hasCache == true && generator?.hasNext() == true) { - safeApiCall { - generator?.generateLinks( - type = LoadType.InApp, - clearCache = false, - callback = {}, - subtitleCallback = {}, - offset = 1 - ) + currentLoadingEpisodeId = id + + currentJob = viewModelScope.launch { + try { + if (generator?.hasCache == true && generator?.hasNext() == true) { + safeApiCall { + generator?.generateLinks( + type = LoadType.InApp, + clearCache = false, + callback = {}, + subtitleCallback = {}, + offset = 1 + ) + } + } + } catch (t: Throwable) { + logError(t) + } finally { + if (currentLoadingEpisodeId == id) { + currentLoadingEpisodeId = null } } } @@ -162,14 +183,14 @@ class PlayerGeneratorViewModel : ViewModel() { // load more data _loadingLinks.postValue(Resource.Loading()) val loadingState = safeApiCall { - generator?.generateLinks(type = type,clearCache = clearCache, callback = { + generator?.generateLinks(type = type, clearCache = clearCache, callback = { currentLinks.add(it) // Clone to prevent ConcurrentModificationException normalSafeApiCall { // Extra normalSafeApiCall since .toSet() iterates. _currentLinks.postValue(currentLinks.toSet()) } - }, subtitleCallback = { + }, subtitleCallback = { currentSubs.add(it) normalSafeApiCall { _currentSubs.postValue(currentSubs.toSet()) From 0d2a19b350021427922d7419f485ef1c46550366 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sun, 17 Sep 2023 20:38:59 +0200 Subject: [PATCH 07/42] bump --- app/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 66ba16c6..ca99894d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 60 - versionName = "4.1.9" + versionCode = 61 + versionName = "4.1.10" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") From 2ae5b6cefbaa85fffaa12838a87d5623e15e73ca Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 18 Sep 2023 22:28:26 +0200 Subject: [PATCH 08/42] fixed the fucking updater :skull: --- app/build.gradle.kts | 4 ++-- .../lagradost/cloudstream3/utils/InAppUpdater.kt | 13 +++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ca99894d..0f815f8b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,8 +58,8 @@ android { minSdk = 21 targetSdk = 33 - versionCode = 61 - versionName = "4.1.10" + versionCode = 62 + versionName = "4.2.0" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt index b2c4aa5c..0dce0b2a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/InAppUpdater.kt @@ -109,18 +109,19 @@ class InAppUpdater { releases.sortedWith(compareBy { versionRegex.find(it.name)?.groupValues?.get(2) }).toList().lastOrNull()*/ - val found = + val foundList = response.filter { rel -> !rel.prerelease }.sortedWith(compareBy { release -> - release.assets.filter { it.content_type == "application/vnd.android.package-archive" } - .getOrNull(0)?.name?.let { it1 -> + release.assets.firstOrNull { it.content_type == "application/vnd.android.package-archive" }?.name?.let { it1 -> versionRegex.find( it1 - )?.groupValues?.get(2) + )?.groupValues?.let { + it[3].toInt() * 100_000_000 + it[4].toInt() * 10_000 + it[5].toInt() + } } - }).toList().lastOrNull() - + }).toList() + val found = foundList.lastOrNull() val foundAsset = found?.assets?.getOrNull(0) val currentVersion = packageName?.let { packageManager.getPackageInfo( From 15333123cd298357baf85fa2c5f044ada60481d6 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Mon, 18 Sep 2023 21:22:39 +0000 Subject: [PATCH 09/42] TV UI fixes (#612) * TV UI fixes --- .../cloudstream3/ui/result/ActorAdaptor.kt | 26 +++++++++++--- .../ui/result/ResultFragmentTv.kt | 35 +++++++++++++------ .../ui/settings/SettingsFragment.kt | 5 +++ .../main/res/layout/fragment_search_tv.xml | 2 +- 4 files changed, 51 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt index 531cb5d2..7b743388 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ActorAdaptor.kt @@ -3,6 +3,8 @@ package com.lagradost.cloudstream3.ui.result import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView @@ -12,7 +14,10 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.CastItemBinding import com.lagradost.cloudstream3.utils.UIHelper.setImage -class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerView.Adapter() { +class ActorAdaptor( + private var nextFocusUpId: Int? = null, + private val focusCallback: (View?) -> Unit = {} +) : RecyclerView.Adapter() { data class ActorMetaData( var isInverted: Boolean, val actor: ActorData, @@ -22,7 +27,8 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return CardViewHolder( - CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), focusCallback + CastItemBinding.inflate(LayoutInflater.from(parent.context), parent, false), + focusCallback ) } @@ -64,10 +70,10 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV } } - private class CardViewHolder + private inner class CardViewHolder constructor( val binding: CastItemBinding, - private val focusCallback : (View?) -> Unit = {} + private val focusCallback: (View?) -> Unit = {} ) : RecyclerView.ViewHolder(binding.root) { @@ -78,8 +84,18 @@ class ActorAdaptor(private val focusCallback : (View?) -> Unit = {}) : RecyclerV Pair(actor.voiceActor?.image, actor.actor.image) } + // Fix tv focus escaping the recyclerview + if (position == 0) { + itemView.nextFocusLeftId = R.id.result_cast_items + } else if ((position - 1) == itemCount) { + itemView.nextFocusRightId = R.id.result_cast_items + } + nextFocusUpId?.let { + itemView.nextFocusUpId = it + } + itemView.setOnFocusChangeListener { v, hasFocus -> - if(hasFocus) { + if (hasFocus) { focusCallback(v) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index c40d995b..4503cb88 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -114,10 +114,20 @@ class ResultFragmentTv : Fragment() { } } - private fun hasNoFocus(): Boolean { - val focus = activity?.currentFocus - if (focus == null || !focus.isVisible) return true - return focus == binding?.resultRoot +// private fun hasNoFocus(): Boolean { +// val focus = activity?.currentFocus +// if (focus == null || !focus.isVisible) return true +// return focus == binding?.resultRoot +// } + + /** + * Force focus any play button. + * Note that this will steal any focus if the episode loading is too slow (unlikely). + */ + private fun focusPlayButton() { + binding?.resultPlayMovie?.requestFocus() + binding?.resultPlaySeries?.requestFocus() + binding?.resultResumeSeries?.requestFocus() } private fun setRecommendations(rec: List?, validApiName: String?) { @@ -413,7 +423,13 @@ class ResultFragmentTv : Fragment() { setHorizontal() } - resultCastItems.adapter = ActorAdaptor { + val aboveCast = listOf( + binding?.resultEpisodesShow, + binding?.resultBookmarkButton, + ).firstOrNull { + it?.isVisible == true + } + resultCastItems.adapter = ActorAdaptor(aboveCast?.id) { toggleEpisodes(false) } } @@ -454,9 +470,7 @@ class ResultFragmentTv : Fragment() { resultPlaySeries.isVisible = false resultResumeSeries.isVisible = true - if (hasNoFocus()) { - resultResumeSeries.requestFocus() - } + focusPlayButton() resultResumeSeries.text = if (resume.isMovie) context?.getString(R.string.play_movie_button) else context?.getNameFull( @@ -539,9 +553,7 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } - if (hasNoFocus()) { - resultPlayMovie.requestFocus() - } + focusPlayButton() } } } @@ -663,6 +675,7 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } + focusPlayButton() } /* 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 e53fa91a..4895b0d2 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 @@ -205,6 +205,11 @@ class SettingsFragment : Fragment() { } } } + + // Default focus on TV + if (isTrueTv) { + settingsGeneral.requestFocus() + } } } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index 4c4af404..5fec8c6a 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -99,7 +99,7 @@ android:id="@+id/search_autofit_results" android:layout_width="match_parent" android:layout_height="match_parent" - + android:layout_marginStart="@dimen/navbar_width" android:background="?attr/primaryBlackBackground" android:clipToPadding="false" android:descendantFocusability="afterDescendants" From 527a6388a96a4fa961e1544e6b54acdf6a6f5ee5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Thu, 21 Sep 2023 22:46:23 +0200 Subject: [PATCH 10/42] small fix --- .../lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 4503cb88..7c784a43 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -648,6 +648,9 @@ class ResultFragmentTv : Fragment() { .show() } } + + // Used to request focus the first time the episodes are loaded. + var hasLoadedEpisodesOnce = false observeNullable(viewModel.episodes) { episodes -> binding?.apply { resultEpisodes.isVisible = episodes is Resource.Success @@ -675,7 +678,10 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } - focusPlayButton() + if (!hasLoadedEpisodesOnce) { + hasLoadedEpisodesOnce = true + focusPlayButton() + } } /* From d4fff7cee67d8f7b5e610caf98c5d1816cabac83 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Thu, 21 Sep 2023 20:50:31 +0000 Subject: [PATCH 11/42] Add homepage search on TV (#618) * Add search button on homepage for TV --- .../lagradost/cloudstream3/MainActivity.kt | 1 + .../ui/home/HomeParentItemAdapterPreview.kt | 5 ++++ .../ui/quicksearch/QuickSearchFragment.kt | 5 ++++ app/src/main/res/layout/fragment_home.xml | 16 +++++++++++++ .../main/res/layout/fragment_home_head_tv.xml | 20 ++++++++++++++-- app/src/main/res/layout/fragment_home_tv.xml | 24 +++++++++++++++++-- 6 files changed, 67 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 263a40f0..627893c3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -497,6 +497,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { R.id.navigation_results_phone, R.id.navigation_results_tv, R.id.navigation_player, + R.id.navigation_quick_search, ).contains(destination.id) binding?.navHostFragment?.apply { 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 1d8e1399..d7956f39 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 @@ -461,6 +461,11 @@ class HomeParentItemAdapterPreview( } } + homePreviewSearchButton.setOnClickListener { _ -> + // Open blank screen. + viewModel.queryTextSubmit("") + } + // This makes the hidden next buttons only available when on the info button // Otherwise you might be able to go to the next item without being at the info button homePreviewInfoBtt.setOnFocusChangeListener { _, hasFocus -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt index 89a09ae2..53c7c2fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/quicksearch/QuickSearchFragment.kt @@ -33,6 +33,7 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchViewModel +import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -269,6 +270,10 @@ class QuickSearchFragment : Fragment() { activity?.popCurrentPage() } + if (isTrueTvSettings()) { + binding?.quickSearch?.requestFocus() + } + arguments?.getString(AUTOSEARCH_KEY)?.let { binding?.quickSearch?.setQuery(it, true) arguments?.remove(AUTOSEARCH_KEY) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index ac660ccd..36cb5f42 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -114,6 +114,22 @@ android:nextFocusRight="@id/home_switch_account" /> + + + + + android:nextFocusRight="@id/home_preview_search_button" + android:nextFocusDown="@id/home_preview_play_btt" /> + + Date: Mon, 25 Sep 2023 09:40:58 +0000 Subject: [PATCH 12/42] Fix episode layout when using RTL language (#631) * Fix episode layout when using RTL language --- .../lagradost/cloudstream3/ui/result/ResultFragmentTv.kt | 6 ++---- app/src/main/res/layout/fragment_result_tv.xml | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt index 7c784a43..5e4869cc 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentTv.kt @@ -216,11 +216,9 @@ class ResultFragmentTv : Fragment() { episodesShadow.fade(show) episodeHolderTv.fade(show) if (episodesShadow.isRtl()) { - episodesShadow.scaleX = -1.0f - episodesShadow.scaleY = -1.0f + episodesShadowBackground.scaleX = -1f } else { - episodesShadow.scaleX = 1.0f - episodesShadow.scaleY = 1.0f + episodesShadowBackground.scaleX = 1f } } } diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index a143fbda..feaf6fbc 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -572,6 +572,7 @@ https://developer.android.com/design/ui/tv/samples/jet-fit app:layout_constraintTop_toTopOf="@+id/shadow_space_1" /> Date: Mon, 25 Sep 2023 17:18:05 +0530 Subject: [PATCH 13/42] refactor: speedostream and newpipe, tmdb update (#632) * migrate speedostream (yomovies) * update tmdb and newpipe --- app/build.gradle.kts | 14 ++++++-------- .../{SpeedoStream.kt => Minoplres.kt} | 17 ++++------------- .../cloudstream3/utils/ExtractorApi.kt | 8 ++------ 3 files changed, 12 insertions(+), 27 deletions(-) rename app/src/main/java/com/lagradost/cloudstream3/extractors/{SpeedoStream.kt => Minoplres.kt} (74%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0f815f8b..f13095fb 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -171,7 +171,7 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") androidTestImplementation("androidx.test:core") - //implementation("io.karn:khttp-android:0.1.2") //okhttp instead + // implementation("io.karn:khttp-android:0.1.2") //okhttp instead // implementation("org.jsoup:jsoup:1.13.1") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") @@ -199,8 +199,6 @@ dependencies { // Custom ffmpeg extension for audio codecs implementation("com.github.recloudstream:media-ffmpeg:1.1.0") - //implementation("com.google.android.exoplayer:extension-leanback:2.14.0") - // Bug reports implementation("ch.acra:acra-core:5.11.0") implementation("ch.acra:acra-toast:5.11.0") @@ -214,13 +212,13 @@ dependencies { // subtitle color picker implementation("com.jaredrummler:colorpicker:1.1.0") - //run JS + // run JS // do not upgrade to 1.7.14, since in 1.7.14 Rhino uses the `SourceVersion` class, which is not // available on Android (even when using desugaring), and `NoClassDefFoundError` is thrown implementation("org.mozilla:rhino:1.7.13") // TorrentStream - //implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0") + // implementation("com.github.TorrentStream:TorrentStream-Android:2.7.0") // Downloading implementation("androidx.work:work-runtime:2.8.1") @@ -236,11 +234,11 @@ dependencies { implementation("com.github.LagradOst:SafeFile:0.0.5") // API because cba maintaining it myself - implementation("com.uwetrottmann.tmdb2:tmdb-java:2.6.0") + implementation("com.uwetrottmann.tmdb2:tmdb-java:2.10.0") implementation("com.github.discord:OverlappingPanels:0.1.5") // debugImplementation because LeakCanary should only run in debug builds. - //debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") + // debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12") // for shimmer when loading implementation("com.facebook.shimmer:shimmer:0.5.0") @@ -252,7 +250,7 @@ dependencies { // newpipe yt taken from https://github.com/TeamNewPipe/NewPipeExtractor/commits/dev // this should be updated frequently to avoid trailer fu*kery - implementation("com.github.teamnewpipe:NewPipeExtractor:1f08d28") + implementation("com.github.teamnewpipe:NewPipeExtractor:917554a") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.6") // Library/extensions searching with Levenshtein distance diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt similarity index 74% rename from app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt rename to app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt index 213ecdf3..702501a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/SpeedoStream.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Minoplres.kt @@ -7,21 +7,12 @@ import com.lagradost.cloudstream3.utils.ExtractorApi import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.M3u8Helper -class SpeedoStream2 : SpeedoStream() { - override val mainUrl = "https://speedostream.mom" -} +open class Minoplres : ExtractorApi() { -class SpeedoStream1 : SpeedoStream() { - override val mainUrl = "https://speedostream.pm" -} - -open class SpeedoStream : ExtractorApi() { - override val name = "SpeedoStream" - override val mainUrl = "https://speedostream.bond" + override val name = "Minoplres" // formerly SpeedoStream override val requiresReferer = true - - // .bond, .pm, .mom redirect to .bond - private val hostUrl = "https://speedostream.bond" + override val mainUrl = "https://minoplres.xyz" // formerly speedostream.bond + private val hostUrl = "https://minoplres.xyz" override suspend fun getUrl(url: String, referer: String?): List { val sources = mutableListOf() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 5edff7a1..9db62dc8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -82,6 +82,7 @@ import com.lagradost.cloudstream3.extractors.Maxstream import com.lagradost.cloudstream3.extractors.Mcloud import com.lagradost.cloudstream3.extractors.Megacloud import com.lagradost.cloudstream3.extractors.Meownime +import com.lagradost.cloudstream3.extractors.Minoplres import com.lagradost.cloudstream3.extractors.MixDrop import com.lagradost.cloudstream3.extractors.MixDropBz import com.lagradost.cloudstream3.extractors.MixDropCh @@ -118,9 +119,6 @@ import com.lagradost.cloudstream3.extractors.Sbthe import com.lagradost.cloudstream3.extractors.Sendvid import com.lagradost.cloudstream3.extractors.ShaveTape import com.lagradost.cloudstream3.extractors.Solidfiles -import com.lagradost.cloudstream3.extractors.SpeedoStream -import com.lagradost.cloudstream3.extractors.SpeedoStream1 -import com.lagradost.cloudstream3.extractors.SpeedoStream2 import com.lagradost.cloudstream3.extractors.Ssbstream import com.lagradost.cloudstream3.extractors.StreamM4u import com.lagradost.cloudstream3.extractors.StreamSB @@ -748,9 +746,7 @@ val extractorApis: MutableList = arrayListOf( Vido(), Linkbox(), Acefile(), - SpeedoStream(), - SpeedoStream1(), - SpeedoStream2(), + Minoplres(), // formerly SpeedoStream Zorofile(), Embedgram(), Mvidoo(), From 74060e7da32e0f8af23051149b383c92a6b1efb2 Mon Sep 17 00:00:00 2001 From: Sofie <117321707+Sofie99@users.noreply.github.com> Date: Mon, 25 Sep 2023 18:48:35 +0700 Subject: [PATCH 14/42] Chillx: fix key (#628) --- .../main/java/com/lagradost/cloudstream3/extractors/Chillx.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt index bcf8848c..a9fafc39 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/extractors/Chillx.kt @@ -29,7 +29,7 @@ open class Chillx : ExtractorApi() { override val requiresReferer = true companion object { - private const val KEY = "m4H6D9%0\$N&F6rQ&" + private const val KEY = "eN0^>\$^#M08uFv%c" } override suspend fun getUrl( From 0351053d809ef9e86c5faabf29702c2c50d9409c Mon Sep 17 00:00:00 2001 From: Osten <11805592+LagradOst@users.noreply.github.com> Date: Mon, 25 Sep 2023 22:57:18 +0200 Subject: [PATCH 15/42] Readded downloads to tv --- app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 627893c3..d5187029 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -547,8 +547,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv // Hide downloads on TV - navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv - navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + //navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv + //navRailView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv } } From 194678c419343cc8c441bf7946906b039d6e47a6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 28 Sep 2023 13:21:03 +0300 Subject: [PATCH 16/42] Player Source & Subs navigation change (#633) --- app/src/main/res/layout/player_select_source_and_subs.xml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml index 7351a41f..f5eab1e7 100644 --- a/app/src/main/res/layout/player_select_source_and_subs.xml +++ b/app/src/main/res/layout/player_select_source_and_subs.xml @@ -66,8 +66,8 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/sort_subtitles" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" tools:layout_height="100dp" tools:listitem="@layout/sort_bottom_single_choice" /> @@ -140,7 +140,8 @@ android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" android:nextFocusLeft="@id/sort_providers" - android:nextFocusRight="@id/cancel_btt" + android:nextFocusRight="@id/apply_btt" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" tools:layout_height="200dp" tools:listfooter="@layout/sort_bottom_footer_add_choice" From 16c2290090a1becb4edab44bf85a54837d6007f0 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Thu, 28 Sep 2023 13:22:51 +0300 Subject: [PATCH 17/42] Amazon FireTV focus fixes (#635) * Fix quick search button focus * Switch profile button focus * Cast & Recommendations focus * Player: Profiles settings focus * Player: Subtitles encoding settings focus * profile selection: card item focus * Search history item selectable & deleteable * Search: search filter button next focus fix --- app/src/main/res/layout/cast_item.xml | 2 +- app/src/main/res/layout/fragment_home_head_tv.xml | 2 ++ app/src/main/res/layout/fragment_search_tv.xml | 2 +- app/src/main/res/layout/player_quality_profile_item.xml | 1 + app/src/main/res/layout/player_select_source_and_subs.xml | 4 ++++ app/src/main/res/layout/search_history_item.xml | 4 ++-- 6 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/layout/cast_item.xml b/app/src/main/res/layout/cast_item.xml index 54df59a8..c09cecfa 100644 --- a/app/src/main/res/layout/cast_item.xml +++ b/app/src/main/res/layout/cast_item.xml @@ -34,7 +34,7 @@ android:id="@+id/actor_image" android:layout_width="match_parent" - + android:focusable="true" android:layout_height="match_parent" android:contentDescription="@string/episode_poster_img_des" android:scaleType="centerCrop" 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 f9ea6974..05cb3a41 100644 --- a/app/src/main/res/layout/fragment_home_head_tv.xml +++ b/app/src/main/res/layout/fragment_home_head_tv.xml @@ -55,6 +55,7 @@ android:layout_gravity="end" android:background="@drawable/player_button_tv_attr_no_bg" android:contentDescription="@string/search" + android:focusable="true" android:nextFocusLeft="@id/home_preview_change_api" android:nextFocusRight="@id/home_preview_switch_account" android:nextFocusDown="@id/home_preview_info_btt" @@ -70,6 +71,7 @@ android:layout_gravity="end" android:background="@drawable/player_button_tv_attr_no_bg" android:contentDescription="@string/account" + android:focusable="true" android:nextFocusLeft="@id/home_preview_search_button" android:nextFocusRight="@id/home_preview_switch_account" android:nextFocusDown="@id/home_preview_info_btt" diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index 5fec8c6a..b3f88cd2 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -84,7 +84,7 @@ android:nextFocusLeft="@id/main_search" android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" - android:nextFocusDown="@id/search_autofit_results" + android:nextFocusDown="@id/tvtypes_chips_scroll" android:src="@drawable/ic_baseline_tune_24" app:tint="?attr/textColor" /> diff --git a/app/src/main/res/layout/player_quality_profile_item.xml b/app/src/main/res/layout/player_quality_profile_item.xml index 0eab2407..5178a12f 100644 --- a/app/src/main/res/layout/player_quality_profile_item.xml +++ b/app/src/main/res/layout/player_quality_profile_item.xml @@ -10,6 +10,7 @@ android:id="@+id/card_view" android:layout_width="0dp" android:layout_height="0dp" + android:focusable="true" app:layout_constraintDimensionRatio="1" android:layout_marginStart="10dp" android:animateLayoutChanges="true" diff --git a/app/src/main/res/layout/player_select_source_and_subs.xml b/app/src/main/res/layout/player_select_source_and_subs.xml index f5eab1e7..6bf8006b 100644 --- a/app/src/main/res/layout/player_select_source_and_subs.xml +++ b/app/src/main/res/layout/player_select_source_and_subs.xml @@ -27,6 +27,7 @@ android:layout_height="wrap_content" android:layout_rowWeight="1" android:layout_marginTop="10dp" + android:focusable="true" android:foreground="@drawable/outline_drawable_forced" android:gravity="center_vertical" android:orientation="horizontal"> @@ -66,6 +67,7 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" + android:nextFocusUp="@id/profiles_click_settings" android:nextFocusRight="@id/sort_subtitles" android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" @@ -94,6 +96,7 @@ android:layout_height="wrap_content" android:layout_rowWeight="1" android:layout_marginTop="10dp" + android:focusable="true" android:foreground="@drawable/outline_drawable_forced" android:orientation="horizontal" android:paddingTop="10dp" @@ -139,6 +142,7 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_forced" + android:nextFocusUp="@id/subtitles_click_settings" android:nextFocusLeft="@id/sort_providers" android:nextFocusRight="@id/apply_btt" android:nextFocusDown="@id/apply_btt" diff --git a/app/src/main/res/layout/search_history_item.xml b/app/src/main/res/layout/search_history_item.xml index 3e9ee833..4c50d0c0 100644 --- a/app/src/main/res/layout/search_history_item.xml +++ b/app/src/main/res/layout/search_history_item.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/outline_drawable_less" - + android:focusable="true" android:nextFocusRight="@id/home_history_remove" android:orientation="horizontal"> diff --git a/app/src/main/res/layout/player_select_source_priority.xml b/app/src/main/res/layout/player_select_source_priority.xml index 86a8a756..2af3c339 100644 --- a/app/src/main/res/layout/player_select_source_priority.xml +++ b/app/src/main/res/layout/player_select_source_priority.xml @@ -42,8 +42,8 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_less" - android:nextFocusLeft="@id/sort_subtitles" - android:nextFocusRight="@id/apply_btt" + android:nextFocusRight="@id/sort_subtitles" + android:nextFocusDown="@id/profile_text_editable" android:requiresFadingEdge="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:layout_height="100dp" @@ -92,6 +92,8 @@ android:layout_height="50dp" android:background="?attr/selectableItemBackgroundBorderless" android:padding="12dp" + android:focusable="true" + android:nextFocusLeft="@id/sort_sources" android:src="@drawable/baseline_help_outline_24" android:contentDescription="@string/help" /> @@ -115,8 +117,10 @@ android:layout_rowWeight="1" android:background="?attr/primaryBlackBackground" android:listSelector="@drawable/outline_drawable_less" - android:nextFocusLeft="@id/sort_providers" - android:nextFocusRight="@id/cancel_btt" + android:nextFocusLeft="@id/sort_sources" + android:nextFocusRight="@id/apply_btt" + android:nextFocusUp="@id/help_btt" + android:nextFocusDown="@id/apply_btt" android:requiresFadingEdge="vertical" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" tools:layout_height="200dp" diff --git a/app/src/main/res/layout/subtitle_offset.xml b/app/src/main/res/layout/subtitle_offset.xml index a98fafef..c17c5eff 100644 --- a/app/src/main/res/layout/subtitle_offset.xml +++ b/app/src/main/res/layout/subtitle_offset.xml @@ -35,6 +35,7 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusRight="@id/subtitle_offset_subtract" android:padding="10dp" android:src="@drawable/ic_baseline_keyboard_arrow_left_24" @@ -48,6 +49,7 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusLeft="@id/subtitle_offset_subtract_more" android:padding="10dp" android:src="@drawable/baseline_remove_24" @@ -70,6 +72,7 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusRight="@id/subtitle_offset_add_more" android:padding="10dp" android:src="@drawable/ic_baseline_add_24" @@ -83,7 +86,9 @@ android:layout_gravity="center" android:layout_weight="1" android:background="?android:attr/selectableItemBackgroundBorderless" + android:focusable="true" android:nextFocusLeft="@id/subtitle_offset_add" + android:nextFocusDown="@id/apply_btt" android:padding="10dp" android:src="@drawable/ic_baseline_keyboard_arrow_right_24" app:tint="?attr/white" diff --git a/app/src/main/res/layout/who_is_watching_account.xml b/app/src/main/res/layout/who_is_watching_account.xml index afa1a2a7..4970d004 100644 --- a/app/src/main/res/layout/who_is_watching_account.xml +++ b/app/src/main/res/layout/who_is_watching_account.xml @@ -11,6 +11,7 @@ android:foreground="?attr/selectableItemBackgroundBorderless" app:cardCornerRadius="@dimen/rounded_image_radius" android:layout_margin="5dp" + android:focusable="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/who_is_watching_account_add.xml b/app/src/main/res/layout/who_is_watching_account_add.xml index ed67e144..91c7e419 100644 --- a/app/src/main/res/layout/who_is_watching_account_add.xml +++ b/app/src/main/res/layout/who_is_watching_account_add.xml @@ -11,6 +11,7 @@ android:foreground="?attr/selectableItemBackgroundBorderless" app:cardCornerRadius="@dimen/rounded_image_radius" android:layout_margin="5dp" + android:focusable="true" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/who_is_watching_account_edit.xml b/app/src/main/res/layout/who_is_watching_account_edit.xml index 74553517..cec37a4f 100644 --- a/app/src/main/res/layout/who_is_watching_account_edit.xml +++ b/app/src/main/res/layout/who_is_watching_account_edit.xml @@ -88,6 +88,7 @@ android:layout_width="match_parent" android:layout_height="60dp" android:layout_gravity="center" + android:focusable="true" android:contentDescription="@string/preview_background_img_des" android:scaleType="centerCrop" android:src="@drawable/profile_bg_blue" /> From bd05a67f260263f1e4d49580cd40fc587b354fd0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Mon, 2 Oct 2023 17:44:06 +0200 Subject: [PATCH 19/42] preview seekbar --- app/build.gradle.kts | 2 + .../ui/player/AbstractPlayerFragment.kt | 42 ++++- .../cloudstream3/ui/player/CS3IPlayer.kt | 53 ++++++- .../cloudstream3/ui/player/GeneratorPlayer.kt | 1 + .../cloudstream3/ui/player/IPlayer.kt | 7 +- .../ui/player/PreviewGenerator.kt | 147 ++++++++++++++++++ .../ui/result/ResultFragmentPhone.kt | 3 +- app/src/main/res/drawable/video_frame.xml | 10 ++ .../main/res/layout/player_custom_layout.xml | 63 ++++++-- .../res/layout/player_custom_layout_tv.xml | 62 ++++++-- .../main/res/layout/trailer_custom_layout.xml | 78 +++++++--- app/src/main/res/values/dimens.xml | 2 +- 12 files changed, 413 insertions(+), 57 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt create mode 100644 app/src/main/res/drawable/video_frame.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f13095fb..9f484c48 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -258,6 +258,8 @@ dependencies { // color palette for images -> colors implementation("androidx.palette:palette-ktx:1.0.0") + // seekbar https://github.com/rubensousa/PreviewSeekBar + implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0") } tasks.register("androidSourcesJar", Jar::class) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 8388e58f..862504a1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -18,10 +18,9 @@ import android.widget.ProgressBar import android.widget.Toast import androidx.annotation.LayoutRes import androidx.annotation.StringRes +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.preference.PreferenceManager -import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import androidx.media3.common.PlaybackException import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaSession @@ -30,6 +29,10 @@ import androidx.media3.ui.DefaultTimeBar import androidx.media3.ui.PlayerView import androidx.media3.ui.SubtitleView import androidx.media3.ui.TimeBar +import androidx.preference.PreferenceManager +import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat +import com.github.rubensousa.previewseekbar.PreviewBar +import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode @@ -454,6 +457,41 @@ abstract class AbstractPlayerFragment( ) if (player is CS3IPlayer) { + // preview bar + val progressBar : PreviewTimeBar? = playerView?.findViewById(R.id.exo_progress) + val previewImageView : ImageView? = playerView?.findViewById(R.id.previewImageView) + val previewFrameLayout : FrameLayout? = playerView?.findViewById(R.id.previewFrameLayout) + if(progressBar != null && previewImageView != null && previewFrameLayout != null) { + var resume = false + progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener { + override fun onScrubStart(previewBar: PreviewBar?) { + progressBar.isPreviewEnabled = player.hasPreview() + resume = player.getIsPlaying() + if (resume) player.handleEvent( + CSPlayerEvent.Pause, + PlayerEventSource.Player + ) + } + + override fun onScrubMove( + previewBar: PreviewBar?, + progress: Int, + fromUser: Boolean + ) { + } + + override fun onScrubStop(previewBar: PreviewBar?) { + if (resume) player.handleEvent(CSPlayerEvent.Play, PlayerEventSource.Player) + } + }) + progressBar.attachPreviewView(previewFrameLayout) + progressBar.setPreviewLoader { currentPosition, max -> + val bitmap = player.getPreview(currentPosition.toFloat().div(max.toFloat())) + previewImageView.isGone = bitmap == null + previewImageView.setImageBitmap(bitmap) + } + } + subView = playerView?.findViewById(R.id.exo_subtitles) subStyle = SubtitlesFragment.getCurrentSavedStyle() player.initSubtitles(subView, subtitleHolder, subStyle) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 331cfb73..946743a3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.ui.player import android.annotation.SuppressLint import android.content.Context +import android.graphics.Bitmap import android.net.Uri import android.os.Handler import android.os.Looper @@ -88,7 +89,9 @@ class CS3IPlayer : IPlayer { private var exoPlayer: ExoPlayer? = null set(value) { // If the old value is not null then the player has not been properly released. - debugAssert({ field != null && value != null }, { "Previous player instance should be released!" }) + debugAssert( + { field != null && value != null }, + { "Previous player instance should be released!" }) field = value } @@ -96,6 +99,8 @@ class CS3IPlayer : IPlayer { var simpleCacheSize = 0L var videoBufferMs = 0L + private val imageGenerator = PreviewGenerator() + private val seekActionTime = 30000L private var ignoreSSL: Boolean = true @@ -182,6 +187,14 @@ class CS3IPlayer : IPlayer { subtitleHelper.initSubtitles(subView, subHolder, style) } + override fun getPreview(fraction: Float): Bitmap? { + return imageGenerator.getPreviewImage(fraction) + } + + override fun hasPreview(): Boolean { + return imageGenerator.hasPreview() + } + override fun loadPlayer( context: Context, sameEpisode: Boolean, @@ -190,7 +203,8 @@ class CS3IPlayer : IPlayer { startPosition: Long?, subtitles: Set, subtitle: SubtitleData?, - autoPlay: Boolean? + autoPlay: Boolean?, + preview : Boolean, ) { Log.i(TAG, "loadPlayer") if (sameEpisode) { @@ -210,9 +224,27 @@ class CS3IPlayer : IPlayer { // release the current exoplayer and cache releasePlayer() if (link != null) { + // only video support atm + if (link.type == ExtractorLinkType.VIDEO && preview) { + val headers = if (link.referer.isBlank()) { + link.headers + } else { + mapOf("referer" to link.referer) + link.headers + } + imageGenerator.load(sameEpisode, link.url, headers) + } else { + imageGenerator.clear(sameEpisode) + } loadOnlinePlayer(context, link) } else if (data != null) { + if (preview) { + imageGenerator.load(sameEpisode, context, data.uri) + } else { + imageGenerator.clear(sameEpisode) + } loadOfflinePlayer(context, data) + } else { + throw IllegalArgumentException("Requires link or uri") } } @@ -494,6 +526,7 @@ class CS3IPlayer : IPlayer { } override fun release() { + imageGenerator.release() releasePlayer() } @@ -871,8 +904,20 @@ class CS3IPlayer : IPlayer { CSPlayerEvent.SeekForward -> seekTime(seekActionTime, source) CSPlayerEvent.SeekBack -> seekTime(-seekActionTime, source) - CSPlayerEvent.NextEpisode -> event(EpisodeSeekEvent(offset = 1, source = source)) - CSPlayerEvent.PrevEpisode -> event(EpisodeSeekEvent(offset = -1, source = source)) + CSPlayerEvent.NextEpisode -> event( + EpisodeSeekEvent( + offset = 1, + source = source + ) + ) + + CSPlayerEvent.PrevEpisode -> event( + EpisodeSeekEvent( + offset = -1, + source = source + ) + ) + CSPlayerEvent.SkipCurrentChapter -> { //val dur = this@CS3IPlayer.getDuration() ?: return@apply getCurrentTimestamp()?.let { lastTimeStamp -> diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index b2542ffa..1c751897 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -180,6 +180,7 @@ class GeneratorPlayer : FullScreenPlayer() { (if (sameEpisode) currentSelectedSubtitles else null) ?: getAutoSelectSubtitle( currentSubs, settings = true, downloads = true ), + preview = isFullScreenPlayer ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index ec006234..a08360ae 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -1,6 +1,7 @@ package com.lagradost.cloudstream3.ui.player import android.content.Context +import android.graphics.Bitmap import android.util.Rational import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.EpisodeSkip @@ -246,11 +247,15 @@ interface IPlayer { startPosition: Long? = null, subtitles: Set, subtitle: SubtitleData?, - autoPlay: Boolean? = true + autoPlay: Boolean? = true, + preview : Boolean = true, ) fun reloadPlayer(context: Context) + fun getPreview(fraction : Float) : Bitmap? + fun hasPreview() : Boolean + fun setActiveSubtitles(subtitles: Set) fun setPreferredSubtitles(subtitle: SubtitleData?): Boolean // returns true if the player requires a reload, null for nothing fun getCurrentPreferredSubtitle(): SubtitleData? diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt new file mode 100644 index 00000000..0f47d009 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -0,0 +1,147 @@ +package com.lagradost.cloudstream3.ui.player + +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.util.Log +import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive +import kotlin.math.absoluteValue +import kotlin.math.ceil +import kotlin.math.log2 + +const val MAX_LOD = 6 +const val MIN_LOD = 3 + +class PreviewGenerator { + // lod = level of detail where the number indicates how many ones there is + // 2^(lod-1) = images + private var loadedLod = 0 + private var loadedImages = 0 + private var images = Array((1 shl MAX_LOD) - 1) { + null + } + + fun hasPreview(): Boolean { + synchronized(images) { + return loadedLod >= MIN_LOD + } + } + + val TAG = "PreviewImg" + + fun getPreviewImage(fraction: Float): Bitmap? { + synchronized(images) { + if (loadedLod < MIN_LOD) { + Log.i(TAG, "Requesting preview for $fraction but $loadedLod < $MIN_LOD") + return null + } + Log.i(TAG, "Requesting preview for $fraction") + + var bestIdx = 0 + var bestDiff = 0.5f.minus(fraction).absoluteValue + + // this should be done mathematically, but for now we just loop all images + for (l in 1..loadedLod + 1) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val idx = items - 1 + i + if (idx > loadedImages) { + break + } + val currentFraction = + (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) + val diff = currentFraction.minus(fraction).absoluteValue + if (diff < bestDiff) { + bestDiff = diff + bestIdx = idx + } + } + } + Log.i(TAG, "Best diff found at ${bestDiff * 100}% diff (${bestIdx})") + return images[bestIdx] + } + } + + // also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever + private val retriever: MediaMetadataRetriever = MediaMetadataRetriever() + + fun clear(keepCache: Boolean = false) { + if (keepCache) return + synchronized(images) { + loadedLod = 0 + loadedImages = 0 + images.fill(null) + } + } + + private var currentJob: Job? = null + fun load(keepCache: Boolean, url: String, headers: Map) { + currentJob?.cancel() + currentJob = ioSafe { + Log.i(TAG, "Loading with url = $url headers = $headers") + clear(keepCache) + retriever.setDataSource(url, headers) + start(this) + } + } + + fun load(keepCache: Boolean, context: Context, uri: Uri) { + currentJob?.cancel() + currentJob = ioSafe { + Log.i(TAG, "Loading with uri = $uri") + clear(keepCache) + retriever.setDataSource(context, uri) + start(this) + } + } + + fun release() { + currentJob?.cancel() + clear(false) + } + + @Throws + @WorkerThread + private fun start(scope: CoroutineScope) { + Log.i(TAG, "Started loading preview") + + val durationMs = + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() + ?: throw IllegalArgumentException("Bad video duration") + val durationUs = (durationMs * 1000L).toFloat() + //val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: throw IllegalArgumentException("Bad video width") + //val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: throw IllegalArgumentException("Bad video height") + + // log2 # 10s durations in the video ~= how many segments we have + val maxLod = ceil(log2((durationMs / 10_000).toFloat())).toInt().coerceIn(MIN_LOD, MAX_LOD) + + for (l in 1..maxLod) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val idx = items - 1 + i // as sum(prev) = cur-1 + // frame = 100 / 2^lod + i * 100 / 2^(lod-1) = duration % where lod is one indexed + val fraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) + Log.i(TAG, "Generating preview for ${fraction * 100}%") + val frame = durationUs * fraction + val img = retriever.getFrameAtTime( + frame.toLong(), + MediaMetadataRetriever.OPTION_CLOSEST_SYNC + ) + if (!scope.isActive) return + synchronized(images) { + images[idx] = img + loadedImages = maxOf(loadedImages,idx) + } + } + + synchronized(images) { + loadedLod = maxOf(loadedLod, l) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt index ef2ed0df..e5f16dd5 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragmentPhone.kt @@ -151,7 +151,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { startPosition = 0L, subtitles = emptySet(), subtitle = null, - autoPlay = false + autoPlay = false, + preview = false ) true } ?: run { diff --git a/app/src/main/res/drawable/video_frame.xml b/app/src/main/res/drawable/video_frame.xml new file mode 100644 index 00000000..19fcf26d --- /dev/null +++ b/app/src/main/res/drawable/video_frame.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 5592f3a6..0f76e4dd 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -86,7 +86,6 @@ + + android:importantForAccessibility="no" + android:visibility="gone" /> + + + - - - + + + + + - - - + - - + tools:ignore="ContentDescription" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> - - + + + + + - + - - + - - - + + + + + - - + + - 62dp 50dp - + 1dp \ No newline at end of file From 1d90858f64bd113df71dc68fa8e03f9be640c542 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Mon, 2 Oct 2023 13:04:40 -0600 Subject: [PATCH 20/42] Make search history account specific (#638) * Make search history account specific * Update for clear history --- .../com/lagradost/cloudstream3/ui/search/SearchFragment.kt | 7 ++++--- .../lagradost/cloudstream3/ui/search/SearchViewModel.kt | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index bdf82377..845c36ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -55,6 +55,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.setKey +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -398,7 +399,7 @@ class SearchFragment : Fragment() { DialogInterface.OnClickListener { _, which -> when (which) { DialogInterface.BUTTON_POSITIVE -> { - removeKeys(SEARCH_HISTORY_KEY) + removeKeys("$currentAccount/$SEARCH_HISTORY_KEY") searchViewModel.updateHistory() } DialogInterface.BUTTON_NEGATIVE -> { @@ -510,7 +511,7 @@ class SearchFragment : Fragment() { binding?.mainSearch?.setQuery(searchItem.searchText, true) } SEARCH_HISTORY_REMOVE -> { - removeKey(SEARCH_HISTORY_KEY, searchItem.key) + removeKey("$currentAccount/$SEARCH_HISTORY_KEY", searchItem.key) searchViewModel.updateHistory() } else -> { @@ -559,4 +560,4 @@ class SearchFragment : Fragment() { .commit()*/ } -} \ No newline at end of file +} 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 320687f8..839b9d3f 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 @@ -14,6 +14,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe import com.lagradost.cloudstream3.ui.APIRepository import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -64,7 +65,7 @@ class SearchViewModel : ViewModel() { fun updateHistory() = viewModelScope.launch { ioSafe { - val items = getKeys(SEARCH_HISTORY_KEY)?.mapNotNull { + val items = getKeys("$currentAccount/$SEARCH_HISTORY_KEY")?.mapNotNull { getKey(it) }?.sortedByDescending { it.searchedAt } ?: emptyList() _currentHistory.postValue(items) @@ -87,7 +88,7 @@ class SearchViewModel : ViewModel() { if (!isQuickSearch) { val key = query.hashCode().toString() setKey( - SEARCH_HISTORY_KEY, + "$currentAccount/$SEARCH_HISTORY_KEY", key, SearchHistoryItem( searchedAt = System.currentTimeMillis(), @@ -140,4 +141,4 @@ class SearchViewModel : ViewModel() { _searchResponse.postValue(Resource.Success(list)) } } -} \ No newline at end of file +} From 08060314ad94054ed8c42bd38824122bfb24565e Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:50:34 +0200 Subject: [PATCH 21/42] preview seekbar m3u8 --- .../cloudstream3/ui/player/CS3IPlayer.kt | 11 +- .../ui/player/PreviewGenerator.kt | 263 +++++++++++++++++- .../cloudstream3/utils/ExtractorApi.kt | 9 + .../cloudstream3/utils/M3u8Helper.kt | 42 ++- 4 files changed, 298 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 946743a3..6256bef6 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -225,20 +225,15 @@ class CS3IPlayer : IPlayer { releasePlayer() if (link != null) { // only video support atm - if (link.type == ExtractorLinkType.VIDEO && preview) { - val headers = if (link.referer.isBlank()) { - link.headers - } else { - mapOf("referer" to link.referer) + link.headers - } - imageGenerator.load(sameEpisode, link.url, headers) + if (preview) { + imageGenerator.load(link, sameEpisode) } else { imageGenerator.clear(sameEpisode) } loadOnlinePlayer(context, link) } else if (data != null) { if (preview) { - imageGenerator.load(sameEpisode, context, data.uri) + imageGenerator.load(context, data, sameEpisode) } else { imageGenerator.clear(sameEpisode) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 0f47d009..53699782 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -6,10 +6,18 @@ import android.media.MediaMetadataRetriever import android.net.Uri import android.util.Log import androidx.annotation.WorkerThread +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.ExtractorLink +import com.lagradost.cloudstream3.utils.ExtractorLinkType +import com.lagradost.cloudstream3.utils.ExtractorUri +import com.lagradost.cloudstream3.utils.M3u8Helper +import com.lagradost.cloudstream3.utils.M3u8Helper2 import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.isActive +import kotlinx.coroutines.withContext import kotlin.math.absoluteValue import kotlin.math.ceil import kotlin.math.log2 @@ -17,7 +25,248 @@ import kotlin.math.log2 const val MAX_LOD = 6 const val MIN_LOD = 3 -class PreviewGenerator { +interface IPreviewGenerator { + fun hasPreview(): Boolean + fun getPreviewImage(fraction: Float): Bitmap? + fun clear(keepCache: Boolean = false) + fun release() +} + +class PreviewGenerator : IPreviewGenerator { + private var currentGenerator: IPreviewGenerator = NoPreviewGenerator() + override fun hasPreview(): Boolean { + return currentGenerator.hasPreview() + } + + override fun getPreviewImage(fraction: Float): Bitmap? { + return try { + currentGenerator.getPreviewImage(fraction) + } catch (t: Throwable) { + logError(t) + null + } + } + + override fun clear(keepCache: Boolean) { + currentGenerator.clear(keepCache) + } + + override fun release() { + currentGenerator.release() + } + + fun load(link: ExtractorLink, keepCache: Boolean) { + val gen = currentGenerator + when (link.type) { + ExtractorLinkType.M3U8 -> { + if (gen is M3u8PreviewGenerator) { + gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } else { + currentGenerator.release() + currentGenerator = M3u8PreviewGenerator().apply { + load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } + } + } + + ExtractorLinkType.VIDEO -> { + if (gen is Mp4PreviewGenerator) { + gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } else { + currentGenerator.release() + currentGenerator = Mp4PreviewGenerator().apply { + load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) + } + } + } + + else -> { + currentGenerator.clear(keepCache) + } + } + } + + fun load(context: Context, link: ExtractorUri, keepCache: Boolean) { + val gen = currentGenerator + if (gen is Mp4PreviewGenerator) { + gen.load(keepCache = keepCache, context = context, uri = link.uri) + } else { + currentGenerator.release() + currentGenerator = Mp4PreviewGenerator().apply { + load(keepCache = keepCache, context = context, uri = link.uri) + } + } + } +} + +class NoPreviewGenerator : IPreviewGenerator { + override fun hasPreview(): Boolean = false + override fun getPreviewImage(fraction: Float): Bitmap? = null + override fun clear(keepCache: Boolean) = Unit + override fun release() = Unit +} + +class M3u8PreviewGenerator : IPreviewGenerator { + // generated images 1:1 to idx of hsl + private var images: Array = arrayOf() + + private val TAG = "PreviewImgM3u8" + + // prefixSum[i] = sum(hsl.ts[0..i].time) + // where [0] = 0, [1] = hsl.ts[0].time aka time at start of segment, do [b] - [a] for range a,b + private var prefixSum: Array = arrayOf() + + // how many images has been generated + private var loadedImages: Int = 0 + + // how many images we can generate in total, == hsl.size ?: 0 + private var totalImages: Int = 0 + + override fun hasPreview(): Boolean { + return totalImages > 0 && loadedImages >= minOf(totalImages, 4) + } + + override fun getPreviewImage(fraction: Float): Bitmap? { + var bestIdx = -1 + var bestDiff = Double.MAX_VALUE + synchronized(images) { + // just find the best one in a for loop, we don't care about bin searching rn + for (i in 0..images.size) { + val diff = prefixSum[i].minus(fraction).absoluteValue + if (diff > bestDiff) { + break + } + if (images[i] != null) { + bestIdx = i + bestDiff = diff + } + } + return images.getOrNull(bestIdx) + } + /* + val targetIndex = prefixSum.binarySearch(target) + var ret = images[targetIndex] + if (ret != null) { + return ret + } + for (i in 0..images.size) { + ret = images.getOrNull(i+targetIndex) ?: + }*/ + } + + override fun clear(keepCache: Boolean) { + synchronized(images) { + currentJob?.cancel() + images = arrayOf() + prefixSum = arrayOf() + loadedImages = 0 + totalImages = 0 + } + } + + override fun release() { + clear() + images = arrayOf() + } + + private var currentJob: Job? = null + fun load(keepCache: Boolean, url: String, headers: Map) { + clear(keepCache) + currentJob?.cancel() + currentJob = ioSafe { + withContext(Dispatchers.IO) { + Log.i(TAG, "Loading with url = $url headers = $headers") + //tmpFile = + // File.createTempFile("video", ".ts", context.cacheDir).apply { + // deleteOnExit() + // } + val retriever = MediaMetadataRetriever() + val hsl = M3u8Helper2.hslLazy( + listOf( + M3u8Helper.M3u8Stream( + streamUrl = url, + headers = headers + ) + ), + selectBest = false + ) + + // no support for encryption atm + if (hsl.isEncrypted) { + totalImages = 0 + return@withContext + } + + // total duration of the entire m3u8 in seconds + val duration = hsl.allTsLinks.sumOf { it.time ?: 0.0 } + val durationInv = 1.0 / duration + + // if the total duration is less then 10s then something is very wrong or + // too short playback to matter + if (duration <= 10.0) { + totalImages = 0 + return@withContext + } + + totalImages = hsl.allTsLinks.size + + // we cant init directly as it is no guarantee of in order + prefixSum = Array(hsl.allTsLinks.size + 1) { 0.0 } + var runningSum = 0.0 + for (i in hsl.allTsLinks.indices) { + runningSum += (hsl.allTsLinks[i].time ?: 0.0) + prefixSum[i + 1] = runningSum * durationInv + } + synchronized(images) { + images = Array(hsl.size) { null } + loadedImages = 0 + } + + val maxLod = ceil(log2(duration)).toInt().coerceIn(MIN_LOD, MAX_LOD) + val count = hsl.allTsLinks.size + for (l in 1..maxLod) { + val items = (1 shl (l - 1)) + for (i in 0 until items) { + val index = (count.div(1 shl l) + (i * count) / items).coerceIn(0, hsl.size) + if (synchronized(images) { images[index] } != null) { + continue + } + Log.i(TAG, "Generating preview for $index") + + val ts = hsl.allTsLinks[index] + try { + retriever.setDataSource(ts.url, hsl.headers) + if (!isActive) { + return@withContext + } + val frame = retriever.getFrameAtTime(0) + if (!isActive) { + return@withContext + } + synchronized(images) { + images[index] = frame + loadedImages += 1 + } + } catch (t: Throwable) { + logError(t) + continue + } + + /* + val buffer = hsl.resolveLinkSafe(index) ?: continue + tmpFile?.writeBytes(buffer) + val buff = FileOutputStream(tmpFile) + retriever.setDataSource(buff.fd) + val frame = retriever.getFrameAtTime(0L)*/ + } + } + + } + } + } +} + +class Mp4PreviewGenerator : IPreviewGenerator { // lod = level of detail where the number indicates how many ones there is // 2^(lod-1) = images private var loadedLod = 0 @@ -26,15 +275,15 @@ class PreviewGenerator { null } - fun hasPreview(): Boolean { + override fun hasPreview(): Boolean { synchronized(images) { return loadedLod >= MIN_LOD } } - val TAG = "PreviewImg" + val TAG = "PreviewImgMp4" - fun getPreviewImage(fraction: Float): Bitmap? { + override fun getPreviewImage(fraction: Float): Bitmap? { synchronized(images) { if (loadedLod < MIN_LOD) { Log.i(TAG, "Requesting preview for $fraction but $loadedLod < $MIN_LOD") @@ -70,7 +319,7 @@ class PreviewGenerator { // also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever private val retriever: MediaMetadataRetriever = MediaMetadataRetriever() - fun clear(keepCache: Boolean = false) { + override fun clear(keepCache: Boolean) { if (keepCache) return synchronized(images) { loadedLod = 0 @@ -100,7 +349,7 @@ class PreviewGenerator { } } - fun release() { + override fun release() { currentJob?.cancel() clear(false) } @@ -135,7 +384,7 @@ class PreviewGenerator { if (!scope.isActive) return synchronized(images) { images[idx] = img - loadedImages = maxOf(loadedImages,idx) + loadedImages = maxOf(loadedImages, idx) } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt index 9db62dc8..d89e67fa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/ExtractorApi.kt @@ -378,6 +378,15 @@ open class ExtractorLink constructor( val isM3u8 : Boolean get() = type == ExtractorLinkType.M3U8 val isDash : Boolean get() = type == ExtractorLinkType.DASH + fun getAllHeaders() : Map { + if (referer.isBlank()) { + return headers + } else if (headers.keys.none { it.equals("referer", ignoreCase = true) }) { + return headers + mapOf("referer" to referer) + } + return headers + } + constructor( source: String, name: String, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 11dfa441..d3fe7162 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -71,7 +71,7 @@ object M3u8Helper2 { private val QUALITY_REGEX = Regex("""#EXT-X-STREAM-INF:(?:(?:.*?(?:RESOLUTION=\d+x(\d+)).*?\s+(.*))|(?:.*?\s+(.*)))""") private val TS_EXTENSION_REGEX = - Regex("""#EXTINF:.*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways + Regex("""#EXTINF:(([0-9]*[.])?[0-9]+|).*\n(.+?\n)""") // fuck it we ball, who cares about the type anyways //Regex("""(.*\.(ts|jpg|html).*)""") //.jpg here 'case vizcloud uses .jpg instead of .ts private fun absoluteExtensionDetermination(url: String): String? { @@ -122,6 +122,15 @@ object M3u8Helper2 { return result.lastOrNull() } + private fun selectWorst(qualities: List): M3u8Helper.M3u8Stream? { + val result = qualities.sortedBy { + if (it.quality != null && it.quality <= 1080) it.quality else 0 + }.filter { + listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) + } + return result.firstOrNull() + } + private fun getParentLink(uri: String): String { val split = uri.split("/").toMutableList() split.removeLast() @@ -173,14 +182,20 @@ object M3u8Helper2 { return list } + data class TsLink( + val url : String, + val time : Double?, + ) + data class LazyHlsDownloadData( private val encryptionData: ByteArray, private val encryptionIv: ByteArray, - private val isEncrypted: Boolean, - private val allTsLinks: List, - private val relativeUrl: String, - private val headers: Map, + val isEncrypted: Boolean, + val allTsLinks: List, + val relativeUrl: String, + val headers: Map, ) { + val size get() = allTsLinks.size suspend fun resolveLinkWhileSafe( @@ -228,9 +243,9 @@ object M3u8Helper2 { @Throws suspend fun resolveLink(index: Int): ByteArray { if (index < 0 || index >= size) throw IllegalArgumentException("index must be in the bounds of the ts") - val url = allTsLinks[index] + val ts = allTsLinks[index] - val tsResponse = app.get(url, headers = headers, verify = false) + val tsResponse = app.get(ts.url, headers = headers, verify = false) val tsData = tsResponse.body.bytes() if (tsData.isEmpty()) throw ErrorLoadingException("no data") @@ -244,15 +259,16 @@ object M3u8Helper2 { @Throws suspend fun hslLazy( - qualities: List + qualities: List, selectBest : Boolean = true ): LazyHlsDownloadData { if (qualities.isEmpty()) throw IllegalArgumentException("qualities must be non empty") - val selected = selectBest(qualities) ?: qualities.first() + val selected = if(selectBest) { selectBest(qualities) } else { selectWorst(qualities) } ?: qualities.first() val headers = selected.headers val streams = qualities.map { m3u8Generation(it, false) }.flatten() // this selects the best quality of the qualities offered, // due to the recursive nature of m3u8, we only go 2 depth - val secondSelection = selectBest(streams.ifEmpty { listOf(selected) }) + val innerStreams = streams.ifEmpty { listOf(selected) } + val secondSelection = if(selectBest) { selectBest(innerStreams) } else { selectWorst(innerStreams) } ?: throw IllegalArgumentException("qualities has no streams") val m3u8Response = @@ -285,12 +301,14 @@ object M3u8Helper2 { } val relativeUrl = getParentLink(secondSelection.streamUrl) val allTsList = TS_EXTENSION_REGEX.findAll(m3u8Response + "\n").map { ts -> - val value = ts.groupValues[1] - if (isNotCompleteUrl(value)) { + val time = ts.groupValues[1] + val value = ts.groupValues[3] + val url = if (isNotCompleteUrl(value)) { "$relativeUrl/${value}" } else { value } + TsLink(url = url, time = time.toDoubleOrNull()) }.toList() if (allTsList.isEmpty()) throw IllegalArgumentException("ts must be non empty") From 462073bd747c62ab995ce200b5a55ddce18811ad Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 08:56:38 -0600 Subject: [PATCH 22/42] Make search prefs account specific (#640) --- .../cloudstream3/ui/search/SearchFragment.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 845c36ef..bad78624 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -194,7 +194,7 @@ class SearchFragment : Fragment() { validAPIs.flatMap { api -> api.supportedTypes }.distinct() ) { list -> if (selectedSearchTypes.toSet() != list.toSet()) { - setKey(SEARCH_PREF_TAGS, selectedSearchTypes) + setKey("$currentAccount/$SEARCH_PREF_TAGS", selectedSearchTypes) selectedSearchTypes.clear() selectedSearchTypes.addAll(list) search(binding?.mainSearch?.query?.toString()) @@ -236,7 +236,7 @@ class SearchFragment : Fragment() { context?.let { ctx -> val validAPIs = ctx.filterProviderByPreferredMedia() selectedApis = ctx.getKey( - SEARCH_PREF_PROVIDERS, + "$currentAccount/$SEARCH_PREF_PROVIDERS", defVal = validAPIs.map { it.name } )!!.toMutableSet() } @@ -287,7 +287,7 @@ class SearchFragment : Fragment() { } fun updateList(types: List) { - setKey(SEARCH_PREF_TAGS, types.map { it.name }) + setKey("$currentAccount/$SEARCH_PREF_TAGS", types.map { it.name }) arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> @@ -312,7 +312,7 @@ class SearchFragment : Fragment() { arrayAdapter.notifyDataSetChanged() } - val selectedSearchTypes = getKey>(SEARCH_PREF_TAGS) + val selectedSearchTypes = getKey>("$currentAccount/$SEARCH_PREF_TAGS") ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } @@ -343,7 +343,7 @@ class SearchFragment : Fragment() { } dialog.setOnDismissListener { - context?.setKey(SEARCH_PREF_PROVIDERS, currentSelectedApis.toList()) + context?.setKey("$currentAccount/$SEARCH_PREF_PROVIDERS", currentSelectedApis.toList()) selectedApis = currentSelectedApis } updateList(selectedSearchTypes.toList()) @@ -354,7 +354,7 @@ class SearchFragment : Fragment() { val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) } val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true - selectedSearchTypes = context?.getKey>(SEARCH_PREF_TAGS) + selectedSearchTypes = context?.getKey>("$currentAccount/$SEARCH_PREF_TAGS") ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } ?.toMutableList() ?: mutableListOf(TvType.Movie, TvType.TvSeries) From cc00e73e16459365bb7d412ee07422a354767ad0 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:25:31 -0600 Subject: [PATCH 23/42] Make homepage preferences account specific (#647) * Make homepage preferences account specific * Fix accidentally removed whitespace * Fix in setkey --- .../java/com/lagradost/cloudstream3/ui/home/HomeFragment.kt | 4 ++-- .../com/lagradost/cloudstream3/ui/home/HomeViewModel.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) 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 0797e9a0..ebbb245c 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 @@ -377,7 +377,7 @@ class HomeFragment : Fragment() { var currentApiName = selectedApiName var currentValidApis: MutableList = mutableListOf() - val preSelectedTypes = this.getKey>(HOME_PREF_HOMEPAGE) + val preSelectedTypes = this.getKey>("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE") ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } ?.toMutableList() ?: mutableListOf(TvType.Movie, TvType.TvSeries) @@ -408,7 +408,7 @@ class HomeFragment : Fragment() { } fun updateList() { - this.setKey(HOME_PREF_HOMEPAGE, preSelectedTypes) + this.setKey("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE", preSelectedTypes) arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> 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 13d34b59..a5ef2bb4 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 @@ -171,7 +171,7 @@ class HomeViewModel : ViewModel() { if (currentWatchTypes.size <= 0) { setKey( - HOME_BOOKMARK_VALUE_LIST, + "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", intArrayOf() ) _availableWatchStatusTypes.postValue(setOf() to setOf()) @@ -182,7 +182,7 @@ class HomeViewModel : ViewModel() { val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first()) //if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first()) setKey( - HOME_BOOKMARK_VALUE_LIST, + "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", watchPrefNotNull.map { it.internalId }.toIntArray() ) _availableWatchStatusTypes.postValue( @@ -463,7 +463,7 @@ class HomeViewModel : ViewModel() { fun loadStoredData() { val list = EnumSet.noneOf(WatchType::class.java) - getKey(HOME_BOOKMARK_VALUE_LIST)?.map { WatchType.fromInternalId(it) }?.let { + getKey("${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST")?.map { WatchType.fromInternalId(it) }?.let { list.addAll(it) } loadStoredData(list) From b5d4c3bd27cc70edaa461dd0a29c4c5828be8fcf Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 11:26:56 -0600 Subject: [PATCH 24/42] Make player preferences account specific (#646) --- .../cloudstream3/ui/player/AbstractPlayerFragment.kt | 5 +++-- .../java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt | 5 +++-- .../com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 862504a1..52974ff7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -48,6 +48,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI @@ -442,7 +443,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("SetTextI18n", "UnsafeOptInUsageError") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - resizeMode = getKey(RESIZE_MODE_KEY) ?: 0 + resizeMode = getKey("$currentAccount/$RESIZE_MODE_KEY") ?: 0 resize(resizeMode, false) player.releaseCallbacks() @@ -573,7 +574,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("UnsafeOptInUsageError") fun resize(resize: PlayerResize, showToast: Boolean) { - setKey(RESIZE_MODE_KEY, resize.ordinal) + setKey("$currentAccount/$RESIZE_MODE_KEY", resize.ordinal) val type = when (resize) { PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FILL PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt index 6256bef6..49904f6a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/CS3IPlayer.kt @@ -57,6 +57,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.DrmExtractorLink import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.ExtractorLink @@ -536,12 +537,12 @@ class CS3IPlayer : IPlayer { **/ var preferredAudioTrackLanguage: String? = null get() { - return field ?: getKey(PREFERRED_AUDIO_LANGUAGE_KEY, field)?.also { + return field ?: getKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", field)?.also { field = it } } set(value) { - setKey(PREFERRED_AUDIO_LANGUAGE_KEY, value) + setKey("$currentAccount/$PREFERRED_AUDIO_LANGUAGE_KEY", value) field = value } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index e698191d..43e8aa0b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -49,6 +49,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -356,7 +357,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setPlayBackSpeed(speed: Float) { try { - setKey(PLAYBACK_SPEED_KEY, speed) + setKey("$currentAccount/$PLAYBACK_SPEED_KEY", speed) playerBinding?.playerSpeedBtt?.text = getString(R.string.player_speed_text_format).format(speed) .replace(".0x", "x") @@ -1194,7 +1195,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // init variables - setPlayBackSpeed(getKey(PLAYBACK_SPEED_KEY) ?: 1.0f) + setPlayBackSpeed(getKey("$currentAccount/$PLAYBACK_SPEED_KEY") ?: 1.0f) savedInstanceState?.getLong(SUBTITLE_DELAY_BUNDLE_KEY)?.let { subtitleDelay = it } From 3f5119525c3a17a13a3dce42bef6a9caffd5c86e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:59:26 -0600 Subject: [PATCH 25/42] Make library preferences account specific (#649) --- .../lagradost/cloudstream3/ui/library/LibraryFragment.kt | 9 +++++---- .../cloudstream3/ui/library/LibraryViewModel.kt | 5 +++-- 2 files changed, 8 insertions(+), 6 deletions(-) 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 04ef3d96..d5fdc1aa 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 @@ -35,6 +35,7 @@ import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount @@ -180,7 +181,7 @@ class LibraryFragment : Fragment() { val items = baseOptions.map { txt(it.stringRes).asString(this) } + availableProviders - val savedSelection = getKey(LIBRARY_FOLDER, key) + val savedSelection = getKey("$currentAccount/$LIBRARY_FOLDER", key) val selectedIndex = when { savedSelection == null -> 0 @@ -215,7 +216,7 @@ class LibraryFragment : Fragment() { } setKey( - LIBRARY_FOLDER, + "$currentAccount/$LIBRARY_FOLDER", key, savedData, ) @@ -262,8 +263,8 @@ class LibraryFragment : Fragment() { // This basically first selects the individual opener and if that is default then // selects the whole list opener val savedListSelection = - getKey(LIBRARY_FOLDER, syncName.name) - val savedSelection = getKey(LIBRARY_FOLDER, syncId).takeIf { + getKey("$currentAccount/$LIBRARY_FOLDER", syncName.name) + val savedSelection = getKey("$currentAccount/$LIBRARY_FOLDER", syncId).takeIf { it?.openType != LibraryOpenerType.Default } ?: savedListSelection diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index 14d31356..25a5a0f8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -11,6 +11,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount enum class ListSorting(@StringRes val stringRes: Int) { Query(R.string.none), @@ -35,12 +36,12 @@ class LibraryViewModel : ViewModel() { get() = SyncApis.filter { it.hasAccount() } var currentSyncApi = availableSyncApis.let { allApis -> - val lastSelection = getKey(LAST_SYNC_API_KEY) + val lastSelection = getKey("$currentAccount/$LAST_SYNC_API_KEY") availableSyncApis.firstOrNull { it.name == lastSelection } ?: allApis.firstOrNull() } private set(value) { field = value - setKey(LAST_SYNC_API_KEY, field?.name) + setKey("$currentAccount/$LAST_SYNC_API_KEY", field?.name) } val availableApiNames: List From 177b1e47f3f08110c07e3839a2d1935fe491a4da Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Wed, 4 Oct 2023 11:33:55 +0200 Subject: [PATCH 26/42] added extra logging --- .../cloudstream3/ui/player/PreviewGenerator.kt | 11 +++++++++-- .../com/lagradost/cloudstream3/utils/M3u8Helper.kt | 12 ++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 53699782..946c1d33 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -81,6 +81,7 @@ class PreviewGenerator : IPreviewGenerator { } else -> { + Log.i("PreviewImg", "unsupported format for $link") currentGenerator.clear(keepCache) } } @@ -193,6 +194,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { // no support for encryption atm if (hsl.isEncrypted) { + Log.i(TAG, "m3u8 is encrypted") totalImages = 0 return@withContext } @@ -239,12 +241,13 @@ class M3u8PreviewGenerator : IPreviewGenerator { if (!isActive) { return@withContext } - val frame = retriever.getFrameAtTime(0) + val img = retriever.getFrameAtTime(0) if (!isActive) { return@withContext } + if(img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { - images[index] = frame + images[index] = img loadedImages += 1 } } catch (t: Throwable) { @@ -302,6 +305,9 @@ class Mp4PreviewGenerator : IPreviewGenerator { if (idx > loadedImages) { break } + if(images[idx] == null) { + continue + } val currentFraction = (1.0f.div((1 shl l).toFloat()) + i * 1.0f.div(items.toFloat())) val diff = currentFraction.minus(fraction).absoluteValue @@ -382,6 +388,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { MediaMetadataRetriever.OPTION_CLOSEST_SYNC ) if (!scope.isActive) return + if(img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { images[idx] = img loadedImages = maxOf(loadedImages, idx) diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index d3fe7162..298f1601 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -115,19 +115,19 @@ object M3u8Helper2 { private fun selectBest(qualities: List): M3u8Helper.M3u8Stream? { val result = qualities.sortedBy { - if (it.quality != null && it.quality <= 1080) it.quality else 0 - }.filter { + it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0 + }/*.filter { listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) - } + }*/ return result.lastOrNull() } private fun selectWorst(qualities: List): M3u8Helper.M3u8Stream? { val result = qualities.sortedBy { - if (it.quality != null && it.quality <= 1080) it.quality else 0 - }.filter { + it.quality ?: Qualities.Unknown.value //if (it.quality != null && it.quality <= 1080) else 0 + }/*.filter { listOf("m3u", "m3u8").contains(absoluteExtensionDetermination(it.streamUrl)) - } + }*/ return result.firstOrNull() } From 0a327ccbda8d3622127cae2e419a386357250b6e Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:50:31 -0600 Subject: [PATCH 27/42] Reload library when reloading home (#656) So that library is reloaded when switching accounts. Fixes #650 --- .../cloudstream3/ui/library/LibraryViewModel.kt | 10 ++++++++++ .../lagradost/cloudstream3/utils/DataStoreHelper.kt | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index 25a5a0f8..c104a7c3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis @@ -101,4 +102,13 @@ class LibraryViewModel : ViewModel() { } } } + + init { + MainActivity.reloadHomeEvent += ::reloadPages + } + + override fun onCleared() { + MainActivity.reloadHomeEvent -= ::reloadPages + super.onCleared() + } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 7bce1b6c..775cb718 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -131,7 +131,6 @@ object DataStoreHelper { // update UI setAccount(getDefaultAccount(context), true) - MainActivity.bookmarksUpdatedEvent(true) dialog?.dismissSafe() } From 77294dc68e6ba19c21017f4ac17847755572ad18 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 7 Oct 2023 01:39:30 +0200 Subject: [PATCH 28/42] cleanup --- .../lagradost/cloudstream3/MainActivity.kt | 40 +++--- .../cloudstream3/ui/home/HomeFragment.kt | 25 +--- .../cloudstream3/ui/home/HomeViewModel.kt | 22 ++- .../ui/player/AbstractPlayerFragment.kt | 11 +- .../ui/player/FullScreenPlayer.kt | 6 +- .../cloudstream3/ui/player/IPlayer.kt | 9 -- .../ui/player/PreviewGenerator.kt | 134 ++++++++++++------ .../cloudstream3/ui/search/SearchFragment.kt | 43 ++---- .../cloudstream3/utils/DataStoreHelper.kt | 68 +++++++++ .../main/res/layout/player_custom_layout.xml | 4 +- 10 files changed, 215 insertions(+), 147 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index d5187029..17823f7c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1122,23 +1122,25 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (isTvSettings()) { val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) setContentView(newLocalBinding.root) - TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) - newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> - // println("refocus $oldFocus -> $newFocus") - try { - val r = Rect(0, 0, 0, 0) - newFocus.getDrawingRect(r) - val x = r.centerX() - val y = r.centerY() - val dx = 0 //screenWidth / 2 - val dy = screenHeight / 2 - val r2 = Rect(x - dx, y - dy, x + dx, y + dy) - newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_: Throwable) { - } - TvFocus.updateFocusView(newFocus) - /*var focus = newFocus + + if(isTrueTvSettings()) { + TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> + // println("refocus $oldFocus -> $newFocus") + try { + val r = Rect(0, 0, 0, 0) + newFocus.getDrawingRect(r) + val x = r.centerX() + val y = r.centerY() + val dx = 0 //screenWidth / 2 + val dy = screenHeight / 2 + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) + newFocus.requestRectangleOnScreen(r2, false) + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } + TvFocus.updateFocusView(newFocus) + /*var focus = newFocus while(focus != null) { if(focus is ScrollingView && focus.canScrollVertically()) { @@ -1149,7 +1151,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { else -> break } }*/ + } + } else { + newLocalBinding.focusOutline.isVisible = false } + newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) } 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 ebbb245c..4d940123 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 @@ -7,7 +7,6 @@ import android.content.DialogInterface import android.content.Intent import android.content.res.Configuration import android.net.Uri -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -23,18 +22,12 @@ import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.button.MaterialButton import com.google.android.material.chip.Chip -import com.google.android.material.chip.ChipGroup import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.apis import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.CommonActivity.showToast -import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent -import com.lagradost.cloudstream3.MainActivity.Companion.bookmarksUpdatedEvent -import com.lagradost.cloudstream3.MainActivity.Companion.mainPluginsLoadedEvent import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.HomeEpisodesExpandedBinding import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding @@ -45,37 +38,26 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable 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.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.* import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings -import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable -import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.ioSafe -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.Event import com.lagradost.cloudstream3.utils.SubtitleHelper.getFlagFromIso import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe -import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes - import java.util.* -const val HOME_BOOKMARK_VALUE_LIST = "home_bookmarked_last_list" -const val HOME_PREF_HOMEPAGE = "home_pref_homepage" - class HomeFragment : Fragment() { companion object { val configEvent = Event() @@ -377,10 +359,7 @@ class HomeFragment : Fragment() { var currentApiName = selectedApiName var currentValidApis: MutableList = mutableListOf() - val preSelectedTypes = this.getKey>("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE") - ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + val preSelectedTypes = DataStoreHelper.homePreference.toMutableList() binding.cancelBtt.setOnClickListener { dialog.dismissSafe() @@ -408,7 +387,7 @@ class HomeFragment : Fragment() { } fun updateList() { - this.setKey("${DataStoreHelper.currentAccount}/$HOME_PREF_HOMEPAGE", preSelectedTypes) + DataStoreHelper.homePreference = preSelectedTypes arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> 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 a5ef2bb4..ad75aa9d 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 @@ -12,7 +12,6 @@ import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse @@ -170,10 +169,7 @@ class HomeViewModel : ViewModel() { currentWatchTypes.remove(WatchType.NONE) if (currentWatchTypes.size <= 0) { - setKey( - "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", - intArrayOf() - ) + DataStoreHelper.homeBookmarkedList = intArrayOf() _availableWatchStatusTypes.postValue(setOf() to setOf()) _bookmarks.postValue(Pair(false, ArrayList())) return@launchSafe @@ -181,16 +177,14 @@ class HomeViewModel : ViewModel() { val watchPrefNotNull = preferredWatchStatus ?: EnumSet.of(currentWatchTypes.first()) //if (currentWatchTypes.any { watchPrefNotNull.contains(it) }) watchPrefNotNull else listOf(currentWatchTypes.first()) - setKey( - "${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST", - watchPrefNotNull.map { it.internalId }.toIntArray() - ) + + DataStoreHelper.homeBookmarkedList = watchPrefNotNull.map { it.internalId }.toIntArray() _availableWatchStatusTypes.postValue( - Pair( - watchPrefNotNull, - currentWatchTypes, + + watchPrefNotNull to + currentWatchTypes, + ) - ) val list = withContext(Dispatchers.IO) { watchStatusIds.filter { watchPrefNotNull.contains(it.second) } @@ -463,7 +457,7 @@ class HomeViewModel : ViewModel() { fun loadStoredData() { val list = EnumSet.noneOf(WatchType::class.java) - getKey("${DataStoreHelper.currentAccount}/$HOME_BOOKMARK_VALUE_LIST")?.map { WatchType.fromInternalId(it) }?.let { + DataStoreHelper.homeBookmarkedList.map { WatchType.fromInternalId(it) }.let { list.addAll(it) } loadStoredData(list) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt index 52974ff7..431e4fe1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/AbstractPlayerFragment.kt @@ -33,8 +33,6 @@ import androidx.preference.PreferenceManager import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.github.rubensousa.previewseekbar.PreviewBar import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.canEnterPipMode import com.lagradost.cloudstream3.CommonActivity.isInPIPMode import com.lagradost.cloudstream3.CommonActivity.keyEventListener @@ -48,7 +46,7 @@ import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.utils.AppUtils import com.lagradost.cloudstream3.utils.AppUtils.requestLocalAudioFocus -import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.EpisodeSkip import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI @@ -443,7 +441,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("SetTextI18n", "UnsafeOptInUsageError") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - resizeMode = getKey("$currentAccount/$RESIZE_MODE_KEY") ?: 0 + resizeMode = DataStoreHelper.resizeMode resize(resizeMode, false) player.releaseCallbacks() @@ -466,7 +464,8 @@ abstract class AbstractPlayerFragment( var resume = false progressBar.addOnScrubListener(object : PreviewBar.OnScrubListener { override fun onScrubStart(previewBar: PreviewBar?) { - progressBar.isPreviewEnabled = player.hasPreview() + val hasPreview = player.hasPreview() + progressBar.isPreviewEnabled = hasPreview resume = player.getIsPlaying() if (resume) player.handleEvent( CSPlayerEvent.Pause, @@ -574,7 +573,7 @@ abstract class AbstractPlayerFragment( @SuppressLint("UnsafeOptInUsageError") fun resize(resize: PlayerResize, showToast: Boolean) { - setKey("$currentAccount/$RESIZE_MODE_KEY", resize.ordinal) + DataStoreHelper.resizeMode = resize.ordinal val type = when (resize) { PlayerResize.Fill -> AspectRatioFrameLayout.RESIZE_MODE_FILL PlayerResize.Fit -> AspectRatioFrameLayout.RESIZE_MODE_FIT diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt index 43e8aa0b..819e50ba 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/FullScreenPlayer.kt @@ -49,7 +49,7 @@ import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData -import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -357,7 +357,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private fun setPlayBackSpeed(speed: Float) { try { - setKey("$currentAccount/$PLAYBACK_SPEED_KEY", speed) + DataStoreHelper.playBackSpeed = speed playerBinding?.playerSpeedBtt?.text = getString(R.string.player_speed_text_format).format(speed) .replace(".0x", "x") @@ -1195,7 +1195,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // init variables - setPlayBackSpeed(getKey("$currentAccount/$PLAYBACK_SPEED_KEY") ?: 1.0f) + setPlayBackSpeed(DataStoreHelper.playBackSpeed) savedInstanceState?.getLong(SUBTITLE_DELAY_BUNDLE_KEY)?.let { subtitleDelay = it } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt index a08360ae..0e54e2cb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/IPlayer.kt @@ -199,17 +199,8 @@ data class CurrentTracks( class InvalidFileException(msg: String) : Exception(msg) //http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 -const val STATE_RESUME_WINDOW = "resumeWindow" -const val STATE_RESUME_POSITION = "resumePosition" -const val STATE_PLAYER_FULLSCREEN = "playerFullscreen" -const val STATE_PLAYER_PLAYING = "playerOnPlay" const val ACTION_MEDIA_CONTROL = "media_control" const val EXTRA_CONTROL_TYPE = "control_type" -const val PLAYBACK_SPEED = "playback_speed" -const val RESIZE_MODE_KEY = "resize_mode" // Last used resize mode -const val PLAYBACK_SPEED_KEY = "playback_speed" // Last used playback speed -const val PREFERRED_SUBS_KEY = "preferred_subtitles" // Last used resize mode -//const val PLAYBACK_FASTFORWARD = "playback_fastforward" // Last used resize mode /** Abstract Exoplayer logic, can be expanded to other players */ interface IPlayer { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt index 946c1d33..ffb4751f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PreviewGenerator.kt @@ -28,86 +28,122 @@ const val MIN_LOD = 3 interface IPreviewGenerator { fun hasPreview(): Boolean fun getPreviewImage(fraction: Float): Bitmap? - fun clear(keepCache: Boolean = false) fun release() + + var durationMs: Long + var loadedImages: Int } +/** PreviewGenerator that hides the implementation details of the sub generators that is used, used for source switch cache */ class PreviewGenerator : IPreviewGenerator { + /** the most up to date generator, will always mirror the actual source in the player */ private var currentGenerator: IPreviewGenerator = NoPreviewGenerator() + /** the longest generated preview of the same episode */ + private var lastGenerator: IPreviewGenerator = NoPreviewGenerator() + /** always NoPreviewGenerator, used as a cache for nothing */ + private val dummy: IPreviewGenerator = NoPreviewGenerator() + + /** if the current generator is the same as the last by checking time */ + private fun isSameLength(): Boolean = + currentGenerator.durationMs.minus(lastGenerator.durationMs).absoluteValue < 10_000L + + /** use the backup if the current generator is init or if they have the same length */ + private val backupGenerator: IPreviewGenerator + get() { + if (currentGenerator.durationMs == 0L || isSameLength()) { + return lastGenerator + } + return dummy + } + override fun hasPreview(): Boolean { - return currentGenerator.hasPreview() + return currentGenerator.hasPreview() || backupGenerator.hasPreview() } override fun getPreviewImage(fraction: Float): Bitmap? { return try { - currentGenerator.getPreviewImage(fraction) + currentGenerator.getPreviewImage(fraction) ?: backupGenerator.getPreviewImage(fraction) } catch (t: Throwable) { logError(t) null } } - override fun clear(keepCache: Boolean) { - currentGenerator.clear(keepCache) + override fun release() { + lastGenerator.release() + currentGenerator.release() + lastGenerator = NoPreviewGenerator() + currentGenerator = NoPreviewGenerator() } - override fun release() { - currentGenerator.release() + override var durationMs: Long + get() = currentGenerator.durationMs + set(_) {} + override var loadedImages: Int + get() = currentGenerator.loadedImages + set(_) {} + + fun clear(keepCache: Boolean) { + if (keepCache) { + if (!isSameLength() || currentGenerator.loadedImages >= lastGenerator.loadedImages || lastGenerator.durationMs == 0L) { + // the current generator is better than the last generator, therefore keep the current + // or the lengths are not the same, therefore favoring the more recent selection + + // if they are the same we favor the current generator + lastGenerator.release() + lastGenerator = currentGenerator + } else { + // otherwise just keep the last generator and throw away the current generator + currentGenerator.release() + } + } else { + // we switched the episode, therefore keep nothing + lastGenerator.release() + lastGenerator = NoPreviewGenerator() + currentGenerator.release() + // we assume that we set currentGenerator right after this, so currentGenerator != NoPreviewGenerator + } } fun load(link: ExtractorLink, keepCache: Boolean) { - val gen = currentGenerator + clear(keepCache) + when (link.type) { ExtractorLinkType.M3U8 -> { - if (gen is M3u8PreviewGenerator) { - gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } else { - currentGenerator.release() - currentGenerator = M3u8PreviewGenerator().apply { - load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } + currentGenerator = M3u8PreviewGenerator().apply { + load(url = link.url, headers = link.getAllHeaders()) } } ExtractorLinkType.VIDEO -> { - if (gen is Mp4PreviewGenerator) { - gen.load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } else { - currentGenerator.release() - currentGenerator = Mp4PreviewGenerator().apply { - load(keepCache = keepCache, url = link.url, headers = link.getAllHeaders()) - } + currentGenerator = Mp4PreviewGenerator().apply { + load(url = link.url, headers = link.getAllHeaders()) } } else -> { Log.i("PreviewImg", "unsupported format for $link") - currentGenerator.clear(keepCache) } } } fun load(context: Context, link: ExtractorUri, keepCache: Boolean) { - val gen = currentGenerator - if (gen is Mp4PreviewGenerator) { - gen.load(keepCache = keepCache, context = context, uri = link.uri) - } else { - currentGenerator.release() - currentGenerator = Mp4PreviewGenerator().apply { - load(keepCache = keepCache, context = context, uri = link.uri) - } + clear(keepCache) + currentGenerator = Mp4PreviewGenerator().apply { + load(keepCache = keepCache, context = context, uri = link.uri) } } } -class NoPreviewGenerator : IPreviewGenerator { +private class NoPreviewGenerator : IPreviewGenerator { override fun hasPreview(): Boolean = false override fun getPreviewImage(fraction: Float): Bitmap? = null - override fun clear(keepCache: Boolean) = Unit override fun release() = Unit + override var durationMs: Long = 0L + override var loadedImages: Int = 0 } -class M3u8PreviewGenerator : IPreviewGenerator { +private class M3u8PreviewGenerator : IPreviewGenerator { // generated images 1:1 to idx of hsl private var images: Array = arrayOf() @@ -118,7 +154,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { private var prefixSum: Array = arrayOf() // how many images has been generated - private var loadedImages: Int = 0 + override var loadedImages: Int = 0 // how many images we can generate in total, == hsl.size ?: 0 private var totalImages: Int = 0 @@ -155,7 +191,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { }*/ } - override fun clear(keepCache: Boolean) { + private fun clear() { synchronized(images) { currentJob?.cancel() images = arrayOf() @@ -170,9 +206,11 @@ class M3u8PreviewGenerator : IPreviewGenerator { images = arrayOf() } + override var durationMs: Long = 0L + private var currentJob: Job? = null - fun load(keepCache: Boolean, url: String, headers: Map) { - clear(keepCache) + fun load(url: String, headers: Map) { + clear() currentJob?.cancel() currentJob = ioSafe { withContext(Dispatchers.IO) { @@ -201,6 +239,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { // total duration of the entire m3u8 in seconds val duration = hsl.allTsLinks.sumOf { it.time ?: 0.0 } + durationMs = (duration * 1000.0).toLong() val durationInv = 1.0 / duration // if the total duration is less then 10s then something is very wrong or @@ -245,7 +284,7 @@ class M3u8PreviewGenerator : IPreviewGenerator { if (!isActive) { return@withContext } - if(img == null || img.width <= 1 || img.height <= 1) continue + if (img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { images[index] = img loadedImages += 1 @@ -269,11 +308,11 @@ class M3u8PreviewGenerator : IPreviewGenerator { } } -class Mp4PreviewGenerator : IPreviewGenerator { +private class Mp4PreviewGenerator : IPreviewGenerator { // lod = level of detail where the number indicates how many ones there is // 2^(lod-1) = images private var loadedLod = 0 - private var loadedImages = 0 + override var loadedImages = 0 private var images = Array((1 shl MAX_LOD) - 1) { null } @@ -305,7 +344,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { if (idx > loadedImages) { break } - if(images[idx] == null) { + if (images[idx] == null) { continue } val currentFraction = @@ -325,7 +364,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { // also check out https://github.com/wseemann/FFmpegMediaMetadataRetriever private val retriever: MediaMetadataRetriever = MediaMetadataRetriever() - override fun clear(keepCache: Boolean) { + private fun clear(keepCache: Boolean) { if (keepCache) return synchronized(images) { loadedLod = 0 @@ -335,11 +374,11 @@ class Mp4PreviewGenerator : IPreviewGenerator { } private var currentJob: Job? = null - fun load(keepCache: Boolean, url: String, headers: Map) { + fun load(url: String, headers: Map) { currentJob?.cancel() currentJob = ioSafe { Log.i(TAG, "Loading with url = $url headers = $headers") - clear(keepCache) + clear(true) retriever.setDataSource(url, headers) start(this) } @@ -360,6 +399,8 @@ class Mp4PreviewGenerator : IPreviewGenerator { clear(false) } + override var durationMs: Long = 0L + @Throws @WorkerThread private fun start(scope: CoroutineScope) { @@ -368,6 +409,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: throw IllegalArgumentException("Bad video duration") + this.durationMs = durationMs val durationUs = (durationMs * 1000L).toFloat() //val width = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: throw IllegalArgumentException("Bad video width") //val height = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: throw IllegalArgumentException("Bad video height") @@ -388,7 +430,7 @@ class Mp4PreviewGenerator : IPreviewGenerator { MediaMetadataRetriever.OPTION_CLOSEST_SYNC ) if (!scope.isActive) return - if(img == null || img.width <= 1 || img.height <= 1) continue + if (img == null || img.width <= 1 || img.height <= 1) continue synchronized(images) { images[idx] = img loadedImages = maxOf(loadedImages, idx) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index bad78624..0e994be8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -22,17 +22,22 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.button.MaterialButton -import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.filterSearchResultByFilmQuality import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiProviderLangSettings import com.lagradost.cloudstream3.APIHolder.getApiSettings -import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKeys -import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.AllLanguagesName +import com.lagradost.cloudstream3.AnimeSearchResponse +import com.lagradost.cloudstream3.HomePageList +import com.lagradost.cloudstream3.MainAPI +import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.SearchResponse +import com.lagradost.cloudstream3.TvType import com.lagradost.cloudstream3.databinding.FragmentSearchBinding import com.lagradost.cloudstream3.databinding.HomeSelectMainpageBinding import com.lagradost.cloudstream3.mvvm.Resource @@ -53,8 +58,7 @@ import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.Coroutines.main -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.currentAccount import com.lagradost.cloudstream3.utils.SubtitleHelper import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe @@ -63,9 +67,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import java.util.concurrent.locks.ReentrantLock -const val SEARCH_PREF_TAGS = "search_pref_tags" -const val SEARCH_PREF_PROVIDERS = "search_pref_providers" - class SearchFragment : Fragment() { companion object { fun List.filterSearchResponse(): List { @@ -194,7 +195,7 @@ class SearchFragment : Fragment() { validAPIs.flatMap { api -> api.supportedTypes }.distinct() ) { list -> if (selectedSearchTypes.toSet() != list.toSet()) { - setKey("$currentAccount/$SEARCH_PREF_TAGS", selectedSearchTypes) + DataStoreHelper.searchPreferenceTags = list selectedSearchTypes.clear() selectedSearchTypes.addAll(list) search(binding?.mainSearch?.query?.toString()) @@ -233,13 +234,7 @@ class SearchFragment : Fragment() { //searchMagIcon.scaleX = 0.65f //searchMagIcon.scaleY = 0.65f - context?.let { ctx -> - val validAPIs = ctx.filterProviderByPreferredMedia() - selectedApis = ctx.getKey( - "$currentAccount/$SEARCH_PREF_PROVIDERS", - defVal = validAPIs.map { it.name } - )!!.toMutableSet() - } + selectedApis = DataStoreHelper.searchPreferenceProviders.toMutableSet() binding?.searchFilter?.setOnClickListener { searchView -> searchView?.context?.let { ctx -> @@ -287,7 +282,7 @@ class SearchFragment : Fragment() { } fun updateList(types: List) { - setKey("$currentAccount/$SEARCH_PREF_TAGS", types.map { it.name }) + DataStoreHelper.searchPreferenceTags = types arrayAdapter.clear() currentValidApis = validAPIs.filter { api -> @@ -312,12 +307,7 @@ class SearchFragment : Fragment() { arrayAdapter.notifyDataSetChanged() } - val selectedSearchTypes = getKey>("$currentAccount/$SEARCH_PREF_TAGS") - ?.mapNotNull { listName -> - TvType.values().firstOrNull { it.name == listName } - } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + val selectedSearchTypes = DataStoreHelper.searchPreferenceTags bindChips( binding.tvtypesChipsScroll.tvtypesChips, @@ -343,7 +333,7 @@ class SearchFragment : Fragment() { } dialog.setOnDismissListener { - context?.setKey("$currentAccount/$SEARCH_PREF_PROVIDERS", currentSelectedApis.toList()) + DataStoreHelper.searchPreferenceProviders = currentSelectedApis.toList() selectedApis = currentSelectedApis } updateList(selectedSearchTypes.toList()) @@ -354,10 +344,7 @@ class SearchFragment : Fragment() { val settingsManager = context?.let { PreferenceManager.getDefaultSharedPreferences(it) } val isAdvancedSearch = settingsManager?.getBoolean("advanced_search", true) ?: true - selectedSearchTypes = context?.getKey>("$currentAccount/$SEARCH_PREF_TAGS") - ?.mapNotNull { listName -> TvType.values().firstOrNull { it.name == listName } } - ?.toMutableList() - ?: mutableListOf(TvType.Movie, TvType.TvSeries) + selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList() if (isTrueTvSettings()) { binding?.searchFilter?.isFocusable = true diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 775cb718..10c0546f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -10,7 +10,9 @@ import androidx.core.widget.doOnTextChanged import com.fasterxml.jackson.annotation.JsonProperty import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.* +import com.lagradost.cloudstream3.APIHolder.filterProviderByPreferredMedia import com.lagradost.cloudstream3.APIHolder.unixTimeMS +import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKeys import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey @@ -31,6 +33,8 @@ import com.lagradost.cloudstream3.ui.result.setImage import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import kotlin.reflect.KClass +import kotlin.reflect.KProperty const val VIDEO_POS_DUR = "video_pos_dur" const val VIDEO_WATCH_STATE = "video_watch_state" @@ -44,6 +48,28 @@ const val RESULT_EPISODE = "result_episode" const val RESULT_SEASON = "result_season" const val RESULT_DUB = "result_dub" + +class UserPreferenceDelegate( + private val key: String, private val default: T //, private val klass: KClass +) { + private val klass: KClass = default::class + private val realKey get() = "${DataStoreHelper.currentAccount}/$key" + operator fun getValue(self: Any?, property: KProperty<*>) = + AcraApplication.getKeyClass(realKey, klass.java) ?: default + + operator fun setValue( + self: Any?, + property: KProperty<*>, + t: T? + ) { + if (t == null) { + removeKey(realKey) + } else { + AcraApplication.setKeyClass(realKey, t) + } + } +} + object DataStoreHelper { // be aware, don't change the index of these as Account uses the index for the art private val profileImages = arrayOf( @@ -56,6 +82,48 @@ object DataStoreHelper { R.drawable.profile_bg_teal ) + private var searchPreferenceProvidersStrings : List by UserPreferenceDelegate( + /** java moment right here, as listOf()::class.java != List(0) { "" }::class.java */ + "search_pref_providers", List(0) { "" } + ) + + private fun serializeTv(data : List) : List = data.map { it.name } + + private fun deserializeTv(data : List) : List { + return data.mapNotNull { listName -> + TvType.values().firstOrNull { it.name == listName } + } + } + + var searchPreferenceProviders : List + get() { + val ret = searchPreferenceProvidersStrings + return ret.ifEmpty { + context?.filterProviderByPreferredMedia()?.map { it.name } ?: emptyList() + } + } set(value) { + searchPreferenceProvidersStrings = value + } + + private var searchPreferenceTagsStrings : List by UserPreferenceDelegate("search_pref_tags", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) + var searchPreferenceTags : List + get() = deserializeTv(searchPreferenceTagsStrings) + set(value) { + searchPreferenceTagsStrings = serializeTv(value) + } + + + private var homePreferenceStrings : List by UserPreferenceDelegate("home_pref_homepage", listOf(TvType.Movie, TvType.TvSeries).map { it.name }) + var homePreference : List + get() = deserializeTv(homePreferenceStrings) + set(value) { + homePreferenceStrings = serializeTv(value) + } + + var homeBookmarkedList : IntArray by UserPreferenceDelegate("home_bookmarked_last_list", IntArray(0)) + var playBackSpeed : Float by UserPreferenceDelegate("playback_speed", 1.0f) + var resizeMode : Int by UserPreferenceDelegate("resize_mode", 0) + data class Account( @JsonProperty("keyIndex") val keyIndex: Int, diff --git a/app/src/main/res/layout/player_custom_layout.xml b/app/src/main/res/layout/player_custom_layout.xml index 0f76e4dd..38df4c5b 100644 --- a/app/src/main/res/layout/player_custom_layout.xml +++ b/app/src/main/res/layout/player_custom_layout.xml @@ -349,14 +349,15 @@ android:layout_width="150dp" android:layout_height="40dp" android:layout_marginEnd="100dp" + android:layout_marginTop="60dp" android:backgroundTint="@color/skipOpTransparent" android:maxLines="1" android:padding="10dp" android:textColor="@color/white" android:visibility="gone" app:cornerRadius="@dimen/rounded_button_radius" - app:layout_constraintBottom_toTopOf="@+id/bottom_player_bar" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/player_top_holder" app:strokeColor="@color/white" app:strokeWidth="1dp" tools:text="Skip Opening" @@ -435,6 +436,7 @@ android:id="@+id/player_video_bar" android:layout_width="match_parent" android:layout_height="wrap_content" + tools:visibility="visible" android:layoutDirection="ltr" android:orientation="horizontal"> From f14557fe6a3268b07ea1421df109367e7d040da0 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 7 Oct 2023 01:54:34 +0200 Subject: [PATCH 29/42] lib fix --- .../lagradost/cloudstream3/ui/library/LibraryFragment.kt | 8 ++++++++ .../lagradost/cloudstream3/ui/library/LibraryViewModel.kt | 2 ++ 2 files changed, 10 insertions(+) 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 d5fdc1aa..a3cc16c9 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 @@ -15,6 +15,7 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayoutMediator import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.allProviders @@ -229,6 +230,7 @@ class LibraryFragment : Fragment() { } binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) + binding?.viewpager?.adapter = binding?.viewpager?.adapter ?: ViewpagerAdapter( mutableListOf(), @@ -357,6 +359,7 @@ class LibraryFragment : Fragment() { 0, viewpager.adapter?.itemCount ?: 0 ) + binding?.viewpager?.setCurrentItem(libraryViewModel.currentPage, false) // Only stop loading after 300ms to hide the fade effect the viewpager produces when updating // Without this there would be a flashing effect: @@ -415,6 +418,11 @@ class LibraryFragment : Fragment() { } } } + binding?.viewpager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + libraryViewModel.currentPage = position + } + }) } override fun onConfigurationChanged(newConfig: Configuration) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index c104a7c3..e590b151 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -30,6 +30,8 @@ class LibraryViewModel : ViewModel() { private val _pages: MutableLiveData>> = MutableLiveData(null) val pages: LiveData>> = _pages + var currentPage : Int = 0 + private val _currentApiName: MutableLiveData = MutableLiveData("") val currentApiName: LiveData = _currentApiName From 33eb3a3b29a1dd7dd21a0847222bac1e6cc04bf5 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 7 Oct 2023 21:48:24 +0200 Subject: [PATCH 30/42] lib fix2 --- .../ui/library/LibraryViewModel.kt | 32 +++++++++++++------ .../cloudstream3/utils/DataStoreHelper.kt | 2 ++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt index e590b151..b44913d9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/LibraryViewModel.kt @@ -12,6 +12,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.SyncApis import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.utils.Coroutines.ioSafe +import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount enum class ListSorting(@StringRes val stringRes: Int) { @@ -30,7 +31,7 @@ class LibraryViewModel : ViewModel() { private val _pages: MutableLiveData>> = MutableLiveData(null) val pages: LiveData>> = _pages - var currentPage : Int = 0 + var currentPage: Int = 0 private val _currentApiName: MutableLiveData = MutableLiveData("") val currentApiName: LiveData = _currentApiName @@ -62,13 +63,21 @@ class LibraryViewModel : ViewModel() { reloadPages(true) } - fun sort(method: ListSorting, query: String? = null) { - val currentList = pages.value ?: return + fun sort(method: ListSorting, query: String? = null) = ioSafe { + val value = _pages.value ?: return@ioSafe + if (value is Resource.Success) { + sort(method, query, value.value) + } + } + + private fun sort(method: ListSorting, query: String? = null, items: List) { currentSortingMethod = method - (currentList as? Resource.Success)?.value?.forEachIndexed { _, page -> + DataStoreHelper.librarySortingMode = method.ordinal + + items.forEach { page -> page.sort(method, query) } - _pages.postValue(currentList) + _pages.postValue(Resource.Success(items)) } fun reloadPages(forceReload: Boolean) { @@ -89,8 +98,6 @@ class LibraryViewModel : ViewModel() { val library = (libraryResource as? Resource.Success)?.value ?: return@let sortingMethods = library.supportedListSorting.toList() - currentSortingMethod = null - repo.requireLibraryRefresh = false val pages = library.allLibraryLists.map { @@ -100,11 +107,18 @@ class LibraryViewModel : ViewModel() { ) } - _pages.postValue(Resource.Success(pages)) + val desiredSortingMethod = + ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode) + if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) { + sort(desiredSortingMethod, null, pages) + } else { + // null query = no sorting + sort(ListSorting.Query, null, pages) + } } } } - + init { MainActivity.reloadHomeEvent += ::reloadPages } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt index 10c0546f..952422a4 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStoreHelper.kt @@ -26,6 +26,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WhoIsWatchingAdapter +import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.VideoWatchState @@ -123,6 +124,7 @@ object DataStoreHelper { var homeBookmarkedList : IntArray by UserPreferenceDelegate("home_bookmarked_last_list", IntArray(0)) var playBackSpeed : Float by UserPreferenceDelegate("playback_speed", 1.0f) var resizeMode : Int by UserPreferenceDelegate("resize_mode", 0) + var librarySortingMode : Int by UserPreferenceDelegate("library_sorting_mode", ListSorting.AlphabeticalA.ordinal) data class Account( @JsonProperty("keyIndex") From d277d8a9aabce833b6384948716a5c3d8c680807 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:56:30 +0200 Subject: [PATCH 31/42] bump upstream --- app/build.gradle.kts | 36 +++++++++++++++--------------- app/src/main/res/values/styles.xml | 2 +- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f484c48..639932c4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -50,16 +50,16 @@ android { } } - compileSdk = 33 + compileSdk = 34 buildToolsVersion = "34.0.0" defaultConfig { applicationId = "com.lagradost.cloudstream3" minSdk = 21 - targetSdk = 33 + targetSdk = 34 versionCode = 62 - versionName = "4.2.0" + versionName = "4.2.1" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") @@ -154,18 +154,18 @@ repositories { dependencies { implementation("com.google.android.mediahome:video:1.0.0") implementation("androidx.test.ext:junit-ktx:1.1.5") - testImplementation("org.json:json:20180813") + testImplementation("org.json:json:20230618") - implementation("androidx.core:core-ktx:1.10.1") + implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") // need target 32 for 1.5.0 // dont change this to 1.6.0 it looks ugly af - implementation("com.google.android.material:material:1.5.0") + implementation("com.google.android.material:material:1.10.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") - implementation("androidx.navigation:navigation-fragment-ktx:2.6.0") - implementation("androidx.navigation:navigation-ui-ktx:2.6.0") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.1") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.4") + implementation("androidx.navigation:navigation-ui-ktx:2.7.4") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") @@ -173,9 +173,9 @@ dependencies { // implementation("io.karn:khttp-android:0.1.2") //okhttp instead // implementation("org.jsoup:jsoup:1.13.1") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.2") - implementation("androidx.preference:preference-ktx:1.2.0") + implementation("androidx.preference:preference-ktx:1.2.1") implementation("com.github.bumptech.glide:glide:4.13.1") kapt("com.github.bumptech.glide:compiler:4.13.1") @@ -200,14 +200,14 @@ dependencies { implementation("com.github.recloudstream:media-ffmpeg:1.1.0") // Bug reports - implementation("ch.acra:acra-core:5.11.0") - implementation("ch.acra:acra-toast:5.11.0") + implementation("ch.acra:acra-core:5.11.2") + implementation("ch.acra:acra-toast:5.11.2") - compileOnly("com.google.auto.service:auto-service-annotations:1.0") + compileOnly("com.google.auto.service:auto-service-annotations:1.1.1") //either for java sources: - annotationProcessor("com.google.auto.service:auto-service:1.0") + annotationProcessor("com.google.auto.service:auto-service:1.1.1") //or for kotlin sources (requires kapt gradle plugin): - kapt("com.google.auto.service:auto-service:1.0") + kapt("com.google.auto.service:auto-service:1.1.1") // subtitle color picker implementation("com.jaredrummler:colorpicker:1.1.0") @@ -229,7 +229,7 @@ dependencies { // implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.9.1") implementation("com.github.Blatzar:NiceHttp:0.4.3") // To fix SSL fuckery on android 9 - implementation("org.conscrypt:conscrypt-android:2.2.1") + implementation("org.conscrypt:conscrypt-android:2.5.2") // Util to skip the URI file fuckery 🙏 implementation("com.github.LagradOst:SafeFile:0.0.5") diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e2f11221..5f7adea4 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -388,7 +388,7 @@ true @null @null - @color/transparent + ?attr/primaryBlackBackground @drawable/rounded_dialog 512dp From 5b4fd8d77de24dbc012d6c2814e35db26d251fa9 Mon Sep 17 00:00:00 2001 From: Luna712 <142361265+Luna712@users.noreply.github.com> Date: Tue, 10 Oct 2023 09:05:34 -0600 Subject: [PATCH 32/42] Fix issue where DownloadedPlayerActivity interferes with MainActivity (#674) * Fix issue where DownloadedPlayerActivity interferes with MainActivity --- app/src/main/AndroidManifest.xml | 4 ++- .../lagradost/cloudstream3/CommonActivity.kt | 27 ++++++++++++------- .../lagradost/cloudstream3/MainActivity.kt | 2 ++ .../ui/player/DownloadedPlayerActivity.kt | 5 ++++ 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 15767d7b..503cd76b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -61,7 +61,9 @@ android:exported="true" android:resizeableActivity="true" android:screenOrientation="userLandscape" - android:supportsPictureInPicture="true"> + android:supportsPictureInPicture="true" + android:taskAffinity="com.lagradost.cloudstream3.downloadedplayer" + android:launchMode="singleTask"> diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index a7d899b6..16a438b3 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -65,6 +65,11 @@ object CommonActivity { _activity = WeakReference(value) } + @MainThread + fun setActivityInstance(newActivity: Activity?) { + activity = newActivity + } + @MainThread fun Activity?.getCastSession(): CastSession? { return (this as MainActivity?)?.mSessionManager?.currentCastSession @@ -203,23 +208,25 @@ object CommonActivity { setLocale(this, localeCode) } - fun init(act: ComponentActivity?) { - if (act == null) return - activity = act + fun init(act: Activity) { + setActivityInstance(act) + + val componentActivity = activity as? ComponentActivity ?: return + //https://stackoverflow.com/questions/52594181/how-to-know-if-user-has-disabled-picture-in-picture-feature-permission //https://developer.android.com/guide/topics/ui/picture-in-picture canShowPipMode = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && // OS SUPPORT - act.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN - act.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS + componentActivity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && // HAS FEATURE, MIGHT BE BLOCKED DUE TO POWER DRAIN + componentActivity.hasPIPPermission() // CHECK IF FEATURE IS ENABLED IN SETTINGS - act.updateLocale() - act.updateTv() + componentActivity.updateLocale() + componentActivity.updateTv() NewPipe.init(DownloaderTestImpl.getInstance()) for (resumeApp in resumeApps) { resumeApp.launcher = - act.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + componentActivity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val resultCode = result.resultCode val data = result.data if (resultCode == AppCompatActivity.RESULT_OK && data != null && resumeApp.position != null && resumeApp.duration != null) { @@ -236,11 +243,11 @@ object CommonActivity { // Ask for notification permissions on Android 13 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ContextCompat.checkSelfPermission( - act, + componentActivity, Manifest.permission.POST_NOTIFICATIONS ) != PackageManager.PERMISSION_GRANTED ) { - val requestPermissionLauncher = act.registerForActivityResult( + val requestPermissionLauncher = componentActivity.registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> Log.d(TAG, "Notification permission: $isGranted") diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 17823f7c..5595c377 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -67,6 +67,7 @@ import com.lagradost.cloudstream3.CommonActivity.onColorSelectedEvent import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.onUserLeaveHint import com.lagradost.cloudstream3.CommonActivity.screenHeight +import com.lagradost.cloudstream3.CommonActivity.setActivityInstance import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.updateLocale import com.lagradost.cloudstream3.databinding.ActivityMainBinding @@ -590,6 +591,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { override fun onResume() { super.onResume() afterPluginsLoadedEvent += ::onAllPluginsLoaded + setActivityInstance(this) try { if (isCastApiAvailable()) { //mCastSession = mSessionManager.currentCastSession diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt index d181e175..4c3376bb 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/DownloadedPlayerActivity.kt @@ -110,4 +110,9 @@ class DownloadedPlayerActivity : AppCompatActivity() { return } } + + override fun onResume() { + super.onResume() + CommonActivity.setActivityInstance(this) + } } \ No newline at end of file From b120a7bce206a1c7f537dc686446be3f0255eac6 Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 10 Oct 2023 18:16:35 +0300 Subject: [PATCH 33/42] Library on TV (#663) * implementation for Library on TV --- .../cloudstream3/ExampleInstrumentedTest.kt | 4 + .../lagradost/cloudstream3/MainActivity.kt | 6 +- .../ui/library/LibraryFragment.kt | 98 +++++++-- .../ui/library/ViewpagerAdapter.kt | 5 +- app/src/main/res/layout/fragment_library.xml | 17 +- .../main/res/layout/fragment_library_tv.xml | 200 ++++++++++++++++++ .../res/layout/library_viewpager_page.xml | 2 + 7 files changed, 305 insertions(+), 27 deletions(-) create mode 100644 app/src/main/res/layout/fragment_library_tv.xml diff --git a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt index df41ef91..a84b2457 100644 --- a/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/lagradost/cloudstream3/ExampleInstrumentedTest.kt @@ -9,6 +9,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.databinding.FragmentHomeBinding import com.lagradost.cloudstream3.databinding.FragmentHomeTvBinding +import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding +import com.lagradost.cloudstream3.databinding.FragmentLibraryTvBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerBinding import com.lagradost.cloudstream3.databinding.FragmentPlayerTvBinding import com.lagradost.cloudstream3.databinding.FragmentResultBinding @@ -120,6 +122,8 @@ class ExampleInstrumentedTest { testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) testAllLayouts(activity, R.layout.homepage_parent_tv, R.layout.homepage_parent) + testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library) + testAllLayouts(activity, R.layout.fragment_library_tv, R.layout.fragment_library) } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 5595c377..f9fff88c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -543,9 +543,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { navRailView.isVisible = isNavVisible && landscape // Hide library on TV since it is not supported yet :( - val isTrueTv = isTrueTvSettings() - navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv - navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + //val isTrueTv = isTrueTvSettings() + //navView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv + //navRailView.menu.findItem(R.id.navigation_library)?.isVisible = !isTrueTv // Hide downloads on TV //navView.menu.findItem(R.id.navigation_downloads)?.isVisible = !isTrueTv 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 a3cc16c9..85f0aedd 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 @@ -1,38 +1,52 @@ package com.lagradost.cloudstream3.ui.library +import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.res.Configuration import android.os.Bundle import android.os.Handler import android.os.Looper +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewGroup.FOCUS_AFTER_DESCENDANTS +import android.view.ViewGroup.FOCUS_BLOCK_DESCENDANTS import android.view.animation.AlphaAnimation +import android.widget.LinearLayout +import android.widget.TextView +import android.widget.Toast import androidx.annotation.StringRes import androidx.appcompat.widget.SearchView +import androidx.core.view.allViews import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.viewpager2.widget.ViewPager2 +import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.allProviders import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.openBrowser import com.lagradost.cloudstream3.AcraApplication.Companion.setKey +import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.FragmentLibraryBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugAssert +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.syncproviders.SyncAPI import com.lagradost.cloudstream3.syncproviders.SyncIdName +import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA +import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity @@ -80,9 +94,21 @@ class LibraryFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val localBinding = FragmentLibraryBinding.inflate(inflater, container, false) - binding = localBinding - return localBinding.root + val layout = + if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library + val root = inflater.inflate(layout, container, false) + binding = try { + FragmentLibraryBinding.bind(root) + } catch (t: Throwable) { + CommonActivity.showToast( + txt(R.string.unable_to_inflate, t.message ?: ""), + Toast.LENGTH_LONG + ) + logError(t) + null + } + + return root //return inflater.inflate(R.layout.fragment_library, container, false) } @@ -99,24 +125,16 @@ class LibraryFragment : Fragment() { super.onSaveInstanceState(outState) } + @SuppressLint("ResourceType", "CutPasteId") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) fixPaddingStatusbar(binding?.searchStatusBarPadding) - binding?.sortFab?.setOnClickListener { - val methods = libraryViewModel.sortingMethods.map { - txt(it.stringRes).asString(view.context) - } + binding?.sortFab?.setOnClickListener(sortChangeClickListener) + binding?.librarySort?.setOnClickListener(sortChangeClickListener) - activity?.showBottomDialog(methods, - libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod), - txt(R.string.sort_by).asString(view.context), - false, - {}, - { - val method = libraryViewModel.sortingMethods[it] - libraryViewModel.sort(method) - }) + binding?.libraryRoot?.findViewById(R.id.search_src_text)?.apply { + tag = "tv_no_focus_tag" } binding?.mainSearch?.setOnQueryTextListener(object : SearchView.OnQueryTextListener { @@ -266,7 +284,10 @@ class LibraryFragment : Fragment() { // selects the whole list opener val savedListSelection = getKey("$currentAccount/$LIBRARY_FOLDER", syncName.name) - val savedSelection = getKey("$currentAccount/$LIBRARY_FOLDER", syncId).takeIf { + val savedSelection = getKey( + "$currentAccount/$LIBRARY_FOLDER", + syncId + ).takeIf { it?.openType != LibraryOpenerType.Default } ?: savedListSelection @@ -354,6 +375,12 @@ class LibraryFragment : Fragment() { } (viewpager.adapter as? ViewpagerAdapter)?.pages = pages + //fix focus on the viewpager itself + (viewpager.getChildAt(0) as RecyclerView).apply { + tag = "tv_no_focus_tag" + //isFocusable = false + } + // Using notifyItemRangeChanged keeps the animations when sorting viewpager.adapter?.notifyItemRangeChanged( 0, @@ -396,6 +423,9 @@ class LibraryFragment : Fragment() { viewpager, ) { tab, position -> tab.text = pages.getOrNull(position)?.title?.asStringNull(context) + tab.view.tag = "tv_no_focus_tag" + tab.view.nextFocusDownId = R.id.search_result_root + tab.view.setOnClickListener { val currentItem = binding?.viewpager?.currentItem ?: return@setOnClickListener @@ -418,17 +448,45 @@ class LibraryFragment : Fragment() { } } } - binding?.viewpager?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + binding?.viewpager?.registerOnPageChangeCallback(object : + ViewPager2.OnPageChangeCallback() { override fun onPageSelected(position: Int) { - libraryViewModel.currentPage = position + val all = binding?.viewpager?.allViews?.toList() + ?.filterIsInstance() + + all?.forEach { view -> + view.isVisible = view.tag == position + view.isFocusable = view.tag == position + + if (view.tag == position) + view.descendantFocusability = FOCUS_AFTER_DESCENDANTS + else + view.descendantFocusability = FOCUS_BLOCK_DESCENDANTS + } + super.onPageSelected(position) } }) } - override fun onConfigurationChanged(newConfig: Configuration) { (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() super.onConfigurationChanged(newConfig) } + + private val sortChangeClickListener = View.OnClickListener { view -> + val methods = libraryViewModel.sortingMethods.map { + txt(it.stringRes).asString(view.context) + } + + activity?.showBottomDialog(methods, + libraryViewModel.sortingMethods.indexOf(libraryViewModel.currentSortingMethod), + txt(R.string.sort_by).asString(view.context), + false, + {}, + { + val method = libraryViewModel.sortingMethods[it] + libraryViewModel.sort(method) + }) + } } class MenuSearchView(context: Context) : SearchView(context) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt index 95fefcbe..76028487 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/library/ViewpagerAdapter.kt @@ -25,7 +25,7 @@ class ViewpagerAdapter( override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is PageViewHolder -> { - holder.bind(pages[position], unbound.remove(position)) + holder.bind(pages[position], position, unbound.remove(position)) } } } @@ -43,7 +43,8 @@ class ViewpagerAdapter( inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : RecyclerView.ViewHolder(binding.root) { - fun bind(page: SyncAPI.Page, rebind: Boolean) { + fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) { + binding.pageRecyclerview.tag = position binding.pageRecyclerview.apply { spanCount = this@PageViewHolder.itemView.context.getSpanCount() ?: 3 diff --git a/app/src/main/res/layout/fragment_library.xml b/app/src/main/res/layout/fragment_library.xml index 985d055d..879ddbd9 100644 --- a/app/src/main/res/layout/fragment_library.xml +++ b/app/src/main/res/layout/fragment_library.xml @@ -41,6 +41,20 @@ android:src="@drawable/ic_baseline_extension_24" app:tint="?attr/textColor" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/library_viewpager_page.xml b/app/src/main/res/layout/library_viewpager_page.xml index 7d278cff..aa9745fb 100644 --- a/app/src/main/res/layout/library_viewpager_page.xml +++ b/app/src/main/res/layout/library_viewpager_page.xml @@ -5,5 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" + android:focusable="false" + android:tag="tv_no_focus_tag" tools:listitem="@layout/home_result_grid_expanded" /> From abbad1bc949588009bb1cf4c22405e33d790d09c Mon Sep 17 00:00:00 2001 From: KingLucius Date: Tue, 10 Oct 2023 18:17:18 +0300 Subject: [PATCH 34/42] Delete Focus frame from empty Downloads list & Search TV Layout (#675) * Delete Focus frame in search TV layout * Delete focus frame for empty Downloads list * Chip rounded stroke frame --- .../com/lagradost/cloudstream3/ui/search/SearchFragment.kt | 3 ++- app/src/main/res/layout/fragment_downloads.xml | 1 + app/src/main/res/layout/fragment_search_tv.xml | 2 ++ app/src/main/res/values/styles.xml | 5 +++-- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt index 0e994be8..ce92d723 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchFragment.kt @@ -11,6 +11,7 @@ import android.widget.AbsListView import android.widget.ArrayAdapter import android.widget.ImageView import android.widget.ListView +import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.view.isVisible @@ -221,7 +222,7 @@ class SearchFragment : Fragment() { SearchHelper.handleSearchClickCallback(callback) } - + searchRoot.findViewById(R.id.search_src_text)?.tag = "tv_no_focus_tag" searchAutofitResults.adapter = adapter searchLoadingBar.alpha = 0f } diff --git a/app/src/main/res/layout/fragment_downloads.xml b/app/src/main/res/layout/fragment_downloads.xml index 65f36209..5623bc7e 100644 --- a/app/src/main/res/layout/fragment_downloads.xml +++ b/app/src/main/res/layout/fragment_downloads.xml @@ -137,6 +137,7 @@ android:background="?attr/primaryBlackBackground" android:descendantFocusability="afterDescendants" android:nextFocusLeft="@id/nav_rail_view" + android:tag = "@string/tv_no_focus_tag" app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:listitem="@layout/download_header_episode" /> diff --git a/app/src/main/res/layout/fragment_search_tv.xml b/app/src/main/res/layout/fragment_search_tv.xml index b3f88cd2..e34b0ac3 100644 --- a/app/src/main/res/layout/fragment_search_tv.xml +++ b/app/src/main/res/layout/fragment_search_tv.xml @@ -85,6 +85,7 @@ android:nextFocusRight="@id/main_search" android:nextFocusUp="@id/nav_rail_view" android:nextFocusDown="@id/tvtypes_chips_scroll" + android:tag = "@string/tv_no_focus_tag" android:src="@drawable/ic_baseline_tune_24" app:tint="?attr/textColor" /> @@ -141,6 +142,7 @@ android:nextFocusLeft="@id/nav_rail_view" android:nextFocusUp="@id/tvtypes_chips" android:nextFocusDown="@id/search_clear_call_history" + android:tag = "@string/tv_no_focus_tag" android:paddingBottom="50dp" android:visibility="visible" tools:listitem="@layout/search_history_item" /> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5f7adea4..c00970d3 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -86,8 +86,8 @@ From 91b195241e1f6a07a037732376c7007e4cbc3f30 Mon Sep 17 00:00:00 2001 From: self-similarity <137652432+self-similarity@users.noreply.github.com> Date: Tue, 10 Oct 2023 15:19:27 +0000 Subject: [PATCH 35/42] Automatic backups (#592) Co-authored-by: Cloudburst <18114966+C10udburst@users.noreply.github.com> --- .../lagradost/cloudstream3/MainActivity.kt | 2 +- .../services/BackupWorkManager.kt | 96 +++++++++++++++++++ .../ui/settings/SettingsUpdates.kt | 37 ++++++- .../cloudstream3/utils/BackupUtils.kt | 52 +++++----- .../lagradost/cloudstream3/utils/UIHelper.kt | 2 +- app/src/main/res/values/array.xml | 22 ++++- app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/settings_updates.xml | 15 ++- 8 files changed, 193 insertions(+), 35 deletions(-) create mode 100644 app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index f9fff88c..51032a6e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1110,7 +1110,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { if (appVer != lastAppAutoBackup) { setKey("VERSION_NAME", BuildConfig.VERSION_NAME) normalSafeApiCall { - backup() + backup(this) } normalSafeApiCall { // Recompile oat on new version diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt new file mode 100644 index 00000000..6ed7a447 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/services/BackupWorkManager.kt @@ -0,0 +1,96 @@ +package com.lagradost.cloudstream3.services + +import android.content.Context +import androidx.core.app.NotificationCompat +import androidx.work.Constraints +import androidx.work.CoroutineWorker +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ForegroundInfo +import androidx.work.PeriodicWorkRequest +import androidx.work.WorkManager +import androidx.work.WorkerParameters +import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel +import com.lagradost.cloudstream3.utils.BackupUtils +import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute +import java.util.concurrent.TimeUnit + +const val BACKUP_CHANNEL_ID = "cloudstream3.backups" +const val BACKUP_WORK_NAME = "work_backup" +const val BACKUP_CHANNEL_NAME = "Backups" +const val BACKUP_CHANNEL_DESCRIPTION = "Notifications for background backups" +const val BACKUP_NOTIFICATION_ID = 938712898 // Random unique + +class BackupWorkManager(val context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + companion object { + fun enqueuePeriodicWork(context: Context?, intervalHours: Long) { + if (context == null) return + + if (intervalHours == 0L) { + WorkManager.getInstance(context).cancelUniqueWork(BACKUP_WORK_NAME) + return + } + + val constraints = Constraints.Builder() + .setRequiresStorageNotLow(true) + .build() + + val periodicSyncDataWork = + PeriodicWorkRequest.Builder( + BackupWorkManager::class.java, + intervalHours, + TimeUnit.HOURS + ) + .addTag(BACKUP_WORK_NAME) + .setConstraints(constraints) + .build() + + WorkManager.getInstance(context).enqueueUniquePeriodicWork( + BACKUP_WORK_NAME, + ExistingPeriodicWorkPolicy.UPDATE, + periodicSyncDataWork + ) + + // Uncomment below for testing + +// val oneTimeBackupWork = +// OneTimeWorkRequest.Builder(BackupWorkManager::class.java) +// .addTag(BACKUP_WORK_NAME) +// .setConstraints(constraints) +// .build() +// +// WorkManager.getInstance(context).enqueue(oneTimeBackupWork) + } + } + + private val backupNotificationBuilder = + NotificationCompat.Builder(context, BACKUP_CHANNEL_ID) + .setColorized(true) + .setOnlyAlertOnce(true) + .setSilent(true) + .setAutoCancel(true) + .setContentTitle(context.getString(R.string.pref_category_backup)) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setColor(context.colorFromAttribute(R.attr.colorPrimary)) + .setSmallIcon(R.drawable.ic_cloudstream_monochrome_big) + + override suspend fun doWork(): Result { + context.createNotificationChannel( + BACKUP_CHANNEL_ID, + BACKUP_CHANNEL_NAME, + BACKUP_CHANNEL_DESCRIPTION + ) + + setForeground( + ForegroundInfo( + BACKUP_NOTIFICATION_ID, + backupNotificationBuilder.build() + ) + ) + + BackupUtils.backup(context) + + return Result.success() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt index 62e46c08..2f796801 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUpdates.kt @@ -19,14 +19,16 @@ import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.databinding.LogcatBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.network.initClient +import com.lagradost.cloudstream3.services.BackupWorkManager import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.utils.BackupUtils.backup +import com.lagradost.cloudstream3.utils.BackupUtils import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog +import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.VideoDownloadManager @@ -48,7 +50,30 @@ class SettingsUpdates : PreferenceFragmentCompat() { //val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) getPref(R.string.backup_key)?.setOnPreferenceClickListener { - activity?.backup() + BackupUtils.backup(activity) + return@setOnPreferenceClickListener true + } + + getPref(R.string.automatic_backup_key)?.setOnPreferenceClickListener { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext()) + + val prefNames = resources.getStringArray(R.array.periodic_work_names) + val prefValues = resources.getIntArray(R.array.periodic_work_values) + val current = settingsManager.getInt(getString(R.string.automatic_backup_key), 0) + + activity?.showDialog( + prefNames.toList(), + prefValues.indexOf(current), + getString(R.string.backup_frequency), + true, + {}) { index -> + settingsManager.edit() + .putInt(getString(R.string.automatic_backup_key), prefValues[index]).apply() + BackupWorkManager.enqueuePeriodicWork( + context ?: AcraApplication.context, + prefValues[index].toLong() + ) + } return@setOnPreferenceClickListener true } @@ -65,7 +90,7 @@ class SettingsUpdates : PreferenceFragmentCompat() { val builder = AlertDialog.Builder(pref.context, R.style.AlertDialogCustom) - val binding = LogcatBinding.inflate(layoutInflater,null,false ) + val binding = LogcatBinding.inflate(layoutInflater, null, false) builder.setView(binding.root) val dialog = builder.create() @@ -176,7 +201,8 @@ class SettingsUpdates : PreferenceFragmentCompat() { val settingsManager = PreferenceManager.getDefaultSharedPreferences(it.context) val prefNames = resources.getStringArray(R.array.auto_download_plugin) - val prefValues = enumValues().sortedBy { x -> x.value }.map { x -> x.value } + val prefValues = + enumValues().sortedBy { x -> x.value }.map { x -> x.value } val current = settingsManager.getInt(getString(R.string.auto_download_plugins_key), 0) @@ -186,7 +212,8 @@ class SettingsUpdates : PreferenceFragmentCompat() { getString(R.string.automatic_plugin_download_mode_title), true, {}) { - settingsManager.edit().putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() + settingsManager.edit() + .putInt(getString(R.string.auto_download_plugins_key), prefValues[it]).apply() (context ?: AcraApplication.context)?.let { ctx -> app.initClient(ctx) } } return@setOnPreferenceClickListener true diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt index 96593769..e50131fe 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -10,6 +10,7 @@ import androidx.annotation.WorkerThread import androidx.fragment.app.FragmentActivity import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.module.kotlin.readValue +import com.lagradost.cloudstream3.AcraApplication.Companion.getActivity import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError @@ -90,9 +91,11 @@ object BackupUtils { ) @Suppress("UNCHECKED_CAST") - fun Context.getBackup(): BackupFile { - val allData = getSharedPrefs().all.filter { it.key.isTransferable() } - val allSettings = getDefaultSharedPrefs().all.filter { it.key.isTransferable() } + private fun getBackup(context: Context?): BackupFile? { + if (context == null) return null + + val allData = context.getSharedPrefs().all.filter { it.key.isTransferable() } + val allSettings = context.getDefaultSharedPrefs().all.filter { it.key.isTransferable() } val allDataSorted = BackupVars( allData.filter { it.value is Boolean } as? Map, @@ -119,46 +122,50 @@ object BackupUtils { } @WorkerThread - fun Context.restore( + fun restore( + context: Context?, backupFile: BackupFile, restoreSettings: Boolean, restoreDataStore: Boolean ) { + if (context == null) return if (restoreSettings) { - restoreMap(backupFile.settings._Bool, true) - restoreMap(backupFile.settings._Int, true) - restoreMap(backupFile.settings._String, true) - restoreMap(backupFile.settings._Float, true) - restoreMap(backupFile.settings._Long, true) - restoreMap(backupFile.settings._StringSet, true) + context.restoreMap(backupFile.settings._Bool, true) + context.restoreMap(backupFile.settings._Int, true) + context.restoreMap(backupFile.settings._String, true) + context.restoreMap(backupFile.settings._Float, true) + context.restoreMap(backupFile.settings._Long, true) + context.restoreMap(backupFile.settings._StringSet, true) } if (restoreDataStore) { - restoreMap(backupFile.datastore._Bool) - restoreMap(backupFile.datastore._Int) - restoreMap(backupFile.datastore._String) - restoreMap(backupFile.datastore._Float) - restoreMap(backupFile.datastore._Long) - restoreMap(backupFile.datastore._StringSet) + context.restoreMap(backupFile.datastore._Bool) + context.restoreMap(backupFile.datastore._Int) + context.restoreMap(backupFile.datastore._String) + context.restoreMap(backupFile.datastore._Float) + context.restoreMap(backupFile.datastore._Long) + context.restoreMap(backupFile.datastore._StringSet) } } @SuppressLint("SimpleDateFormat") - fun FragmentActivity.backup() = ioSafe { + fun backup(context: Context?) = ioSafe { + if (context == null) return@ioSafe + var fileStream: OutputStream? = null var printStream: PrintWriter? = null try { - if (!checkWrite()) { + if (!context.checkWrite()) { showToast(R.string.backup_failed, Toast.LENGTH_LONG) - requestRW() + context.getActivity()?.requestRW() return@ioSafe } val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis())) val ext = "txt" val displayName = "CS3_Backup_${date}" - val backupFile = getBackup() - val stream = setupStream(this@backup, displayName, null, ext, false) + val backupFile = getBackup(context) + val stream = setupStream(context, displayName, null, ext, false) fileStream = stream.openNew() printStream = PrintWriter(fileStream) @@ -198,7 +205,8 @@ object BackupUtils { val restoredValue = mapper.readValue(input) - activity.restore( + restore( + activity, restoredValue, restoreSettings = true, restoreDataStore = true diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt index 038a2f11..9b40e70e 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -71,7 +71,7 @@ object UIHelper { val Int.toDp: Int get() = (this / Resources.getSystem().displayMetrics.density).toInt() val Float.toDp: Float get() = (this / Resources.getSystem().displayMetrics.density) - fun Activity.checkWrite(): Boolean { + fun Context.checkWrite(): Boolean { return (ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_EXTERNAL_STORAGE diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 1df7b9d6..b8f0cbf8 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -41,7 +41,7 @@ 0 1 - + @string/disable @@ -126,6 +126,26 @@ 30min + + @string/none + 3h + 6h + 12h + 24h + 3d + 7d + + + + 0 + 3 + 6 + 12 + 24 + 72 + 168 + + 0 60 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 13251c7c..c722f33f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ primary_color_key restore_key backup_key + automatic_backup_key prefer_media_type_key_2 app_theme_key episode_sync_enabled_key @@ -229,6 +230,7 @@ Automatically sync your current episode progress Restore data from backup Back up data + Backup frequency Loaded backup file Failed to restore data from file %s Data stored diff --git a/app/src/main/res/xml/settings_updates.xml b/app/src/main/res/xml/settings_updates.xml index 9989e47b..e3b36648 100644 --- a/app/src/main/res/xml/settings_updates.xml +++ b/app/src/main/res/xml/settings_updates.xml @@ -9,7 +9,7 @@ android:summaryOn="@string/bug_report_settings_on" android:title="@string/pref_disable_acra" /> - - + + - - + - Date: Tue, 10 Oct 2023 18:05:31 +0200 Subject: [PATCH 36/42] reverted to instant outline --- .../lagradost/cloudstream3/MainActivity.kt | 55 +++++---- app/src/main/res/drawable/outline.xml | 4 +- .../main/res/drawable/outline_drawable.xml | 4 +- .../res/drawable/outline_drawable_less.xml | 2 +- app/src/main/res/layout/fragment_result.xml | 2 +- app/src/main/res/layout/result_episode.xml | 4 +- .../main/res/layout/result_episode_both.xml | 9 -- .../res/layout/result_episode_both_old.xml | 14 +++ .../res/layout/result_episode_both_tv.xml | 21 ---- .../res/layout/result_episode_both_tv_old.xml | 21 ++++ .../main/res/layout/result_episode_large.xml | 1 + .../res/layout/result_episode_large_tv.xml | 111 ----------------- .../layout/result_episode_large_tv_old.xml | 112 ++++++++++++++++++ app/src/main/res/layout/result_episode_tv.xml | 60 ---------- .../main/res/layout/result_episode_tv_old.xml | 59 +++++++++ app/src/main/res/values/styles.xml | 9 +- 16 files changed, 246 insertions(+), 242 deletions(-) delete mode 100644 app/src/main/res/layout/result_episode_both.xml create mode 100644 app/src/main/res/layout/result_episode_both_old.xml delete mode 100644 app/src/main/res/layout/result_episode_both_tv.xml create mode 100644 app/src/main/res/layout/result_episode_both_tv_old.xml delete mode 100644 app/src/main/res/layout/result_episode_large_tv.xml create mode 100644 app/src/main/res/layout/result_episode_large_tv_old.xml delete mode 100644 app/src/main/res/layout/result_episode_tv.xml create mode 100644 app/src/main/res/layout/result_episode_tv_old.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 51032a6e..4e0d93c9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -283,6 +283,7 @@ var app = Requests(responseParser = object : ResponseParser { class MainActivity : AppCompatActivity(), ColorPickerDialogListener { companion object { const val TAG = "MAINACT" + const val ANIMATED_OUTLINE : Boolean = false var lastError: String? = null /** @@ -1070,7 +1071,22 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { } } - + private fun centerView(view : View?) { + if(view == null) return + try { + Log.v(TAG, "centerView: $view") + val r = Rect(0, 0, 0, 0) + view.getDrawingRect(r) + val x = r.centerX() + val y = r.centerY() + val dx = r.width() / 2 //screenWidth / 2 + val dy = screenHeight / 2 + val r2 = Rect(x - dx, y - dy, x + dx, y + dy) + view.requestRectangleOnScreen(r2, false) + // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) + } catch (_: Throwable) { + } + } override fun onCreate(savedInstanceState: Bundle?) { app.initClient(this) val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) @@ -1125,43 +1141,26 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener { val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) setContentView(newLocalBinding.root) - if(isTrueTvSettings()) { + if(isTrueTvSettings() && ANIMATED_OUTLINE) { TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) + newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { + TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) + } newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> - // println("refocus $oldFocus -> $newFocus") - try { - val r = Rect(0, 0, 0, 0) - newFocus.getDrawingRect(r) - val x = r.centerX() - val y = r.centerY() - val dx = 0 //screenWidth / 2 - val dy = screenHeight / 2 - val r2 = Rect(x - dx, y - dy, x + dx, y + dy) - newFocus.requestRectangleOnScreen(r2, false) - // TvFocus.current =TvFocus.current.copy(y=y.toFloat()) - } catch (_: Throwable) { - } TvFocus.updateFocusView(newFocus) - /*var focus = newFocus - - while(focus != null) { - if(focus is ScrollingView && focus.canScrollVertically()) { - focus.scrollBy() - } - when(focus.parent) { - is View -> focus = newFocus - else -> break - } - }*/ } } else { newLocalBinding.focusOutline.isVisible = false } - newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { - TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) + if(isTrueTvSettings()) { + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> + centerView(newFocus) + } } + + ActivityMainBinding.bind(newLocalBinding.root) // this may crash } else { val newLocalBinding = ActivityMainBinding.inflate(layoutInflater, null, false) diff --git a/app/src/main/res/drawable/outline.xml b/app/src/main/res/drawable/outline.xml index 30077a98..7b436c7d 100644 --- a/app/src/main/res/drawable/outline.xml +++ b/app/src/main/res/drawable/outline.xml @@ -2,11 +2,9 @@ - - \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_drawable.xml b/app/src/main/res/drawable/outline_drawable.xml index 8eec2d0b..16eba83c 100644 --- a/app/src/main/res/drawable/outline_drawable.xml +++ b/app/src/main/res/drawable/outline_drawable.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/outline_drawable_less.xml b/app/src/main/res/drawable/outline_drawable_less.xml index db74a092..aa3a8d0d 100644 --- a/app/src/main/res/drawable/outline_drawable_less.xml +++ b/app/src/main/res/drawable/outline_drawable_less.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 87de7186..9d748c5a 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -875,7 +875,7 @@ android:descendantFocusability="afterDescendants" android:paddingBottom="100dp" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" - tools:listitem="@layout/result_episode_both_tv" /> + tools:listitem="@layout/result_episode" /> diff --git a/app/src/main/res/layout/result_episode.xml b/app/src/main/res/layout/result_episode.xml index 80ff4bec..b56cdb1d 100644 --- a/app/src/main/res/layout/result_episode.xml +++ b/app/src/main/res/layout/result_episode.xml @@ -11,7 +11,9 @@ android:nextFocusRight="@id/download_button" app:cardBackgroundColor="@color/transparent" app:cardCornerRadius="@dimen/rounded_image_radius" - app:cardElevation="0dp"> + app:cardElevation="0dp" + android:foreground="@drawable/outline_drawable" + > diff --git a/app/src/main/res/layout/result_episode_both.xml b/app/src/main/res/layout/result_episode_both.xml deleted file mode 100644 index 61102e84..00000000 --- a/app/src/main/res/layout/result_episode_both.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_old.xml b/app/src/main/res/layout/result_episode_both_old.xml new file mode 100644 index 00000000..6472ecc1 --- /dev/null +++ b/app/src/main/res/layout/result_episode_both_old.xml @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_tv.xml b/app/src/main/res/layout/result_episode_both_tv.xml deleted file mode 100644 index 13888b7e..00000000 --- a/app/src/main/res/layout/result_episode_both_tv.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_both_tv_old.xml b/app/src/main/res/layout/result_episode_both_tv_old.xml new file mode 100644 index 00000000..f273a118 --- /dev/null +++ b/app/src/main/res/layout/result_episode_both_tv_old.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_large.xml b/app/src/main/res/layout/result_episode_large.xml index 75292965..76e8c434 100644 --- a/app/src/main/res/layout/result_episode_large.xml +++ b/app/src/main/res/layout/result_episode_large.xml @@ -8,6 +8,7 @@ android:layout_height="wrap_content" android:layout_marginBottom="10dp" + android:foreground="@drawable/outline_drawable" android:nextFocusRight="@id/download_button" app:cardBackgroundColor="?attr/boxItemBackground" diff --git a/app/src/main/res/layout/result_episode_large_tv.xml b/app/src/main/res/layout/result_episode_large_tv.xml deleted file mode 100644 index 5a9dee30..00000000 --- a/app/src/main/res/layout/result_episode_large_tv.xml +++ /dev/null @@ -1,111 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_large_tv_old.xml b/app/src/main/res/layout/result_episode_large_tv_old.xml new file mode 100644 index 00000000..3a7cef3c --- /dev/null +++ b/app/src/main/res/layout/result_episode_large_tv_old.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_tv.xml b/app/src/main/res/layout/result_episode_tv.xml deleted file mode 100644 index 53590b6b..00000000 --- a/app/src/main/res/layout/result_episode_tv.xml +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/result_episode_tv_old.xml b/app/src/main/res/layout/result_episode_tv_old.xml new file mode 100644 index 00000000..62546cf9 --- /dev/null +++ b/app/src/main/res/layout/result_episode_tv_old.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c00970d3..c047c749 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -86,8 +86,8 @@ + + + + + +