diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 31e225de..c2ba2907 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -60,7 +60,7 @@ android { targetSdk = 33 /* Android 14 is Fu*ked ^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/ versionCode = 63 - versionName = "4.3.1" + versionName = "4.3.2" resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") @@ -163,15 +163,15 @@ dependencies { // Android Core & Lifecycle implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.appcompat:appcompat:1.6.1") - implementation("androidx.navigation:navigation-ui-ktx:2.7.6") + implementation("androidx.navigation:navigation-ui-ktx:2.7.7") implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") - implementation("androidx.navigation:navigation-fragment-ktx:2.7.6") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.7") // Design & UI implementation("jp.wasabeef:glide-transformations:4.3.0") implementation("androidx.preference:preference-ktx:1.2.1") - implementation("com.google.android.material:material:1.10.0") + implementation("com.google.android.material:material:1.11.0") implementation("androidx.constraintlayout:constraintlayout:2.1.4") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") diff --git a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt index c93f0f9b..1680d698 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/AcraApplication.kt @@ -11,7 +11,9 @@ import androidx.fragment.app.FragmentActivity import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.plugins.PluginManager -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.DataStore.getKey @@ -31,7 +33,6 @@ import org.acra.sender.ReportSenderFactory import java.io.File import java.io.FileNotFoundException import java.io.PrintStream -import java.lang.Exception import java.lang.ref.WeakReference import kotlin.concurrent.thread import kotlin.system.exitProcess @@ -211,7 +212,7 @@ class AcraApplication : Application() { fun openBrowser(url: String, activity: FragmentActivity?) { openBrowser( url, - isTvSettings(), + isLayout(TV or EMULATOR), activity?.supportFragmentManager?.fragments?.lastOrNull() ) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt index 80de223e..4dc78dc7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/CommonActivity.kt @@ -11,11 +11,9 @@ import android.util.DisplayMetrics import android.util.Log import android.view.Gravity import android.view.KeyEvent -import android.view.LayoutInflater import android.view.View import android.view.View.NO_ID import android.view.ViewGroup -import android.widget.TextView import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts @@ -31,11 +29,12 @@ import com.google.android.material.chip.ChipGroup import com.google.android.material.navigationrail.NavigationRailView import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey +import com.lagradost.cloudstream3.databinding.ToastBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.UiText -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv +import com.lagradost.cloudstream3.ui.settings.Globals.updateTv import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.Event @@ -99,8 +98,7 @@ object CommonActivity { var playerEventListener: ((PlayerEventType) -> Unit)? = null var keyEventListener: ((Pair) -> Boolean)? = null - - var currentToast: Toast? = null + private var currentToast: Toast? = null fun showToast(@StringRes message: Int, duration: Int? = null) { val act = activity ?: return @@ -156,25 +154,19 @@ object CommonActivity { } catch (e: Exception) { logError(e) } + try { - val inflater = - act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater - - val layout: View = inflater.inflate( - R.layout.toast, - act.findViewById(R.id.toast_layout_root) as ViewGroup? - ) - - val text = layout.findViewById(R.id.text) as TextView - text.text = message.trim() + val binding = ToastBinding.inflate(act.layoutInflater) + binding.text.text = message.trim() + // custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11) val toast = Toast(act) - toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) toast.duration = duration ?: Toast.LENGTH_SHORT - toast.view = layout - //https://github.com/PureWriter/ToastCompat - toast.show() + toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx) + toast.view = binding.root currentToast = toast + toast.show() + } catch (e: Exception) { logError(e) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 7a25b738..273e267b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -18,6 +18,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklAp import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.syncproviders.providers.SimklApi import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.ui.result.ResultViewModel2 import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.Coroutines.mainWork @@ -119,7 +120,8 @@ object APIHolder { } fun LoadResponse.getId(): Int { - return getLoadResponseIdFromUrl(url, apiName) + // this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked + return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) ?: getLoadResponseIdFromUrl(url, apiName) } /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 48798ce6..5a7e72ef 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -86,6 +86,7 @@ import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver +import com.lagradost.cloudstream3.services.SubscriptionWorkManager import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.OAuth2Apis import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString @@ -112,11 +113,11 @@ import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchResultBuilder -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.ui.settings.Globals.updateTv import com.lagradost.cloudstream3.ui.settings.SettingsGeneral import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions @@ -290,7 +291,8 @@ var app = Requests(responseParser = object : ResponseParser { defaultHeaders = mapOf("user-agent" to USER_AGENT) } -class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthenticator.BiometricAuthCallback { +class MainActivity : AppCompatActivity(), ColorPickerDialogListener, + BiometricAuthenticator.BiometricAuthCallback { companion object { const val TAG = "MAINACT" const val ANIMATED_OUTLINE: Boolean = false @@ -336,10 +338,12 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu // kinda shitty solution, but cant com main->home otherwise for popups val bookmarksUpdatedEvent = Event() + /** * Used by DataStoreHelper to fully reload home when switching accounts */ val reloadHomeEvent = Event() + /** * Used by DataStoreHelper to fully reload library when switching accounts */ @@ -467,7 +471,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } var lastPopup: SearchResponse? = null - fun loadPopup(result: SearchResponse, load : Boolean = true) { + fun loadPopup(result: SearchResponse, load: Boolean = true) { lastPopup = result val syncName = syncViewModel.syncName(result.apiName) @@ -488,8 +492,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu .contains(DubStatus.Dubbed) ) DubStatus.Dubbed else DubStatus.Subbed, null ) - }else { - viewModel.loadSmall(this,result) + } else { + viewModel.loadSmall(this, result) } } @@ -554,7 +558,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu binding?.navHostFragment?.apply { val params = layoutParams as ConstraintLayout.LayoutParams val push = - if (!dontPush && isTvSettings()) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0 + if (!dontPush && isLayout(TV or EMULATOR)) resources.getDimensionPixelSize(R.dimen.navbar_width) else 0 if (!this.isLtr()) { params.setMargins( @@ -581,7 +585,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } Configuration.ORIENTATION_PORTRAIT -> { - isTvSettings() + isLayout(TV or EMULATOR) } else -> { @@ -787,9 +791,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } lateinit var viewModel: ResultViewModel2 - lateinit var syncViewModel : SyncViewModel + lateinit var syncViewModel: SyncViewModel + /** kinda dirty, however it signals that we should use the watch status as sync or not*/ - var isLocalList : Boolean = false + var isLocalList: Boolean = false override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? { viewModel = ViewModelProvider(this)[ResultViewModel2::class.java] @@ -1105,8 +1110,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } - private fun centerView(view : View?) { - if(view == null) return + private fun centerView(view: View?) { + if (view == null) return try { Log.v(TAG, "centerView: $view") val r = Rect(0, 0, 0, 0) @@ -1172,11 +1177,11 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH binding = try { - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) setContentView(newLocalBinding.root) - if (isTrueTvSettings() && ANIMATED_OUTLINE) { + if (isLayout(TV) && ANIMATED_OUTLINE) { TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) @@ -1188,7 +1193,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu newLocalBinding.focusOutline.isVisible = false } - if(isTrueTvSettings()) { + if (isLayout(TV)) { // Put here any button you don't want focusing it to center the view val exceptionButtons = listOf( R.id.home_preview_play_btt, @@ -1205,7 +1210,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu R.id.result_search_Button, R.id.result_episodes_show_button, ) - + newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener centerView(newFocus) @@ -1223,18 +1228,21 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu null } - changeStatusBarState(isEmulatorSettings()) + changeStatusBarState(isLayout(EMULATOR)) /** Biometric stuff for users without accounts **/ val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) - val noAccounts = settingsManager.getBoolean(getString(R.string.skip_startup_account_select_key), false) || accounts.count() <= 1 + val noAccounts = settingsManager.getBoolean( + getString(R.string.skip_startup_account_select_key), + false + ) || accounts.count() <= 1 - if (isTruePhone() && authEnabled && noAccounts) { + if (isLayout(PHONE) && authEnabled && noAccounts) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication(this, R.string.biometric_authentication_title, false) - BiometricAuthenticator.promptInfo?.let { - BiometricAuthenticator.biometricPrompt?.authenticate(it) + BiometricAuthenticator.promptInfo?.let { promt -> + BiometricAuthenticator.biometricPrompt?.authenticate(promt) } // hide background while authenticating, Sorry moms & dads 🙏 @@ -1326,7 +1334,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } - fun setUserData(status : Resource?) { + fun setUserData(status: Resource?) { if (isLocalList) return bottomPreviewBinding?.apply { when (status) { @@ -1351,7 +1359,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } - fun setWatchStatus(state : WatchType?) { + fun setWatchStatus(state: WatchType?) { if (!isLocalList || state == null) return bottomPreviewBinding?.resultviewPreviewBookmark?.apply { @@ -1360,13 +1368,42 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } - observe(viewModel.watchStatus) { state -> - setWatchStatus(state) - } - observe(syncViewModel.userData) { status -> - setUserData(status) + fun setSubscribeStatus(state: Boolean?) { + bottomPreviewBinding?.resultviewPreviewSubscribe?.apply { + if (state != null) { + val drawable = if (state) { + R.drawable.ic_baseline_notifications_active_24 + } else { + R.drawable.baseline_notifications_none_24 + } + setImageResource(drawable) + } + isVisible = state != null + + setOnClickListener { + viewModel.toggleSubscriptionStatus(context) { newStatus: Boolean? -> + if (newStatus == null) return@toggleSubscriptionStatus + + val message = if (newStatus) { + // Kinda icky to have this here, but it works. + SubscriptionWorkManager.enqueuePeriodicWork(context) + R.string.subscription_new + } else { + R.string.subscription_deleted + } + + val name = (viewModel.page.value as? Resource.Success)?.value?.title + ?: txt(R.string.no_data).asStringNull(context) ?: "" + showToast(txt(message, name), Toast.LENGTH_SHORT) + } + } + } } + observe(viewModel.watchStatus,::setWatchStatus) + observe(syncViewModel.userData, ::setUserData) + observeNullable(viewModel.subscribeStatus, ::setSubscribeStatus) + observeNullable(viewModel.page) { resource -> if (resource == null) { hidePreviewPopupDialog() @@ -1408,6 +1445,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu setUserData(syncViewModel.userData.value) setWatchStatus(viewModel.watchStatus.value) + setSubscribeStatus(viewModel.subscribeStatus.value) resultviewPreviewBookmark.setOnClickListener { //viewModel.updateWatchStatus(WatchType.PLANTOWATCH) @@ -1426,7 +1464,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu ) } } else { - val value = (syncViewModel.userData.value as? Resource.Success)?.value?.status ?: SyncWatchType.NONE + val value = + (syncViewModel.userData.value as? Resource.Success)?.value?.status + ?: SyncWatchType.NONE this@MainActivity.showBottomDialog( SyncWatchType.values().map { getString(it.stringRes) }.toList(), @@ -1453,7 +1493,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu resultviewPreviewFavorite.setImageResource(drawable) } - resultviewPreviewFavorite.setOnClickListener{ + resultviewPreviewFavorite.setOnClickListener { viewModel.toggleFavoriteStatus(this@MainActivity) { newStatus: Boolean? -> if (newStatus == null) return@toggleFavoriteStatus @@ -1469,7 +1509,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } - if (!isTvSettings()) // dont want this clickable on tv layout + if (isLayout(PHONE)) // dont want this clickable on tv layout resultviewPreviewDescription.setOnClickListener { view -> view.context?.let { ctx -> val builder: AlertDialog.Builder = @@ -1544,7 +1584,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu } } - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { if (navDestination.matchDestination(R.id.navigation_home)) { attachBackPressedCallback() } else detachBackPressedCallback() @@ -1580,7 +1620,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu itemRippleColor = rippleColor itemActiveIndicatorColor = rippleColor setupWithNavController(navController) - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { background?.alpha = 200 } else { background?.alpha = 255 diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt index adf5abfa..e2bcd6e1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt @@ -1,5 +1,6 @@ package com.lagradost.cloudstream3.services +import android.annotation.SuppressLint import android.app.NotificationManager import android.app.PendingIntent import android.content.Context @@ -12,7 +13,7 @@ import com.lagradost.cloudstream3.* import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.R -import com.lagradost.cloudstream3.mvvm.safeApiCall +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel @@ -97,128 +98,138 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete ) } + @SuppressLint("UnspecifiedImmutableFlag") override suspend fun doWork(): Result { + try { // println("Update subscriptions!") - context.createNotificationChannel( - SUBSCRIPTION_CHANNEL_ID, - SUBSCRIPTION_CHANNEL_NAME, - SUBSCRIPTION_CHANNEL_DESCRIPTION - ) - - setForeground( - ForegroundInfo( - SUBSCRIPTION_NOTIFICATION_ID, - progressNotificationBuilder.build() + context.createNotificationChannel( + SUBSCRIPTION_CHANNEL_ID, + SUBSCRIPTION_CHANNEL_NAME, + SUBSCRIPTION_CHANNEL_DESCRIPTION ) - ) - val subscriptions = getAllSubscriptions() + setForeground( + ForegroundInfo( + SUBSCRIPTION_NOTIFICATION_ID, + progressNotificationBuilder.build() + ) + ) - if (subscriptions.isEmpty()) { - WorkManager.getInstance(context).cancelWorkById(this.id) + val subscriptions = getAllSubscriptions() + + if (subscriptions.isEmpty()) { + WorkManager.getInstance(context).cancelWorkById(this.id) + return Result.success() + } + + val max = subscriptions.size + var progress = 0 + + updateProgress(max, progress, true) + + // We need all plugins loaded. + PluginManager.loadAllOnlinePlugins(context) + PluginManager.loadAllLocalPlugins(context, false) + + subscriptions.apmap { savedData -> + try { + val id = savedData.id ?: return@apmap null + val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null + + // Reasonable timeout to prevent having this worker run forever. + val response = withTimeoutOrNull(60_000) { + api.load(savedData.url) as? EpisodeResponse + } ?: return@apmap null + + val dubPreference = + getDub(id) ?: if ( + context.getApiDubstatusSettings().contains(DubStatus.Dubbed) + ) { + DubStatus.Dubbed + } else { + DubStatus.Subbed + } + + val latestEpisodes = response.getLatestEpisodes() + val latestPreferredEpisode = latestEpisodes[dubPreference] + + val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) { + val latestSeenEpisode = + savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE + val shouldUpdate = latestPreferredEpisode > latestSeenEpisode + shouldUpdate to latestPreferredEpisode + } else { + val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE + val latestSeenEpisode = + savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE + val shouldUpdate = latestEpisode > latestSeenEpisode + shouldUpdate to latestEpisode + } + + DataStoreHelper.updateSubscribedData( + id, + savedData, + response + ) + + if (shouldUpdate) { + val updateHeader = savedData.name + val updateDescription = txt( + R.string.subscription_episode_released, + latestEpisode, + savedData.name + ).asString(context) + + val intent = Intent(context, MainActivity::class.java).apply { + data = savedData.url.toUri() + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + + val pendingIntent = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.getActivity( + context, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE + ) + } else { + PendingIntent.getActivity(context, 0, intent, 0) + } + + val poster = ioWork { + savedData.posterUrl?.let { url -> + context.getImageBitmapFromUrl( + url, + savedData.posterHeaders + ) + } + } + + val updateNotification = + updateNotificationBuilder.setContentTitle(updateHeader) + .setContentText(updateDescription) + .setContentIntent(pendingIntent) + .setLargeIcon(poster) + .build() + + notificationManager.notify(id, updateNotification) + } + + // You can probably get some issues here since this is async but it does not matter much. + updateProgress(max, ++progress, false) + } catch (t: Throwable) { + logError(t) + } + } + + return Result.success() + } catch (t: Throwable) { + logError(t) + // ye, while this is not correct, but because gods know why android just crashes + // and this causes major battery usage as it retries it inf times. This is better, just + // in case android decides to be android and fuck us return Result.success() } - - val max = subscriptions.size - var progress = 0 - - updateProgress(max, progress, true) - - // We need all plugins loaded. - PluginManager.loadAllOnlinePlugins(context) - PluginManager.loadAllLocalPlugins(context, false) - - subscriptions.apmap { savedData -> - try { - val id = savedData.id ?: return@apmap null - val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null - - // Reasonable timeout to prevent having this worker run forever. - val response = withTimeoutOrNull(60_000) { - api.load(savedData.url) as? EpisodeResponse - } ?: return@apmap null - - val dubPreference = - getDub(id) ?: if ( - context.getApiDubstatusSettings().contains(DubStatus.Dubbed) - ) { - DubStatus.Dubbed - } else { - DubStatus.Subbed - } - - val latestEpisodes = response.getLatestEpisodes() - val latestPreferredEpisode = latestEpisodes[dubPreference] - - val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) { - val latestSeenEpisode = - savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE - val shouldUpdate = latestPreferredEpisode > latestSeenEpisode - shouldUpdate to latestPreferredEpisode - } else { - val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE - val latestSeenEpisode = - savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE - val shouldUpdate = latestEpisode > latestSeenEpisode - shouldUpdate to latestEpisode - } - - DataStoreHelper.updateSubscribedData( - id, - savedData, - response - ) - - if (shouldUpdate) { - val updateHeader = savedData.name - val updateDescription = txt( - R.string.subscription_episode_released, - latestEpisode, - savedData.name - ).asString(context) - - val intent = Intent(context, MainActivity::class.java).apply { - data = savedData.url.toUri() - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - - val pendingIntent = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - PendingIntent.getActivity( - context, - 0, - intent, - PendingIntent.FLAG_IMMUTABLE - ) - } else { - PendingIntent.getActivity(context, 0, intent, 0) - } - - val poster = ioWork { - savedData.posterUrl?.let { url -> - context.getImageBitmapFromUrl( - url, - savedData.posterHeaders - ) - } - } - - val updateNotification = - updateNotificationBuilder.setContentTitle(updateHeader) - .setContentText(updateDescription) - .setContentIntent(pendingIntent) - .setLargeIcon(poster) - .build() - - notificationManager.notify(id, updateNotification) - } - - // You can probably get some issues here since this is async but it does not matter much. - updateProgress(max, ++progress, false) - } catch (_: Throwable) { - } - } - - return Result.success() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt index 99723e90..7552fe9d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/syncproviders/providers/LocalList.kt @@ -8,7 +8,8 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.Coroutines.ioWork import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions @@ -71,9 +72,9 @@ class LocalList : SyncAPI { }?.distinctBy { it.first } ?: return null val list = ioWork { - val isTrueTv = isTrueTvSettings() + val isTrueTv = isLayout(TV) - val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate { + val baseMap = WatchType.entries.filter { it != WatchType.NONE }.associate { // None is not something to display it.stringRes to emptyList() } + mapOf( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt new file mode 100644 index 00000000..7439bfdf --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt @@ -0,0 +1,250 @@ +package com.lagradost.cloudstream3.ui + +import android.view.View +import android.view.ViewGroup +import androidx.core.view.children +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModel +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import androidx.viewbinding.ViewBinding +import java.util.concurrent.CopyOnWriteArrayList + +open class ViewHolderState(val view: ViewBinding) : ViewHolder(view.root) { + open fun save(): T? = null + open fun restore(state: T) = Unit + open fun onViewAttachedToWindow() = Unit + open fun onViewDetachedFromWindow() = Unit + open fun onViewRecycled() = Unit +} + + +// Based of the concept https://github.com/brahmkshatriya/echo/blob/main/app%2Fsrc%2Fmain%2Fjava%2Fdev%2Fbrahmkshatriya%2Fecho%2Fui%2Fadapters%2FMediaItemsContainerAdapter.kt#L108-L154 +class StateViewModel : ViewModel() { + val layoutManagerStates = hashMapOf>() +} + +abstract class NoStateAdapter(fragment: Fragment) : BaseAdapter(fragment, 0) + +/** + * BaseAdapter is a persistent state stored adapter that supports headers and footers. + * This should be used for restoring eg scroll or focus related to a view when it is recreated. + * + * Id is a per fragment based unique id used to store the underlying data done in an internal ViewModel. + * + * diffCallback is how the view should be handled when updating, override onUpdateContent for updates + * + * NOTE: + * + * By default it should save automatically, but you can also call save(recycle) + * + * By default no state is stored, but doing an id != 0 will store + * + * By default no headers or footers exist, override footers and headers count + */ +abstract class BaseAdapter< + T : Any, + S : Any>( + fragment: Fragment, + val id: Int = 0, + diffCallback: DiffUtil.ItemCallback = BaseDiffCallback() +) : RecyclerView.Adapter>() { + open val footers: Int = 0 + open val headers: Int = 0 + + fun getItem(position: Int): T { + return mDiffer.currentList[position] + } + + fun getItemOrNull(position: Int): T? { + return mDiffer.currentList.getOrNull(position) + } + + private val mDiffer: AsyncListDiffer = AsyncListDiffer( + object : NonFinalAdapterListUpdateCallback(this) { + override fun onMoved(fromPosition: Int, toPosition: Int) { + super.onMoved(fromPosition + headers, toPosition + headers) + } + + override fun onRemoved(position: Int, count: Int) { + super.onRemoved(position + headers, count) + } + + override fun onChanged(position: Int, count: Int, payload: Any?) { + super.onChanged(position + headers, count, payload) + } + + override fun onInserted(position: Int, count: Int) { + super.onInserted(position + headers, count) + } + }, + AsyncDifferConfig.Builder(diffCallback).build() + ) + + fun submitList(list: List?) { + // deep copy at least the top list, because otherwise adapter can go crazy + mDiffer.submitList(list?.let { CopyOnWriteArrayList(it) }) + } + + override fun getItemCount(): Int { + return mDiffer.currentList.size + footers + headers + } + + open fun onUpdateContent(holder: ViewHolderState, item: T, position: Int) = + onBindContent(holder, item, position) + + open fun onBindContent(holder: ViewHolderState, item: T, position: Int) = Unit + open fun onBindFooter(holder: ViewHolderState) = Unit + open fun onBindHeader(holder: ViewHolderState) = Unit + open fun onCreateContent(parent: ViewGroup): ViewHolderState = throw NotImplementedError() + open fun onCreateFooter(parent: ViewGroup): ViewHolderState = throw NotImplementedError() + open fun onCreateHeader(parent: ViewGroup): ViewHolderState = throw NotImplementedError() + + override fun onViewAttachedToWindow(holder: ViewHolderState) { + holder.onViewAttachedToWindow() + } + + override fun onViewDetachedFromWindow(holder: ViewHolderState) { + holder.onViewDetachedFromWindow() + } + + fun save(recyclerView: RecyclerView) { + for (child in recyclerView.children) { + val holder = + recyclerView.findContainingViewHolder(child) as? ViewHolderState ?: continue + setState(holder) + } + } + + fun clear() { + stateViewModel.layoutManagerStates[id]?.clear() + } + + private fun getState(holder: ViewHolderState): S? = + stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S + + private fun setState(holder: ViewHolderState) { + if(id == 0) return + + if (!stateViewModel.layoutManagerStates.contains(id)) { + stateViewModel.layoutManagerStates[id] = HashMap() + } + stateViewModel.layoutManagerStates[id]?.let { map -> + map[holder.absoluteAdapterPosition] = holder.save() + } + } + + private val attachListener = object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) = Unit + override fun onViewDetachedFromWindow(v: View) { + if (v !is RecyclerView) return + save(v) + } + } + + final override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { + recyclerView.addOnAttachStateChangeListener(attachListener) + super.onAttachedToRecyclerView(recyclerView) + } + + final override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + recyclerView.removeOnAttachStateChangeListener(attachListener) + super.onDetachedFromRecyclerView(recyclerView) + } + + final override fun getItemViewType(position: Int): Int { + if (position < headers) { + return HEADER + } + if (position - headers >= mDiffer.currentList.size) { + return FOOTER + } + + return CONTENT + } + + private val stateViewModel: StateViewModel by fragment.viewModels() + + final override fun onViewRecycled(holder: ViewHolderState) { + setState(holder) + holder.onViewRecycled() + super.onViewRecycled(holder) + } + + final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderState { + return when (viewType) { + CONTENT -> onCreateContent(parent) + HEADER -> onCreateHeader(parent) + FOOTER -> onCreateFooter(parent) + else -> throw NotImplementedError() + } + } + + // https://medium.com/@domen.lanisnik/efficiently-updating-recyclerview-items-using-payloads-1305f65f3068 + override fun onBindViewHolder( + holder: ViewHolderState, + position: Int, + payloads: MutableList + ) { + if (payloads.isEmpty()) { + super.onBindViewHolder(holder, position, payloads) + return + } + when (getItemViewType(position)) { + CONTENT -> { + val realPosition = position - headers + val item = getItem(realPosition) + onUpdateContent(holder, item, realPosition) + } + + FOOTER -> { + onBindFooter(holder) + } + + HEADER -> { + onBindHeader(holder) + } + } + } + + final override fun onBindViewHolder(holder: ViewHolderState, position: Int) { + when (getItemViewType(position)) { + CONTENT -> { + val realPosition = position - headers + val item = getItem(realPosition) + onBindContent(holder, item, realPosition) + } + + FOOTER -> { + onBindFooter(holder) + } + + HEADER -> { + onBindHeader(holder) + } + } + + getState(holder)?.let { state -> + holder.restore(state) + } + } + + companion object { + private const val HEADER: Int = 1 + private const val FOOTER: Int = 2 + private const val CONTENT: Int = 0 + } +} + +class BaseDiffCallback( + val itemSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() }, + val contentSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() } +) : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: T, newItem: T): Boolean = itemSame(oldItem, newItem) + override fun areContentsTheSame(oldItem: T, newItem: T): Boolean = contentSame(oldItem, newItem) + override fun getChangePayload(oldItem: T, newItem: T): Any = Any() +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt new file mode 100644 index 00000000..f721401e --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/NonFinalAdapterListUpdateCallback.kt @@ -0,0 +1,39 @@ +package com.lagradost.cloudstream3.ui + +import android.annotation.SuppressLint +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListUpdateCallback +import androidx.recyclerview.widget.RecyclerView + + +/** + * ListUpdateCallback that dispatches update events to the given adapter. + * + * @see DiffUtil.DiffResult.dispatchUpdatesTo + */ +open class NonFinalAdapterListUpdateCallback +/** + * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter. + * + * @param adapter The Adapter to send updates to. + */(private var mAdapter: RecyclerView.Adapter<*>) : + ListUpdateCallback { + + override fun onInserted(position: Int, count: Int) { + mAdapter.notifyItemRangeInserted(position, count) + } + + override fun onRemoved(position: Int, count: Int) { + mAdapter.notifyItemRangeRemoved(position, count) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + mAdapter.notifyItemMoved(fromPosition, toPosition) + } + + @SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly + override fun onChanged(position: Int, count: Int, payload: Any?) { + mAdapter.notifyItemRangeChanged(position, count, payload) + } +} + diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt index 60260edf..de0b5c05 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountAdapter.kt @@ -12,7 +12,9 @@ import com.lagradost.cloudstream3.databinding.AccountListItemBinding import com.lagradost.cloudstream3.databinding.AccountListItemEditBinding import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountEditDialog import com.lagradost.cloudstream3.ui.result.setImage -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -38,7 +40,7 @@ class AccountAdapter( is AccountListItemBinding -> binding.apply { if (account == null) return@apply - val isTv = isTvSettings() || !root.isInTouchMode + val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex @@ -80,7 +82,7 @@ class AccountAdapter( is AccountListItemEditBinding -> binding.apply { if (account == null) return@apply - val isTv = isTvSettings() || !root.isInTouchMode + val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt index 1dae5a0f..41aef176 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/account/AccountSelectActivity.kt @@ -18,8 +18,10 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.AutofitRecyclerView import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_EDIT_ACCOUNT import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_SELECT_ACCOUNT -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.BiometricAuthenticator import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication @@ -54,7 +56,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet fun askBiometricAuth() { - if (isTruePhone() && authEnabled) { + if (isLayout(PHONE) && authEnabled) { if (deviceHasPasswordPinLock(this)) { startBiometricAuthentication( this, @@ -62,8 +64,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet false ) - BiometricAuthenticator.promptInfo?.let { - BiometricAuthenticator.biometricPrompt?.authenticate(it) + BiometricAuthenticator.promptInfo?.let { promt -> + BiometricAuthenticator.biometricPrompt?.authenticate(promt) } } } @@ -127,7 +129,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet recyclerView.adapter = adapter - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { binding.editAccountButton.setBackgroundResource( R.drawable.player_button_tv_attr_no_bg ) @@ -168,7 +170,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet viewModel.toggleIsEditing() } - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { recyclerView.spanCount = if (liveAccounts.count() + 1 <= 6) { liveAccounts.count() + 1 } else 6 diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt index 27c2e1a3..e08eb772 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt @@ -5,6 +5,7 @@ import android.content.ClipboardManager import android.content.Context import android.os.Build import android.os.Bundle +import android.text.format.Formatter.formatShortFileSize import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -13,17 +14,25 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding +import com.lagradost.cloudstream3.databinding.StreamInputBinding import com.lagradost.cloudstream3.isMovieType +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick +import com.lagradost.cloudstream3.ui.player.BasicLink import com.lagradost.cloudstream3.ui.player.GeneratorPlayer import com.lagradost.cloudstream3.ui.player.LinkGenerator +import com.lagradost.cloudstream3.ui.result.FOCUS_SELF +import com.lagradost.cloudstream3.ui.result.setLinearListLayout +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE @@ -34,15 +43,6 @@ import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadManager -import android.text.format.Formatter.formatShortFileSize -import androidx.core.widget.doOnTextChanged -import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding -import com.lagradost.cloudstream3.databinding.StreamInputBinding -import com.lagradost.cloudstream3.mvvm.normalSafeApiCall -import com.lagradost.cloudstream3.ui.player.BasicLink -import com.lagradost.cloudstream3.ui.result.FOCUS_SELF -import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import java.net.URI @@ -200,7 +200,7 @@ class DownloadFragment : Fragment() { } // Should be visible in emulator layout - binding?.downloadStreamButton?.isGone = isTrueTvSettings() + binding?.downloadStreamButton?.isGone = isLayout(TV) binding?.downloadStreamButton?.setOnClickListener { val dialog = Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index d20fcf93..a729f33a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -2,16 +2,19 @@ package com.lagradost.cloudstream3.ui.download.button import android.content.Context import android.graphics.drawable.Drawable +import android.os.Looper import android.util.AttributeSet import android.util.Log import android.view.View import android.view.animation.AnimationUtils import android.widget.ImageView import android.widget.TextView +import androidx.annotation.MainThread import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible import com.lagradost.cloudstream3.R +import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DELETE_FILE import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK @@ -241,40 +244,54 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) : } }*/ + @MainThread + private fun setStatusInternal(status : DownloadStatusTell?) { + val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading + if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { + val animation = AnimationUtils.loadAnimation(context, waitingAnimation) + progressBarBackground.startAnimation(animation) + } else { + progressBarBackground.clearAnimation() + } + + val progressDrawable = + if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline + + progressBarBackground.background = + ContextCompat.getDrawable(context, progressDrawable) + + val drawable = getDrawableFromStatus(status) + statusView.setImageDrawable(drawable) + val isDrawable = drawable != null + + statusView.isVisible = isDrawable + val hide = hideWhenIcon && isDrawable + if (hide) { + progressBar.clearAnimation() + progressBarBackground.clearAnimation() + } + progressBarBackground.isGone = hide + progressBar.isGone = hide + } + /** Also sets currentStatus */ override fun setStatus(status: DownloadStatusTell?) { currentStatus = status - //progressBar.isVisible = - // status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error - //progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete - progressBarBackground.post { - val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading - if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { - val animation = AnimationUtils.loadAnimation(context, waitingAnimation) - progressBarBackground.startAnimation(animation) - } else { - progressBarBackground.clearAnimation() + // runs on the main thread, but also instant if it already is + if (Looper.myLooper() == Looper.getMainLooper()) { + try { + setStatusInternal(status) + } catch (t : Throwable) { + logError(t) // just in case setStatusInternal throws because thread + progressBarBackground.post { + setStatusInternal(status) + } } - - val progressDrawable = - if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline - - progressBarBackground.background = - ContextCompat.getDrawable(context, progressDrawable) - - val drawable = getDrawableFromStatus(status) - statusView.setImageDrawable(drawable) - val isDrawable = drawable != null - - statusView.isVisible = isDrawable - val hide = hideWhenIcon && isDrawable - if (hide) { - progressBar.clearAnimation() - progressBarBackground.clearAnimation() + } else { + progressBarBackground.post { + setStatusInternal(status) } - progressBarBackground.isGone = hide - progressBar.isGone = hide } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt index f84966eb..ebed901f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeChildItemAdapter.kt @@ -2,31 +2,58 @@ package com.lagradost.cloudstream3.ui.home import android.view.LayoutInflater import android.view.ViewGroup -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView +import androidx.fragment.app.Fragment import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding +import com.lagradost.cloudstream3.ui.BaseAdapter +import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchResultBuilder -import com.lagradost.cloudstream3.utils.AppUtils.isRtl +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout import com.lagradost.cloudstream3.utils.UIHelper.toPx -class HomeChildItemAdapter( - val cardList: MutableList, +class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState(view) { + /*private fun recursive(view : View) : Boolean { + if (view.isFocused) { + println("VIEW: $view | id=${view.id}") + } + return (view as? ViewGroup)?.children?.any { recursive(it) } ?: false + }*/ + // very shitty that we cant store the state when the view clears, + // but this is because the focus clears before the view is removed + // so we have to manually store it + var wasFocused: Boolean = false + override fun save(): Boolean = wasFocused + override fun restore(state: Boolean) { + if (state) { + wasFocused = false + // only refocus if tv + if(isLayout(TV)) { + itemView.requestFocus() + } + } + } +} + +class HomeChildItemAdapter( + fragment: Fragment, + id: Int, private val nextFocusUp: Int? = null, private val nextFocusDown: Int? = null, private val clickCallback: (SearchClickCallback) -> Unit, ) : - RecyclerView.Adapter() { + BaseAdapter(fragment, id) { var isHorizontal: Boolean = false var hasNext: Boolean = false - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + override fun onCreateContent(parent: ViewGroup): ViewHolderState { val expanded = parent.context.IsBottomLayout() /* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid @@ -39,164 +66,78 @@ class HomeChildItemAdapter( parent, false ) else HomeResultGridBinding.inflate(inflater, parent, false) + return HomeScrollViewHolderState(binding) + } + override fun onBindContent( + holder: ViewHolderState, + item: SearchResponse, + position: Int + ) { + when (val binding = holder.view) { + is HomeResultGridBinding -> { + binding.backgroundCard.apply { + val min = 114.toPx + val max = 180.toPx - return CardViewHolder( - binding, - clickCallback, - itemCount, + layoutParams = + layoutParams.apply { + width = if (!isHorizontal) { + min + } else { + max + } + height = if (!isHorizontal) { + max + } else { + min + } + } + } + } + + is HomeResultGridExpandedBinding -> { + binding.backgroundCard.apply { + val min = 114.toPx + val max = 180.toPx + + layoutParams = + layoutParams.apply { + width = if (!isHorizontal) { + min + } else { + max + } + height = if (!isHorizontal) { + max + } else { + min + } + } + } + + if (position == 0) { // to fix tv + binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view + } + } + } + + SearchResultBuilder.bind( + clickCallback = { click -> + // ok, so here we hijack the callback to fix the focus + when (click.action) { + SEARCH_ACTION_LOAD -> (holder as? HomeScrollViewHolderState)?.wasFocused = true + } + clickCallback(click) + }, + item, + position, + holder.itemView, + null, // nextFocusBehavior, nextFocusUp, - nextFocusDown, - isHorizontal, - parent.isRtl() - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is CardViewHolder -> { - holder.itemCount = itemCount // i know ugly af - holder.bind(cardList[position], position) - } - } - } - - override fun getItemCount(): Int { - return cardList.size - } - - override fun getItemId(position: Int): Long { - return (cardList[position].id ?: position).toLong() - } - - fun updateList(newList: List) { - val diffResult = DiffUtil.calculateDiff( - HomeChildDiffCallback(this.cardList, newList) + nextFocusDown ) - cardList.clear() - cardList.addAll(newList) - - diffResult.dispatchUpdatesTo(this) - } - - class CardViewHolder - constructor( - val binding: ViewBinding, - private val clickCallback: (SearchClickCallback) -> Unit, - var itemCount: Int, - private val nextFocusUp: Int? = null, - private val nextFocusDown: Int? = null, - private val isHorizontal: Boolean = false, - private val isRtl: Boolean - ) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(card: SearchResponse, position: Int) { - - // TV focus fixing - /*val nextFocusBehavior = when (position) { - 0 -> true - itemCount - 1 -> false - else -> null - } - - if (position == 0) { // to fix tv - if (isRtl) { - itemView.nextFocusRightId = R.id.nav_rail_view - itemView.nextFocusLeftId = -1 - } - else { - itemView.nextFocusLeftId = R.id.nav_rail_view - itemView.nextFocusRightId = -1 - } - } else { - itemView.nextFocusRightId = -1 - itemView.nextFocusLeftId = -1 - }*/ - - - when (binding) { - is HomeResultGridBinding -> { - binding.backgroundCard.apply { - val min = 114.toPx - val max = 180.toPx - - layoutParams = - layoutParams.apply { - width = if (!isHorizontal) { - min - } else { - max - } - height = if (!isHorizontal) { - max - } else { - min - } - } - } - - - } - - is HomeResultGridExpandedBinding -> { - binding.backgroundCard.apply { - val min = 114.toPx - val max = 180.toPx - - layoutParams = - layoutParams.apply { - width = if (!isHorizontal) { - min - } else { - max - } - height = if (!isHorizontal) { - max - } else { - min - } - } - } - - if (position == 0) { // to fix tv - binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view - } - } - } - - SearchResultBuilder.bind( - clickCallback, - card, - position, - itemView, - null, // nextFocusBehavior, - nextFocusUp, - nextFocusDown - ) - itemView.tag = position - - //val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f) - //ani.fillAfter = true - //ani.duration = 200 - //itemView.startAnimation(ani) - } + holder.itemView.tag = position } } - -class HomeChildDiffCallback( - private val oldList: List, - private val newList: List -) : - DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition].name == newList[newItemPosition].name - - override fun getOldListSize() = oldList.size - - override fun getNewListSize() = newList.size - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition] == newList[newItemPosition] && oldItemPosition < oldList.size - 1 // always update the last item -} \ 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 cd843517..12185cbf 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 @@ -42,8 +42,10 @@ import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLine 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.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.ownHide @@ -311,7 +313,7 @@ class HomeFragment : Fragment() { button?.isVisible = isValid button?.isChecked = isValid && selectedTypes.any { types.contains(it) } button?.isFocusable = true - if (isTrueTvSettings()) { + if (isLayout(TV)) { button?.isFocusableInTouchMode = true } @@ -435,7 +437,7 @@ class HomeFragment : Fragment() { bottomSheetDialog?.ownShow() val layout = - if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home + if (isLayout(TV or EMULATOR)) R.layout.fragment_home_tv else R.layout.fragment_home val root = inflater.inflate(layout, container, false) binding = try { FragmentHomeBinding.bind(root) @@ -449,6 +451,7 @@ class HomeFragment : Fragment() { } override fun onDestroyView() { + bottomSheetDialog?.ownHide() binding = null super.onDestroyView() @@ -485,6 +488,10 @@ class HomeFragment : Fragment() { private var bottomSheetDialog: BottomSheetDialog? = null + // https://github.com/vivchar/RendererRecyclerViewAdapter/blob/185251ee9d94fb6eb3e063b00d646b745186c365/example/src/main/java/com/github/vivchar/example/pages/github/GithubFragment.kt#L32 + // cry about it, but this is android we are talking about, we cant do the most simple shit without making a global variable + private var instanceState: Bundle = Bundle() + private var homeMasterAdapter: HomeParentItemAdapterPreview? = null @SuppressLint("SetTextI18n") override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -505,15 +512,14 @@ class HomeFragment : Fragment() { activity.loadSearchResult(listHomepageItems.random()) } } - - homeMasterRecycler.adapter = - HomeParentItemAdapterPreview( - mutableListOf(), - homeViewModel - ) + homeMasterAdapter = HomeParentItemAdapterPreview( + fragment = this@HomeFragment, + homeViewModel, + ) + homeMasterRecycler.adapter = homeMasterAdapter //fixPaddingStatusbar(homeLoadingStatusbar) - homeApiFab.isVisible = !isTvSettings() + homeApiFab.isVisible = isLayout(PHONE) homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -521,7 +527,7 @@ class HomeFragment : Fragment() { homeApiFab.shrink() // hide homeRandom.shrink() } else if (dy < -5) { - if (!isTvSettings()) { + if (isLayout(PHONE)) { homeApiFab.extend() // show homeRandom.extend() } @@ -540,7 +546,7 @@ class HomeFragment : Fragment() { settingsManager.getBoolean( getString(R.string.random_button_key), false - ) && !isTvSettings() + ) && isLayout(PHONE) binding?.homeRandom?.visibility = View.GONE } @@ -560,10 +566,11 @@ class HomeFragment : Fragment() { val mutableListOfResponse = mutableListOf() listHomepageItems.clear() - (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList( - d.values.toMutableList(), - homeMasterRecycler - ) + (homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(d.values.map { + it.copy( + list = it.list.copy(list = it.list.list.toMutableList()) + ) + }.toMutableList()) homeLoading.isVisible = false homeLoadingError.isVisible = false @@ -612,7 +619,7 @@ class HomeFragment : Fragment() { } is Resource.Loading -> { - (homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf()) + (homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(listOf()) homeLoadingShimmer.startShimmer() homeLoading.isVisible = true homeLoadingError.isVisible = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt index 443278a9..fb75e772 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeParentItemAdapter.kt @@ -1,22 +1,27 @@ package com.lagradost.cloudstream3.ui.home +import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListUpdateCallback +import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.HomepageParentBinding +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.BaseAdapter +import com.lagradost.cloudstream3.ui.BaseDiffCallback +import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SearchClickCallback -import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable class LoadClickCallback( @@ -27,193 +32,85 @@ class LoadClickCallback( ) open class ParentItemAdapter( - private var items: MutableList, - //private val viewModel: HomeViewModel, + open val fragment: Fragment, + id: Int, private val clickCallback: (SearchClickCallback) -> Unit, private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, private val expandCallback: ((String) -> Unit)? = null, -) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - - val layoutResId = when { - isTrueTvSettings() -> R.layout.homepage_parent_tv - parent.context.isEmulatorSettings() -> R.layout.homepage_parent_emulator - else -> R.layout.homepage_parent - } - - val root = LayoutInflater.from(parent.context).inflate(layoutResId, parent, false) - - val binding = HomepageParentBinding.bind(root) - - return ParentViewHolder( - binding, - clickCallback, - moreInfoClickCallback, - expandCallback - ) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is ParentViewHolder -> { - holder.bind(items[position]) - } - } - } - - override fun getItemCount(): Int { - return items.size - } - - override fun getItemId(position: Int): Long { - return items[position].list.name.hashCode().toLong() - } - - @JvmName("updateListHomePageList") - fun updateList(newList: List) { - updateList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) } - .toMutableList()) - } - - @JvmName("updateListExpandableHomepageList") - fun updateList( - newList: MutableList, - recyclerView: RecyclerView? = null - ) { - // this - // 1. prevents deep copy that makes this.items == newList - // 2. filters out undesirable results - // 3. moves empty results to the bottom (sortedBy is a stable sort) - val new = - newList.map { it.copy(list = it.list.copy(list = it.list.list.filterSearchResponse())) } - .sortedBy { it.list.list.isEmpty() } - - val diffResult = DiffUtil.calculateDiff( - SearchDiffCallback(items, new) - ) - items.clear() - items.addAll(new) - - //val mAdapter = this - val delta = if (this@ParentItemAdapter is HomeParentItemAdapterPreview) { - headItems - } else { - 0 - } - - diffResult.dispatchUpdatesTo(object : ListUpdateCallback { - override fun onInserted(position: Int, count: Int) { - //notifyItemRangeChanged(position + delta, count) - notifyItemRangeInserted(position + delta, count) - } - - override fun onRemoved(position: Int, count: Int) { - notifyItemRangeRemoved(position + delta, count) - } - - override fun onMoved(fromPosition: Int, toPosition: Int) { - notifyItemMoved(fromPosition + delta, toPosition + delta) - } - - override fun onChanged(_position: Int, count: Int, payload: Any?) { - - val position = _position + delta - - // I know kinda messy, what this does is using the update or bind instead of onCreateViewHolder -> bind - recyclerView?.apply { - // this loops every viewHolder in the recycle view and checks the position to see if it is within the update range - val missingUpdates = (position until (position + count)).toMutableSet() - for (i in 0 until itemCount) { - val child = getChildAt(i) ?: continue - val viewHolder = getChildViewHolder(child) ?: continue - if (viewHolder !is ParentViewHolder) continue - - val absolutePosition = viewHolder.bindingAdapterPosition - if (absolutePosition >= position && absolutePosition < position + count) { - val expand = items.getOrNull(absolutePosition - delta) ?: continue - missingUpdates -= absolutePosition - //println("Updating ${viewHolder.title.text} ($absolutePosition $position) -> ${expand.list.name}") - if (viewHolder.title.text == expand.list.name) { - viewHolder.update(expand) - } else { - viewHolder.bind(expand) - } - } - } - - // just in case some item did not get updated - for (i in missingUpdates) { - notifyItemChanged(i, payload) - } - } ?: run { - // in case we don't have a nice - notifyItemRangeChanged(position, count, payload) - } - } +) : BaseAdapter( + fragment, + id, + diffCallback = BaseDiffCallback( + itemSame = { a, b -> a.list.name == b.list.name }, + contentSame = { a, b -> + a.list.list == b.list.list }) - - //diffResult.dispatchUpdatesTo(this) - } - - class ParentViewHolder - constructor( - val binding: HomepageParentBinding, - // val viewModel: HomeViewModel, - private val clickCallback: (SearchClickCallback) -> Unit, - private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, - private val expandCallback: ((String) -> Unit)? = null, - ) : - RecyclerView.ViewHolder(binding.root) { - val title: TextView = binding.homeChildMoreInfo - private val recyclerView: RecyclerView = binding.homeChildRecyclerview - private val startFocus = R.id.nav_rail_view - private val endFocus = FOCUS_SELF - fun update(expand: HomeViewModel.ExpandableHomepageList) { - val info = expand.list - (recyclerView.adapter as? HomeChildItemAdapter?)?.apply { - updateList(info.list.toMutableList()) - hasNext = expand.hasNext - } ?: run { - recyclerView.adapter = HomeChildItemAdapter( - info.list.toMutableList(), - clickCallback = clickCallback, - nextFocusUp = recyclerView.nextFocusUpId, - nextFocusDown = recyclerView.nextFocusDownId, - ).apply { - isHorizontal = info.isHorizontalImages - hasNext = expand.hasNext - } - recyclerView.setLinearListLayout( - isHorizontal = true, - nextLeft = startFocus, - nextRight = endFocus, - ) - } +) { + data class ParentItemHolder(val binding: ViewBinding) : ViewHolderState(binding) { + override fun save(): Bundle = Bundle().apply { + val recyclerView = (binding as? HomepageParentBinding)?.homeChildRecyclerview + putParcelable( + "value", + recyclerView?.layoutManager?.onSaveInstanceState() + ) + (recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView) } - fun bind(expand: HomeViewModel.ExpandableHomepageList) { - val info = expand.list - recyclerView.adapter = HomeChildItemAdapter( - info.list.toMutableList(), + override fun restore(state: Bundle) { + (binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState( + state.getParcelable("value") + ) + } + } + + override fun onUpdateContent( + holder: ViewHolderState, + item: HomeViewModel.ExpandableHomepageList, + position: Int + ) { + val binding = holder.view + if (binding !is HomepageParentBinding) return + (binding.homeChildRecyclerview.adapter as? HomeChildItemAdapter)?.submitList(item.list.list) + } + + override fun onBindContent( + holder: ViewHolderState, + item: HomeViewModel.ExpandableHomepageList, + position: Int + ) { + val startFocus = R.id.nav_rail_view + val endFocus = FOCUS_SELF + val binding = holder.view + if (binding !is HomepageParentBinding) return + val info = item.list + binding.apply { + homeChildRecyclerview.adapter = HomeChildItemAdapter( + fragment = fragment, + id = id + position + 100, clickCallback = clickCallback, - nextFocusUp = recyclerView.nextFocusUpId, - nextFocusDown = recyclerView.nextFocusDownId, + nextFocusUp = homeChildRecyclerview.nextFocusUpId, + nextFocusDown = homeChildRecyclerview.nextFocusDownId, ).apply { isHorizontal = info.isHorizontalImages - hasNext = expand.hasNext + hasNext = item.hasNext + submitList(item.list.list) } - recyclerView.setLinearListLayout( + homeChildRecyclerview.setLinearListLayout( isHorizontal = true, nextLeft = startFocus, nextRight = endFocus, ) - title.text = info.name + homeChildMoreInfo.text = info.name - recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + homeChildRecyclerview.addOnScrollListener(object : + RecyclerView.OnScrollListener() { var expandCount = 0 - val name = expand.list.name + val name = item.list.name - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + override fun onScrollStateChanged( + recyclerView: RecyclerView, + newState: Int + ) { super.onScrollStateChanged(recyclerView, newState) val adapter = recyclerView.adapter @@ -237,27 +134,35 @@ open class ParentItemAdapter( }) //(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged() - if (!isTrueTvSettings()) { - title.setOnClickListener { - moreInfoClickCallback.invoke(expand) + if (isLayout(PHONE)) { + homeChildMoreInfo.setOnClickListener { + moreInfoClickCallback.invoke(item) } } } } -} -class SearchDiffCallback( - private val oldList: List, - private val newList: List -) : - DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition].list.name == newList[newItemPosition].list.name + override fun onCreateContent(parent: ViewGroup): ParentItemHolder { + val layoutResId = when { + isLayout(TV) -> R.layout.homepage_parent_tv + isLayout(EMULATOR) -> R.layout.homepage_parent_emulator + else -> R.layout.homepage_parent + } - override fun getOldListSize() = oldList.size + val inflater = LayoutInflater.from(parent.context) + val binding = try { + HomepageParentBinding.bind(inflater.inflate(layoutResId, parent, false)) + } catch (t: Throwable) { + logError(t) + // just in case someone forgot we don't want to crash + HomepageParentBinding.inflate(inflater) + } - override fun getNewListSize() = newList.size + return ParentItemHolder(binding) + } - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - oldList[oldItemPosition] == newList[newItemPosition] + fun updateList(newList: List) { + submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) } + .toMutableList()) + } } \ No newline at end of file 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 0e397f81..52ec06db 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 @@ -1,5 +1,7 @@ package com.lagradost.cloudstream3.ui.home +import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -7,6 +9,7 @@ import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.fragment.app.Fragment import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding @@ -26,6 +29,7 @@ import com.lagradost.cloudstream3.databinding.FragmentHomeHeadTvBinding import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.observe +import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage @@ -36,8 +40,9 @@ import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA import com.lagradost.cloudstream3.ui.search.SearchClickCallback -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes @@ -46,113 +51,87 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView import com.lagradost.cloudstream3.utils.UIHelper.populateChips class HomeParentItemAdapterPreview( - items: MutableList, + override val fragment: Fragment, private val viewModel: HomeViewModel, -) : ParentItemAdapter(items, clickCallback = { - viewModel.click(it) -}, moreInfoClickCallback = { - viewModel.popup(it) -}, expandCallback = { - viewModel.expand(it) -}) { - val headItems = 1 +) : ParentItemAdapter(fragment, id = "HomeParentItemAdapterPreview".hashCode(), + clickCallback = { + viewModel.click(it) + }, moreInfoClickCallback = { + viewModel.popup(it) + }, expandCallback = { + viewModel.expand(it) + }) { + override val headers = 1 + override fun onCreateHeader(parent: ViewGroup): ViewHolderState { + val inflater = LayoutInflater.from(parent.context) + val binding = if (isLayout(TV or EMULATOR)) FragmentHomeHeadTvBinding.inflate( + inflater, + parent, + false + ) else FragmentHomeHeadBinding.inflate(inflater, parent, false) - companion object { - private const val VIEW_TYPE_HEADER = 2 - private const val VIEW_TYPE_ITEM = 1 - } + if (binding is FragmentHomeHeadTvBinding && isLayout(EMULATOR)) { + binding.homeBookmarkParentItemMoreInfo.isVisible = true - override fun getItemViewType(position: Int) = when (position) { - 0 -> VIEW_TYPE_HEADER - else -> VIEW_TYPE_ITEM - } + val marginInDp = 50 + val density = binding.horizontalScrollChips.context.resources.displayMetrics.density + val marginInPixels = (marginInDp * density).toInt() - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is HeaderViewHolder -> {} - else -> super.onBindViewHolder(holder, position - headItems) + val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams + params.marginEnd = marginInPixels + binding.horizontalScrollChips.layoutParams = params + binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds( + null, + null, + ContextCompat.getDrawable( + parent.context, + R.drawable.ic_baseline_arrow_forward_24 + ), + null + ) } + + return HeaderViewHolder(binding, viewModel, fragment = fragment) } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - VIEW_TYPE_HEADER -> { - val inflater = LayoutInflater.from(parent.context) - val binding = if (isTvSettings()) FragmentHomeHeadTvBinding.inflate( - inflater, - parent, - false - ) else FragmentHomeHeadBinding.inflate(inflater, parent, false) + override fun onBindHeader(holder: ViewHolderState) { + (holder as? HeaderViewHolder)?.bind() + } - if (binding is FragmentHomeHeadTvBinding && parent.context.isEmulatorSettings()) { - binding.homeBookmarkParentItemMoreInfo.isVisible = true + private class HeaderViewHolder( + val binding: ViewBinding, val viewModel: HomeViewModel, fragment: Fragment, + ) : + ViewHolderState(binding) { - val marginInDp = 50 - val density = binding.horizontalScrollChips.context.resources.displayMetrics.density - val marginInPixels = (marginInDp * density).toInt() - - val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams - params.marginEnd = marginInPixels - binding.horizontalScrollChips.layoutParams = params - binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds( - null, - null, - ContextCompat.getDrawable( - parent.context, - R.drawable.ic_baseline_arrow_forward_24 - ), - null - ) - } - - HeaderViewHolder( - binding, - viewModel, + override fun save(): Bundle = + Bundle().apply { + putParcelable( + "resumeRecyclerView", + resumeRecyclerView.layoutManager?.onSaveInstanceState() ) + putParcelable( + "bookmarkRecyclerView", + bookmarkRecyclerView.layoutManager?.onSaveInstanceState() + ) + //putInt("previewViewpager", previewViewpager.currentItem) } - VIEW_TYPE_ITEM -> super.onCreateViewHolder(parent, viewType) - else -> error("Unhandled viewType=$viewType") - } - } - - override fun getItemCount(): Int { - return super.getItemCount() + headItems - } - - override fun getItemId(position: Int): Long { - if (position == 0) return 0//previewData.hashCode().toLong() - return super.getItemId(position - headItems) - } - - override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { - when (holder) { - is HeaderViewHolder -> { - holder.onViewDetachedFromWindow() + override fun restore(state: Bundle) { + state.getParcelable("resumeRecyclerView")?.let { recycle -> + resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - - else -> super.onViewDetachedFromWindow(holder) - } - } - - override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { - when (holder) { - is HeaderViewHolder -> { - holder.onViewAttachedToWindow() + state.getParcelable("bookmarkRecyclerView")?.let { recycle -> + bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle) } - - else -> super.onViewAttachedToWindow(holder) + //state.getInt("previewViewpager").let { recycle -> + // previewViewpager.setCurrentItem(recycle,true) + //} } - } - class HeaderViewHolder - constructor( - val binding: ViewBinding, - val viewModel: HomeViewModel, - ) : RecyclerView.ViewHolder(binding.root) { - private var previewAdapter: HomeScrollAdapter = HomeScrollAdapter() - private var resumeAdapter: HomeChildItemAdapter = HomeChildItemAdapter( - ArrayList(), + val previewAdapter = HomeScrollAdapter(fragment = fragment) + private val resumeAdapter = HomeChildItemAdapter( + fragment, + id = "resumeAdapter".hashCode(), nextFocusUp = itemView.nextFocusUpId, nextFocusDown = itemView.nextFocusDownId ) { callback -> @@ -207,8 +186,9 @@ class HomeParentItemAdapterPreview( } } } - private var bookmarkAdapter: HomeChildItemAdapter = HomeChildItemAdapter( - ArrayList(), + private val bookmarkAdapter = HomeChildItemAdapter( + fragment, + id = "bookmarkAdapter".hashCode(), nextFocusUp = itemView.nextFocusUpId, nextFocusDown = itemView.nextFocusDownId ) { callback -> @@ -217,7 +197,10 @@ class HomeParentItemAdapterPreview( return@HomeChildItemAdapter } - (callback.view.context?.getActivity() as? MainActivity)?.loadPopup(callback.card, load = false) + (callback.view.context?.getActivity() as? MainActivity)?.loadPopup( + callback.card, + load = false + ) /* callback.view.context?.getActivity()?.showOptionSelectStringRes( callback.view, @@ -267,7 +250,6 @@ class HomeParentItemAdapterPreview( */ } - private val previewViewpager: ViewPager2 = itemView.findViewById(R.id.home_preview_viewpager) @@ -275,38 +257,24 @@ class HomeParentItemAdapterPreview( itemView.findViewById(R.id.home_preview_viewpager_text) // private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) - private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) - private var resumeRecyclerView: RecyclerView = + private val resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) + private val resumeRecyclerView: RecyclerView = itemView.findViewById(R.id.home_watch_child_recyclerview) - private var bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder) - private var bookmarkRecyclerView: RecyclerView = + private val bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder) + private val bookmarkRecyclerView: RecyclerView = itemView.findViewById(R.id.home_bookmarked_child_recyclerview) - private var homeAccount: View? = - itemView.findViewById(R.id.home_preview_switch_account) - private var alternativeHomeAccount: View? = + private val homeAccount: View? = itemView.findViewById(R.id.home_preview_switch_account) + private val alternativeHomeAccount: View? = itemView.findViewById(R.id.alternative_switch_account) - private var topPadding: View? = itemView.findViewById(R.id.home_padding) + private val topPadding: View? = itemView.findViewById(R.id.home_padding) - private var alternativeAccountPadding: View? = itemView.findViewById(R.id.alternative_account_padding) + private val alternativeAccountPadding: View? = + itemView.findViewById(R.id.alternative_account_padding) private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding) - private val previewCallback: ViewPager2.OnPageChangeCallback = - object : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - previewAdapter.apply { - if (position >= itemCount - 1 && hasMoreItems) { - hasMoreItems = false // don't make two requests - viewModel.loadMoreHomeScrollResponses() - } - } - val item = previewAdapter.getItem(position) ?: return - onSelect(item, position) - } - } - fun onSelect(item: LoadResponse, position: Int) { (binding as? FragmentHomeHeadTvBinding)?.apply { homePreviewDescription.isGone = @@ -379,14 +347,14 @@ class HomeParentItemAdapterPreview( homePreviewBookmark.setOnClickListener { fab -> fab.context.getActivity()?.showBottomDialog( - WatchType.values() + WatchType.entries .map { fab.context.getString(it.stringRes) } .toList(), DataStoreHelper.getResultWatchState(id).ordinal, fab.context.getString(R.string.action_add_to_bookmarks), showApply = false, {}) { - val newValue = WatchType.values()[it] + val newValue = WatchType.entries[it] ResultViewModel2().updateWatchStatus( newValue, @@ -411,38 +379,22 @@ class HomeParentItemAdapterPreview( } } - fun onViewDetachedFromWindow() { - previewViewpager.unregisterOnPageChangeCallback(previewCallback) - } - - fun onViewAttachedToWindow() { - previewViewpager.registerOnPageChangeCallback(previewCallback) - - binding.root.findViewTreeLifecycleOwner()?.apply { - observe(viewModel.preview) { - updatePreview(it) - } - if (binding is FragmentHomeHeadTvBinding) { - observe(viewModel.apiName) { name -> - binding.homePreviewChangeApi.text = name - } - } - observe(viewModel.resumeWatching) { - updateResume(it) - } - observe(viewModel.bookmarks) { - updateBookmarks(it) - } - observe(viewModel.availableWatchStatusTypes) { (checked, visible) -> - for ((chip, watch) in toggleList) { - chip.apply { - isVisible = visible.contains(watch) - isChecked = checked.contains(watch) + private val previewCallback: ViewPager2.OnPageChangeCallback = + object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + previewAdapter.apply { + if (position >= itemCount - 1 && hasMoreItems) { + hasMoreItems = false // don't make two requests + viewModel.loadMoreHomeScrollResponses() } } - toggleListHolder?.isGone = visible.isEmpty() + val item = previewAdapter.getItemOrNull(position) ?: return + onSelect(item, position) } - } ?: debugException { "Expected findViewTreeLifecycleOwner" } + } + + override fun onViewDetachedFromWindow() { + previewViewpager.unregisterOnPageChangeCallback(previewCallback) } private val toggleList = listOf>( @@ -455,6 +407,8 @@ class HomeParentItemAdapterPreview( private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder) + fun bind() = Unit + init { previewViewpager.setPageTransformer(HomeScrollTransformer()) @@ -561,7 +515,9 @@ class HomeParentItemAdapterPreview( when (preview) { is Resource.Success -> { - if (!previewAdapter.setItems( + previewAdapter.submitList(preview.value.second) + previewAdapter.hasMoreItems = preview.value.first + /*if (!.setItems( preview.value.second, preview.value.first ) @@ -573,15 +529,16 @@ class HomeParentItemAdapterPreview( previewViewpager.fakeDragBy(1f) previewViewpager.endFakeDrag() previewCallback.onPageSelected(0) - previewViewpager.isVisible = true - previewViewpagerText.isVisible = true - alternativeAccountPadding?.isVisible = false //previewHeader.isVisible = true - } + }*/ + + previewViewpager.isVisible = true + previewViewpagerText.isVisible = true + alternativeAccountPadding?.isVisible = false } else -> { - previewAdapter.setItems(listOf(), false) + previewAdapter.submitList(listOf()) previewViewpager.setCurrentItem(0, false) previewViewpager.isVisible = false previewViewpagerText.isVisible = false @@ -593,12 +550,12 @@ class HomeParentItemAdapterPreview( private fun updateResume(resumeWatching: List) { resumeHolder.isVisible = resumeWatching.isNotEmpty() - resumeAdapter.updateList(resumeWatching) + resumeAdapter.submitList(resumeWatching) if ( binding is FragmentHomeHeadBinding || binding is FragmentHomeHeadTvBinding && - binding.root.context.isEmulatorSettings() + isLayout(EMULATOR) ) { val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle ?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle @@ -623,12 +580,12 @@ class HomeParentItemAdapterPreview( private fun updateBookmarks(data: Pair>) { val (visible, list) = data bookmarkHolder.isVisible = visible - bookmarkAdapter.updateList(list) + bookmarkAdapter.submitList(list) if ( binding is FragmentHomeHeadBinding || binding is FragmentHomeHeadTvBinding && - binding.root.context.isEmulatorSettings() + isLayout(EMULATOR) ) { val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle ?: (binding as? FragmentHomeHeadTvBinding)?.homeBookmarkParentItemTitle @@ -653,5 +610,35 @@ class HomeParentItemAdapterPreview( } } } + + override fun onViewAttachedToWindow() { + previewViewpager.registerOnPageChangeCallback(previewCallback) + + binding.root.findViewTreeLifecycleOwner()?.apply { + observe(viewModel.preview) { + updatePreview(it) + } + if (binding is FragmentHomeHeadTvBinding) { + observe(viewModel.apiName) { name -> + binding.homePreviewChangeApi.text = name + } + } + observe(viewModel.resumeWatching) { + updateResume(it) + } + observe(viewModel.bookmarks) { + updateBookmarks(it) + } + observe(viewModel.availableWatchStatusTypes) { (checked, visible) -> + for ((chip, watch) in toggleList) { + chip.apply { + isVisible = visible.contains(watch) + isChecked = checked.contains(watch) + } + } + toggleListHolder?.isGone = visible.isEmpty() + } + } ?: debugException { "Expected findViewTreeLifecycleOwner" } + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt index 666fbc24..29186e83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/home/HomeScrollAdapter.kt @@ -4,112 +4,61 @@ import android.content.res.Configuration import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.isGone -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.RecyclerView -import androidx.viewbinding.ViewBinding +import androidx.fragment.app.Fragment import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.NoStateAdapter +import com.lagradost.cloudstream3.ui.ViewHolderState +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.setImage -class HomeScrollAdapter : RecyclerView.Adapter() { - private var items: MutableList = mutableListOf() +class HomeScrollAdapter( + fragment: Fragment +) : NoStateAdapter(fragment) { var hasMoreItems: Boolean = false - fun getItem(position: Int): LoadResponse? { - return items.getOrNull(position) - } - - fun setItems(newItems: List, hasNext: Boolean): Boolean { - val isSame = newItems.firstOrNull()?.url == items.firstOrNull()?.url - hasMoreItems = hasNext - - val diffResult = DiffUtil.calculateDiff( - HomeScrollDiffCallback(this.items, newItems) - ) - - items.clear() - items.addAll(newItems) - - - diffResult.dispatchUpdatesTo(this) - - return isSame - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + override fun onCreateContent(parent: ViewGroup): ViewHolderState { val inflater = LayoutInflater.from(parent.context) - val binding = if (isTvSettings()) { + val binding = if (isLayout(TV or EMULATOR)) { HomeScrollViewTvBinding.inflate(inflater, parent, false) } else { HomeScrollViewBinding.inflate(inflater, parent, false) } - return CardViewHolder( - binding, - //forceHorizontalPosters - ) + return ViewHolderState(binding) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is CardViewHolder -> { - holder.bind(items[position]) + override fun onBindContent( + holder: ViewHolderState, + item: LoadResponse, + position: Int, + ) { + val binding = holder.view + val itemView = holder.itemView + val isHorizontal = + binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + + val posterUrl = + if (isHorizontal) item.backgroundPosterUrl ?: item.posterUrl else item.posterUrl + ?: item.backgroundPosterUrl + + when (binding) { + is HomeScrollViewBinding -> { + binding.homeScrollPreview.setImage(posterUrl) + binding.homeScrollPreviewTags.apply { + text = item.tags?.joinToString(" • ") ?: "" + isGone = item.tags.isNullOrEmpty() + maxLines = 2 + } + binding.homeScrollPreviewTitle.text = item.name + } + + is HomeScrollViewTvBinding -> { + binding.homeScrollPreview.setImage(posterUrl) } } } - - class CardViewHolder - constructor( - val binding: ViewBinding, - //private val forceHorizontalPosters: Boolean? = null - ) : - RecyclerView.ViewHolder(binding.root) { - - fun bind(card: LoadResponse) { - val isHorizontal = - binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE - - val posterUrl = - if (isHorizontal) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl - ?: card.backgroundPosterUrl - - when (binding) { - is HomeScrollViewBinding -> { - binding.homeScrollPreview.setImage(posterUrl) - binding.homeScrollPreviewTags.apply { - text = card.tags?.joinToString(" • ") ?: "" - isGone = card.tags.isNullOrEmpty() - maxLines = 2 - } - binding.homeScrollPreviewTitle.text = card.name - } - - is HomeScrollViewTvBinding -> { - binding.homeScrollPreview.setImage(posterUrl) - } - } - } - } - - class HomeScrollDiffCallback( - private val oldList: List, - private val newList: List - ) : - DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition].url == newList[newItemPosition].url - - override fun getOldListSize() = oldList.size - - override fun getNewListSize() = newList.size - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldList[oldItemPosition] == newList[newItemPosition] - } - - override fun getItemCount(): Int { - return items.size - } } \ No newline at end of file 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 f471fefd..a2c7583f 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 @@ -34,7 +34,8 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -52,6 +53,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext import java.util.EnumSet +import java.util.concurrent.CopyOnWriteArrayList import kotlin.collections.set class HomeViewModel : ViewModel() { @@ -124,7 +126,7 @@ class HomeViewModel : ViewModel() { private val _resumeWatching = MutableLiveData>() private val _preview = MutableLiveData>>>() - private val previewResponses = mutableListOf() + private val previewResponses = CopyOnWriteArrayList() private val previewResponsesAdded = mutableSetOf() val resumeWatching: LiveData> = _resumeWatching @@ -132,7 +134,7 @@ class HomeViewModel : ViewModel() { private fun loadResumeWatching() = viewModelScope.launchSafe { val resumeWatchingResult = getResumeWatching() - if (isTrueTvSettings() && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (isLayout(TV) && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { ioSafe { // this WILL crash on non tvs, so keep this inside a try catch activity?.addProgramsToContinueWatching(resumeWatchingResult) @@ -326,7 +328,13 @@ class HomeViewModel : ViewModel() { val filteredList = context?.filterHomePageListByFilmQuality(list) ?: list expandable[list.name] = - ExpandableHomepageList(filteredList, 1, home.hasNext) + ExpandableHomepageList( + filteredList.copy( + list = CopyOnWriteArrayList( + filteredList.list + ) + ), 1, home.hasNext + ) } } @@ -341,8 +349,7 @@ class HomeViewModel : ViewModel() { val currentList = items.shuffled().filter { it.list.isNotEmpty() } .flatMap { it.list } - .distinctBy { it.url } - .toList() + .distinctBy { it.url }.toList() if (currentList.isNotEmpty()) { val randomItems = 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 fa91d990..90e57ef4 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 @@ -49,7 +49,10 @@ 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.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity @@ -57,6 +60,7 @@ 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 +import java.util.concurrent.CopyOnWriteArrayList import kotlin.math.abs const val LIBRARY_FOLDER = "library_folder" @@ -101,7 +105,7 @@ class LibraryFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { val layout = - if (SettingsFragment.isTvSettings()) R.layout.fragment_library_tv else R.layout.fragment_library + if (isLayout(TV or EMULATOR)) R.layout.fragment_library_tv else R.layout.fragment_library val root = inflater.inflate(layout, container, false) binding = try { FragmentLibraryBinding.bind(root) @@ -160,7 +164,8 @@ class LibraryFragment : Fragment() { } // Set the color for the search exit icon to the correct theme text color - val searchExitIcon = binding?.mainSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) + val searchExitIcon = + binding?.mainSearch?.findViewById(androidx.appcompat.R.id.search_close_btn) val searchExitIconColor = TypedValue() activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true) @@ -220,7 +225,7 @@ class LibraryFragment : Fragment() { settingsManager.getBoolean( getString(R.string.random_button_key), false - ) && !SettingsFragment.isTvSettings() + ) && isLayout(PHONE) binding?.libraryRandom?.visibility = View.GONE } @@ -228,7 +233,7 @@ class LibraryFragment : Fragment() { if (listLibraryItems.isNotEmpty()) { val listLibraryItem = listLibraryItems.random() libraryViewModel.currentSyncApi?.syncIdName?.let { - loadLibraryItem(it, listLibraryItem.syncId,listLibraryItem) + loadLibraryItem(it, listLibraryItem.syncId, listLibraryItem) } } } @@ -307,44 +312,46 @@ class LibraryFragment : Fragment() { binding?.viewpager?.setPageTransformer(LibraryScrollTransformer()) - binding?.viewpager?.adapter = - binding?.viewpager?.adapter ?: ViewpagerAdapter( - mutableListOf(), - { isScrollingDown: Boolean -> - if (isScrollingDown) { - binding?.sortFab?.shrink() - binding?.libraryRandom?.shrink() - } else { - binding?.sortFab?.extend() - binding?.libraryRandom?.extend() - } - }) callback@{ searchClickCallback -> - // To prevent future accidents - debugAssert({ - searchClickCallback.card !is SyncAPI.LibraryItem - }, { - "searchClickCallback ${searchClickCallback.card} is not a LibraryItem" - }) + binding?.viewpager?.adapter = ViewpagerAdapter( + fragment = this, + { isScrollingDown: Boolean -> + if (isScrollingDown) { + binding?.sortFab?.shrink() + binding?.libraryRandom?.shrink() + } else { + binding?.sortFab?.extend() + binding?.libraryRandom?.extend() + } + }) callback@{ searchClickCallback -> + // To prevent future accidents + debugAssert({ + searchClickCallback.card !is SyncAPI.LibraryItem + }, { + "searchClickCallback ${searchClickCallback.card} is not a LibraryItem" + }) - val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId - val syncName = - libraryViewModel.currentSyncApi?.syncIdName ?: return@callback + val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId + val syncName = + libraryViewModel.currentSyncApi?.syncIdName ?: return@callback - when (searchClickCallback.action) { - SEARCH_ACTION_SHOW_METADATA -> { - (activity as? MainActivity)?.loadPopup(searchClickCallback.card, load = false) + when (searchClickCallback.action) { + SEARCH_ACTION_SHOW_METADATA -> { + (activity as? MainActivity)?.loadPopup( + searchClickCallback.card, + load = false + ) /*activity?.showPluginSelectionDialog( syncId, syncName, searchClickCallback.card.apiName )*/ - } + } - SEARCH_ACTION_LOAD -> { - loadLibraryItem(syncName, syncId, searchClickCallback.card) - } + SEARCH_ACTION_LOAD -> { + loadLibraryItem(syncName, syncId, searchClickCallback.card) } } + } binding?.apply { viewpager.offscreenPageLimit = 2 @@ -390,7 +397,11 @@ class LibraryFragment : Fragment() { } } - (viewpager.adapter as? ViewpagerAdapter)?.pages = pages + (viewpager.adapter as? ViewpagerAdapter)?.submitList(pages.map { + it.copy( + items = CopyOnWriteArrayList(it.items) + ) + }) //fix focus on the viewpager itself (viewpager.getChildAt(0) as RecyclerView).apply { tag = "tv_no_focus_tag" @@ -398,10 +409,10 @@ class LibraryFragment : Fragment() { } // Using notifyItemRangeChanged keeps the animations when sorting - viewpager.adapter?.notifyItemRangeChanged( + /*viewpager.adapter?.notifyItemRangeChanged( 0, viewpager.adapter?.itemCount ?: 0 - ) + )*/ libraryViewModel.currentPage.value?.let { page -> binding?.viewpager?.setCurrentItem(page, false) @@ -459,12 +470,14 @@ class LibraryFragment : Fragment() { } }.attach() - binding?.libraryTabLayout?.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener { + binding?.libraryTabLayout?.addOnTabSelectedListener(object : + TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { binding?.libraryTabLayout?.selectedTabPosition?.let { page -> libraryViewModel.switchPage(page) } } + override fun onTabUnselected(tab: TabLayout.Tab?) = Unit override fun onTabReselected(tab: TabLayout.Tab?) = Unit }) @@ -564,8 +577,9 @@ class LibraryFragment : Fragment() { } + @SuppressLint("NotifyDataSetChanged") override fun onConfigurationChanged(newConfig: Configuration) { - (binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() + binding?.viewpager?.adapter?.notifyDataSetChanged() super.onConfigurationChanged(newConfig) } 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 c983ea2f..1bd01c86 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 @@ -113,7 +113,7 @@ class LibraryViewModel : ViewModel() { } val desiredSortingMethod = - ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode) + ListSorting.entries.getOrNull(DataStoreHelper.librarySortingMode) if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) { sort(desiredSortingMethod, null, pages) } else { 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 6731eae2..cfd22220 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 @@ -1,104 +1,123 @@ package com.lagradost.cloudstream3.ui.library import android.os.Build -import android.util.Log +import android.os.Bundle +import android.os.Parcelable import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.doOnAttach -import androidx.recyclerview.widget.RecyclerView +import androidx.fragment.app.Fragment import androidx.recyclerview.widget.RecyclerView.OnFlingListener import com.google.android.material.appbar.AppBarLayout import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding import com.lagradost.cloudstream3.syncproviders.SyncAPI +import com.lagradost.cloudstream3.ui.BaseAdapter +import com.lagradost.cloudstream3.ui.BaseDiffCallback +import com.lagradost.cloudstream3.ui.ViewHolderState import com.lagradost.cloudstream3.ui.search.SearchClickCallback -import com.lagradost.cloudstream3.ui.settings.SettingsFragment +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount +class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) : + ViewHolderState(binding) { + override fun save(): Bundle = + Bundle().apply { + putParcelable( + "pageRecyclerview", + binding.pageRecyclerview.layoutManager?.onSaveInstanceState() + ) + } + + override fun restore(state: Bundle) { + state.getParcelable("pageRecyclerview")?.let { recycle -> + binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle) + } + } +} + class ViewpagerAdapter( - var pages: List, + fragment: Fragment, val scrollCallback: (isScrollingDown: Boolean) -> Unit, val clickCallback: (SearchClickCallback) -> Unit -) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return PageViewHolder( +) : BaseAdapter(fragment, + id = "ViewpagerAdapter".hashCode(), + diffCallback = BaseDiffCallback( + itemSame = { a, b -> + a.title == b.title + }, + contentSame = { a, b -> + a.items == b.items && a.title == b.title + } +)) { + override fun onCreateContent(parent: ViewGroup): ViewHolderState { + return ViewpagerAdapterViewHolderState( LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (holder) { - is PageViewHolder -> { - holder.bind(pages[position], position, unbound.remove(position)) - } - } + override fun onUpdateContent( + holder: ViewHolderState, + item: SyncAPI.Page, + position: Int + ) { + val binding = holder.view + if (binding !is LibraryViewpagerPageBinding) return + (binding.pageRecyclerview.adapter as? PageAdapter)?.updateList(item.items) } - private val unbound = mutableSetOf() + override fun onBindContent(holder: ViewHolderState, item: SyncAPI.Page, position: Int) { + val binding = holder.view + if (binding !is LibraryViewpagerPageBinding) return - /** - * Used to mark all pages for re-binding and forces all items to be refreshed - * Without this the pages will still use the same adapters - **/ - fun rebind() { - unbound.addAll(0..pages.size) - this.notifyItemRangeChanged(0, pages.size) - } - - inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : - RecyclerView.ViewHolder(binding.root) { - fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) { - binding.pageRecyclerview.tag = position - binding.pageRecyclerview.apply { - spanCount = - this@PageViewHolder.itemView.context.getSpanCount() ?: 3 - if (adapter == null || rebind) { - // Only add the items after it has been attached since the items rely on ItemWidth - // Which is only determined after the recyclerview is attached. - // If this fails then item height becomes 0 when there is only one item - doOnAttach { - adapter = PageAdapter( - page.items.toMutableList(), - this, - clickCallback - ) - } - } else { - (adapter as? PageAdapter)?.updateList(page.items) - scrollToPosition(0) + binding.pageRecyclerview.tag = position + binding.pageRecyclerview.apply { + spanCount = + binding.root.context.getSpanCount() ?: 3 + if (adapter == null) { // || rebind + // Only add the items after it has been attached since the items rely on ItemWidth + // Which is only determined after the recyclerview is attached. + // If this fails then item height becomes 0 when there is only one item + doOnAttach { + adapter = PageAdapter( + item.items.toMutableList(), + this, + clickCallback + ) } + } else { + (adapter as? PageAdapter)?.updateList(item.items) + // scrollToPosition(0) + } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> - val diff = scrollY - oldScrollY + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> + val diff = scrollY - oldScrollY - //Expand the top Appbar based on scroll direction up/down, simulate phone behavior - if (SettingsFragment.isTvSettings()) { - binding.root.rootView.findViewById(R.id.search_bar) - .apply { - if (diff <= 0) - setExpanded(true) - else - setExpanded(false) - } - } - if (diff == 0) return@setOnScrollChangeListener - - scrollCallback.invoke(diff > 0) + //Expand the top Appbar based on scroll direction up/down, simulate phone behavior + if (isLayout(TV or EMULATOR)) { + binding.root.rootView.findViewById(R.id.search_bar) + .apply { + if (diff <= 0) + setExpanded(true) + else + setExpanded(false) + } } - } else { - onFlingListener = object : OnFlingListener() { - override fun onFling(velocityX: Int, velocityY: Int): Boolean { - scrollCallback.invoke(velocityY > 0) - return false - } + if (diff == 0) return@setOnScrollChangeListener + + scrollCallback.invoke(diff > 0) + } + } else { + onFlingListener = object : OnFlingListener() { + override fun onFling(velocityX: Int, velocityY: Int): Boolean { + scrollCallback.invoke(velocityY > 0) + return false } } } } } - - override fun getItemCount(): Int { - return pages.size - } } \ No newline at end of file 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 e8d74752..d79c44b7 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 @@ -46,6 +46,10 @@ import com.lagradost.cloudstream3.ui.player.GeneratorPlayer.Companion.subsProvid 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.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.DataStoreHelper @@ -77,7 +81,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() { private var isVerticalOrientation: Boolean = false protected open var lockRotation = true protected open var isFullScreenPlayer = true - protected open var isTv = false protected var playerBinding: PlayerCustomLayoutBinding? = null private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false) @@ -1204,7 +1207,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { // netflix capture back and hide ~monke KeyEvent.KEYCODE_BACK -> { - if (isShowing && isTv) { + if (isShowing && isLayout(TV or EMULATOR)) { onClickChange() return true } @@ -1514,7 +1517,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() { } } // cs3 is peak media center - setRemainingTimeCounter(durationMode || SettingsFragment.isTrueTvSettings()) + setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV)) playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ -> updateRemainingTime() } 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 01069f66..7ff56886 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 @@ -39,7 +39,10 @@ import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSub import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.result.* -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 import com.lagradost.cloudstream3.utils.* @@ -1275,8 +1278,7 @@ class GeneratorPlayer : FullScreenPlayer() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { // this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason - isTv = isTvSettings() - layout = if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player + layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java] 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 6414374b..fb600ef1 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 @@ -9,6 +9,9 @@ import android.util.Log import androidx.annotation.WorkerThread import androidx.core.graphics.scale import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.ExtractorLink @@ -63,7 +66,7 @@ interface IPreviewGenerator { companion object { fun new(): IPreviewGenerator { /** because TV has low ram + not show we disable this for now */ - return if (SettingsFragment.isTrueTvSettings()) { + return if (isLayout(TV)) { empty() } else { PreviewGenerator() 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 5b300c06..e9e00736 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 @@ -34,7 +34,8 @@ 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.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar @@ -173,7 +174,7 @@ class QuickSearchFragment : Fragment() { } } else { binding?.quickSearchMasterRecycler?.adapter = - ParentItemAdapter(mutableListOf(), { callback -> + ParentItemAdapter(fragment = this, id = "quickSearchMasterRecycler".hashCode(), { callback -> SearchHelper.handleSearchClickCallback(callback) //when (callback.action) { //SEARCH_ACTION_LOAD -> { @@ -277,7 +278,7 @@ class QuickSearchFragment : Fragment() { activity?.popCurrentPage() } - if (isTrueTvSettings()) { + if (isLayout(TV)) { binding?.quickSearch?.requestFocus() } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt index 6b63e623..fad349c8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/EpisodeAdapter.kt @@ -15,8 +15,10 @@ import com.lagradost.cloudstream3.databinding.ResultEpisodeLargeBinding import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DownloadClickEvent -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx @@ -172,15 +174,13 @@ class EpisodeAdapter( @SuppressLint("SetTextI18n") fun bind(card: ResultEpisode) { localCard = card - val setWidth = - if (isTvSettings()) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE_LARGE.toPx else ViewGroup.LayoutParams.MATCH_PARENT binding.episodeLinHolder.layoutParams.width = setWidth binding.episodeHolderLarge.layoutParams.width = setWidth binding.episodeHolder.layoutParams.width = setWidth - val isTrueTv = isTrueTvSettings() binding.apply { downloadButton.isVisible = hasDownloadSupport @@ -246,12 +246,21 @@ class EpisodeAdapter( episodeDescript.apply { text = card.description.html() isGone = text.isNullOrBlank() + + var isExpanded = false setOnClickListener { - clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card)) + if (isLayout(TV)) { + clickCallback.invoke(EpisodeClickEvent(ACTION_SHOW_DESCRIPTION, card)) + } else { + isExpanded = !isExpanded + maxLines = if (isExpanded) { + Integer.MAX_VALUE + } else 4 + } } } - if (!isTrueTv) { + if (isLayout(EMULATOR or PHONE)) { episodePoster.setOnClickListener { clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } @@ -266,7 +275,7 @@ class EpisodeAdapter( clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } - if (isTrueTv) { + if (isLayout(TV)) { itemView.isFocusable = true itemView.isFocusableInTouchMode = true //itemView.touchscreenBlocksFocus = false @@ -291,11 +300,9 @@ class EpisodeAdapter( ) : RecyclerView.ViewHolder(binding.root) { @SuppressLint("SetTextI18n") fun bind(card: ResultEpisode) { - val isTrueTv = isTrueTvSettings() - binding.episodeHolder.layoutParams.apply { width = - if (isTvSettings()) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT + if (isLayout(TV or EMULATOR)) TV_EP_SIZE_SMALL.toPx else ViewGroup.LayoutParams.MATCH_PARENT } binding.apply { @@ -352,7 +359,7 @@ class EpisodeAdapter( clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) } - if (isTrueTv) { + if (isLayout(TV)) { itemView.isFocusable = true itemView.isFocusableInTouchMode = true //itemView.touchscreenBlocksFocus = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt index ca2934ef..7b7bae43 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ImageAdapter.kt @@ -5,7 +5,8 @@ import android.view.ViewGroup import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout /* class ImageAdapter(context: Context, val resource: Int) : ArrayAdapter(context, resource) { @@ -83,7 +84,7 @@ class ImageAdapter( this.nextFocusUpId = nextFocusUp } if (clickCallback != null) { - if (isTrueTvSettings()) { + if (isLayout(TV)) { isClickable = true isLongClickable = true isFocusable = true 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 76066c2e..8d0ca37b 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 @@ -2,9 +2,6 @@ package com.lagradost.cloudstream3.ui.result import android.annotation.SuppressLint import android.app.Dialog -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.Rect @@ -34,7 +31,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog import com.lagradost.cloudstream3.APIHolder import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.CommonActivity -import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent @@ -62,16 +58,15 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.utils.AppUtils.getNameFull -import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant -import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.UIHelper +import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard @@ -688,14 +683,15 @@ open class ResultFragmentPhone : FullScreenPlayer() { resultNextAiringTime.setText(d.nextAiringDate) resultPoster.setImage(d.posterImage) resultPosterBackground.setImage(d.posterBackgroundImage) - resultDescription.setTextHtml(d.plotText) - resultDescription.setOnClickListener { - activity?.let { activity -> - activity.showBottomDialogText( - d.titleText.asString(activity), - d.plotText.asString(activity).html(), - {} - ) + + var isExpanded = false + resultDescription.apply { + setTextHtml(d.plotText) + setOnClickListener { + isExpanded = !isExpanded + maxLines = if (isExpanded) { + Integer.MAX_VALUE + } else 10 } } @@ -758,14 +754,8 @@ open class ResultFragmentPhone : FullScreenPlayer() { resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure resultTitle.setOnLongClickListener { - val titleToCopy = resultTitle.text - val clipboardManager = - activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager? - clipboardManager?.setPrimaryClip(ClipData.newPlainText("Title", titleToCopy)) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - showToast(R.string.copyTitle, Toast.LENGTH_SHORT) - } - return@setOnLongClickListener true + clipboardHelper(txt(R.string.title), resultTitle.text) + true } } } @@ -901,14 +891,6 @@ open class ResultFragmentPhone : FullScreenPlayer() { observe(viewModel.recommendations) { recommendations -> setRecommendations(recommendations, null) } - observe(viewModel.episodeSynopsis) { description -> - activity?.let { activity -> - activity.showBottomDialogText( - activity.getString(R.string.synopsis), - description.html() - ) { viewModel.releaseEpisodeSynopsis() } - } - } context?.let { ctx -> val arrayAdapter = ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) /* 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 85e948c2..3263ee93 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 @@ -39,7 +39,10 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_FOCUSED import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchHelper -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.loadCache @@ -308,9 +311,10 @@ class ResultFragmentTv : Fragment() { resultEpisodesShowButton to resultEpisodesShowText ).forEach { (button , text) -> - button.setOnFocusChangeListener { _, hasFocus -> + button.setOnFocusChangeListener { view, hasFocus -> if (!hasFocus) { text.isSelected = false + if (view.id == R.id.result_episodes_show_button) toggleEpisodes(false) return@setOnFocusChangeListener } @@ -376,10 +380,6 @@ class ResultFragmentTv : Fragment() { resultMetaSite.isFocusable = false - //resultReloadConnectionOpenInBrowser.setOnClickListener {view -> - // view.context?.openBrowser(storedData?.url ?: return@setOnClickListener, fallbackWebview = true) - //} - resultSeasonSelection.setAdapter() resultRangeSelection.setAdapter() resultDubSelection.setAdapter() @@ -457,11 +457,12 @@ class ResultFragmentTv : Fragment() { observeNullable(viewModel.resumeWatching) { resume -> binding?.apply { - // > resultResumeSeries is visible when not null if (resume == null) { - resultResumeSeries.isVisible = false return@observeNullable } + resultResumeSeries.isVisible = true + resultPlayMovie.isVisible = false + resultPlaySeries.isVisible = false // show progress no matter if series or movie resume.progress?.let { progress -> @@ -476,10 +477,6 @@ class ResultFragmentTv : Fragment() { resultResumeProgressHolder.isVisible = false } - resultPlayMovie.isVisible = false - resultPlaySeries.isVisible = false - resultResumeSeries.isVisible = true - focusPlayButton() // Stops last button right focus if it is a movie if (resume.isMovie) @@ -490,7 +487,7 @@ class ResultFragmentTv : Fragment() { resume.isMovie -> context?.getString(R.string.resume) resume.result.season != null -> "${getString(R.string.season_short)}${resume.result.season}:${getString(R.string.episode_short)}${resume.result.episode}" - else -> "${getString(R.string.episode)}${resume.result.episode}" + else -> "${getString(R.string.episode)} ${resume.result.episode}" } resultResumeSeriesButton.setOnClickListener { @@ -603,7 +600,7 @@ class ResultFragmentTv : Fragment() { } observeNullable(viewModel.subscribeStatus) { isSubscribed -> - binding?.resultSubscribe?.isVisible = isSubscribed != null && requireContext().isEmulatorSettings() + binding?.resultSubscribe?.isVisible = isSubscribed != null && isLayout(EMULATOR) binding?.resultSubscribeButton?.apply { if (isSubscribed == null) return@observeNullable @@ -646,15 +643,14 @@ class ResultFragmentTv : Fragment() { } observeNullable(viewModel.movie) { data -> - if (data == null) return@observeNullable + if (data == null ) { + return@observeNullable + } binding?.apply { - resultPlayMovie.isVisible = (data is Resource.Success) && !comingSoon - resultPlaySeries.isVisible = false - resultEpisodesShow.isVisible = false (data as? Resource.Success)?.value?.let { (text, ep) -> - //resultPlayMovieText.setText(text) + resultPlayMovieButton.setOnClickListener { viewModel.handleAction( EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep) @@ -666,14 +662,17 @@ class ResultFragmentTv : Fragment() { ) return@setOnLongClickListener true } - //focusPlayButton() - resultPlayMovieButton.requestFocus() + + resultPlayMovie.isVisible = !comingSoon && resultResumeSeries.isGone + if (comingSoon) + resultBookmarkButton.requestFocus() + else + resultPlayMovieButton.requestFocus() // Stops last button right focus resultSearchButton.nextFocusRightId = R.id.result_search_Button } } - //focusPlayButton() } observeNullable(viewModel.selectPopup) { popup -> @@ -754,16 +753,19 @@ class ResultFragmentTv : Fragment() { observe(viewModel.recommendations) { recommendations -> setRecommendations(recommendations, null) } - observe(viewModel.episodeSynopsis) { description -> - view.context?.let { ctx -> - val builder: AlertDialog.Builder = - AlertDialog.Builder(ctx, R.style.AlertDialogCustom) - builder.setMessage(description.html()) - .setTitle(R.string.synopsis) - .setOnDismissListener { - viewModel.releaseEpisodeSynopsis() - } - .show() + + if (isLayout(TV)) { + observe(viewModel.episodeSynopsis) { description -> + view.context?.let { ctx -> + val builder: AlertDialog.Builder = + AlertDialog.Builder(ctx, R.style.AlertDialogCustom) + builder.setMessage(description.html()) + .setTitle(R.string.synopsis) + .setOnDismissListener { + viewModel.releaseEpisodeSynopsis() + } + .show() + } } } @@ -774,16 +776,14 @@ class ResultFragmentTv : Fragment() { binding?.apply { - resultPlayMovie.isVisible = false - resultPlaySeries.isVisible = true && !comingSoon - resultEpisodes.isVisible = true && !comingSoon - resultEpisodesShow.isVisible = true && !comingSoon + if (comingSoon) + resultBookmarkButton.requestFocus() // resultEpisodeLoading.isVisible = episodes is Resource.Loading if (episodes is Resource.Success) { val first = episodes.value.firstOrNull() if (first != null) { - resultPlaySeriesText.text = //"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" + resultPlaySeriesText.text = when { first.season != null -> "${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" @@ -805,8 +805,9 @@ class ResultFragmentTv : Fragment() { } if (!hasLoadedEpisodesOnce) { hasLoadedEpisodesOnce = true - focusPlayButton() - resultPlaySeries.requestFocus() + resultPlaySeries.isVisible = resultResumeSeries.isGone && !comingSoon + resultEpisodesShow.isVisible = true && !comingSoon + resultPlaySeriesButton.requestFocus() } } @@ -874,14 +875,25 @@ class ResultFragmentTv : Fragment() { resultNextAiring.setText(d.nextAiringEpisode) resultNextAiringTime.setText(d.nextAiringDate) resultPoster.setImage(d.posterImage) - resultDescription.setTextHtml(d.plotText) - resultDescription.setOnClickListener { view -> - view.context?.let { ctx -> - val builder: AlertDialog.Builder = - AlertDialog.Builder(ctx, R.style.AlertDialogCustom) - builder.setMessage(d.plotText.asString(ctx).html()) - .setTitle(d.plotHeaderText.asString(ctx)) - .show() + + var isExpanded = false + resultDescription.apply { + setTextHtml(d.plotText) + setOnClickListener { + if (isLayout(EMULATOR)) { + isExpanded = !isExpanded + maxLines = if (isExpanded) { + Integer.MAX_VALUE + } else 10 + } else { + view.context?.let { ctx -> + val builder: AlertDialog.Builder = + AlertDialog.Builder(ctx, R.style.AlertDialogCustom) + builder.setMessage(d.plotText.asString(ctx).html()) + .setTitle(d.plotHeaderText.asString(ctx)) + .show() + } + } } } @@ -904,9 +916,6 @@ class ResultFragmentTv : Fragment() { ) comingSoon = d.comingSoon resultTvComingSoon.isVisible = d.comingSoon - resultPlayMovie.isGone = d.comingSoon - resultPlaySeries.isGone = d.comingSoon - resultDataHolder.isGone = d.comingSoon UIHelper.populateChips(resultTag, d.tags) resultCastItems.isGone = d.actors.isNullOrEmpty() 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 a05b4059..c90e01d0 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 @@ -81,12 +81,12 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData +import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate import kotlinx.coroutines.* import java.io.File import java.util.concurrent.TimeUnit - /** This starts at 1 */ data class EpisodeRange( // used to index data @@ -928,15 +928,20 @@ class ResultViewModel2 : ViewModel() { ) { val isSubscribed = _subscribeStatus.value ?: return val response = currentResponse ?: return - if (response !is EpisodeResponse) return - val currentId = currentId ?: return + // This might be a bit confusing, but even if the loadresponse is not a EpisodeResponse + // _subscribeStatus might be true. + if (isSubscribed) { removeSubscribedData(currentId) statusChangedCallback?.invoke(false) - _subscribeStatus.postValue(false) + _subscribeStatus.postValue(if (response is EpisodeResponse) false else null) + MainActivity.reloadLibraryEvent(true) } else { + if (response !is EpisodeResponse) { + return + } checkAndWarnDuplicates( context, LibraryListType.SUBSCRIPTIONS, @@ -981,8 +986,8 @@ class ResultViewModel2 : ViewModel() { ) _subscribeStatus.postValue(true) - statusChangedCallback?.invoke(true) + MainActivity.reloadLibraryEvent(true) } } } @@ -1693,14 +1698,8 @@ class ResultViewModel2 : ViewModel() { LoadType.ExternalApp, txt(R.string.episode_action_copy_link) ) { (result, index) -> - val act = activity ?: return@acquireSingleLink - val serviceClipboard = - (act.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?) - ?: return@acquireSingleLink val link = result.links[index] - val clip = ClipData.newPlainText(link.name, link.url) - serviceClipboard.setPrimaryClip(clip) - showToast(R.string.copy_link_toast, Toast.LENGTH_SHORT) + clipboardHelper(txt(link.name), link.url) } } @@ -2052,12 +2051,15 @@ class ResultViewModel2 : ViewModel() { } private fun postSubscription(loadResponse: LoadResponse) { + val id = loadResponse.getId() + val data = getSubscribedData(id) if (loadResponse.isEpisodeBased()) { - val id = loadResponse.getId() - val data = getSubscribedData(id) updateSubscribedData(id, data, loadResponse as? EpisodeResponse) - val isSubscribed = data != null - _subscribeStatus.postValue(isSubscribed) + _subscribeStatus.postValue(data != null) + } + // lets say that we have subscribed, then we must be able to unsubscribe no matter what + else if (data != null) { + _subscribeStatus.postValue(true) } } @@ -2591,6 +2593,7 @@ class ResultViewModel2 : ViewModel() { override var posterHeaders: Map? = null, override var backgroundPosterUrl: String? = null, override var contentRating: String? = null, + val id : Int?, ) : LoadResponse fun loadSmall(activity: Activity?, searchResponse : SearchResponse) = ioSafe { @@ -2600,7 +2603,7 @@ class ResultViewModel2 : ViewModel() { val api = APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull(searchResponse.url) ?: APIRepository.noneApi val repo = APIRepository(api) val response = LoadResponseFromSearch(name = searchResponse.name, url = searchResponse.url, apiName = api.name, type = searchResponse.type ?: TvType.Others, - posterUrl = searchResponse.posterUrl).apply { + posterUrl = searchResponse.posterUrl, id = searchResponse.id).apply { if (searchResponse is SyncAPI.LibraryItem) { this.plot = searchResponse.plot this.rating = searchResponse.personalRating?.times(100) ?: searchResponse.rating @@ -2612,12 +2615,14 @@ class ResultViewModel2 : ViewModel() { this.tags = searchResponse.tags } } - val mainId = searchResponse.id ?: response.getId() + val mainId = response.getId() postSuccessful( loadResponse = response, mainId = mainId, - apiRepository = repo, updateEpisodes = false, updateFillers = false) + apiRepository = repo, + updateEpisodes = false, + updateFillers = false) } fun load( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt index 6fe45730..5a23bfc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/SelectAdaptor.kt @@ -6,7 +6,8 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.lagradost.cloudstream3.databinding.ResultSelectionBinding -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout typealias SelectData = Pair @@ -72,8 +73,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter Unit ) { - val isTrueTv = isTrueTvSettings() - if (isTrueTv) { + if (isLayout(TV)) { item.isFocusable = true item.isFocusableInTouchMode = true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt index 24d56897..0e8160db 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/UiText.kt @@ -19,6 +19,13 @@ sealed class UiText { data class DynamicString(val value: String) : UiText() { override fun toString(): String = value + + override fun equals(other: Any?): Boolean { + if (other !is DynamicString) return false + return this.value == other.value + } + + override fun hashCode(): Int = value.hashCode() } class StringResource( @@ -27,6 +34,16 @@ sealed class UiText { ) : UiText() { override fun toString(): String = "resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}" + override fun equals(other: Any?): Boolean { + if (other !is StringResource) return false + return this.resId == other.resId && this.args == other.args + } + + override fun hashCode(): Int { + var result = resId + result = 31 * result + args.hashCode() + return result + } } fun asStringNull(context: Context?): String? { 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 243d9f4e..24e87d30 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 @@ -46,6 +46,7 @@ import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.APIRepository +import com.lagradost.cloudstream3.ui.BaseAdapter import com.lagradost.cloudstream3.ui.home.HomeFragment import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan @@ -54,8 +55,9 @@ import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.updateChips import com.lagradost.cloudstream3.ui.home.ParentItemAdapter import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus @@ -107,13 +109,16 @@ class SearchFragment : Fragment() { ) bottomSheetDialog?.ownShow() - val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search - val root = inflater.inflate(layout, container, false) - // TODO TRYCATCH - binding = FragmentSearchBinding.bind(root) + binding = try { + val layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_search_tv else R.layout.fragment_search + val root = inflater.inflate(layout, container, false) + FragmentSearchBinding.bind(root) + } catch (t : Throwable) { + FragmentSearchBinding.inflate(inflater) + } - return root + return binding?.root } private fun fixGrid() { @@ -157,7 +162,8 @@ class SearchFragment : Fragment() { **/ fun search(query: String?) { if (query == null) return - + // don't resume state from prev search + (binding?.searchMasterRecycler?.adapter as? BaseAdapter<*,*>)?.clear() context?.let { ctx -> val default = enumValues().sorted().filter { it != TvType.NSFW } .map { it.ordinal.toString() }.toSet() @@ -369,7 +375,7 @@ class SearchFragment : Fragment() { selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList() - if (isTrueTvSettings()) { + if (isLayout(TV)) { binding?.searchFilter?.isFocusable = true binding?.searchFilter?.isFocusableInTouchMode = true } @@ -502,8 +508,8 @@ class SearchFragment : Fragment() { }*/ //main_search.onActionViewExpanded()*/ - val masterAdapter: RecyclerView.Adapter = - ParentItemAdapter(mutableListOf(), { callback -> + val masterAdapter = + ParentItemAdapter(fragment = this, id = "masterAdapter".hashCode(), { callback -> SearchHelper.handleSearchClickCallback(callback) }, { item -> bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt index 3e33e01a..5b943105 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchHelper.kt @@ -1,6 +1,5 @@ package com.lagradost.cloudstream3.ui.search -import android.app.Activity import android.widget.Toast import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.showToast @@ -10,7 +9,8 @@ import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.PHONE +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper @@ -56,7 +56,7 @@ object SearchHelper { } } SEARCH_ACTION_SHOW_METADATA -> { - if(!isTvSettings()) { // we only want this on phone as UI is not done yet on tv + if(isLayout(PHONE)) { // we only want this on phone as UI is not done yet on tv (activity as? MainActivity?)?.apply { loadPopup(callback.card) } ?: kotlin.run { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt index e1b72b30..d18c0197 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/search/SearchResultBuilder.kt @@ -17,7 +17,8 @@ import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.syncproviders.SyncAPI -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.getNameFull import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual @@ -164,7 +165,7 @@ object SearchResultBuilder { bg.isFocusable = false bg.isFocusableInTouchMode = false - if(!isTrueTvSettings()) { + if(!isLayout(TV)) { bg.setOnClickListener { click(it) } @@ -207,7 +208,7 @@ object SearchResultBuilder { */ - if (isTrueTvSettings()) { + if (isLayout(TV)) { // bg.isFocusable = true // bg.isFocusableInTouchMode = true // bg.touchscreenBlocksFocus = false diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt new file mode 100644 index 00000000..aa513d87 --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/Globals.kt @@ -0,0 +1,56 @@ +package com.lagradost.cloudstream3.ui.settings + +import android.app.UiModeManager +import android.content.Context +import android.content.res.Configuration +import android.os.Build +import androidx.preference.PreferenceManager +import com.lagradost.cloudstream3.R + +object Globals { + var beneneCount = 0 + + const val PHONE : Int = 0b001 + const val TV : Int = 0b010 + const val EMULATOR : Int = 0b100 + private const val INVALID = -1 + private var layoutId = INVALID + + private fun Context.getLayoutInt(): Int { + val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) + return settingsManager.getInt(this.getString(R.string.app_layout_key), -1) + } + + private fun Context.isAutoTv(): Boolean { + val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? + // AFT = Fire TV + val model = Build.MODEL.lowercase() + return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains( + "AFT" + ) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast") + } + + private fun Context.layoutIntCorrected(): Int { + return when(getLayoutInt()) { + -1 -> if (isAutoTv()) TV else PHONE + 0 -> PHONE + 1 -> TV + 2 -> EMULATOR + else -> PHONE + } + } + + fun Context.updateTv() { + layoutId = layoutIntCorrected() + } + + /** Returns true if the layout is any of the flags, + * so isLayout(TV or EMULATOR) is a valid statement for checking if the layout is in the emulator + * or tv. Auto will become the "TV" or the "PHONE" layout. + * + * Valid flags are: PHONE, TV, EMULATOR + * */ + fun isLayout(flags: Int) : Boolean { + return (layoutId and flags) != 0 + } +} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt index 14f11c1e..298431ee 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsAccount.kt @@ -29,8 +29,10 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklAp import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.OAuth2API +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -76,7 +78,7 @@ class SettingsAccount : PreferenceFragmentCompat() { showAccountSwitch(activity, api) } - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { binding.accountSwitchAccount.requestFocus() } } @@ -140,7 +142,7 @@ class SettingsAccount : PreferenceFragmentCompat() { binding.loginUsernameInput to api.requiresUsername ) - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { visibilityMap.forEach { (input, isVisible) -> input.isVisible = isVisible 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 8dedd896..72e22269 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 @@ -1,9 +1,5 @@ package com.lagradost.cloudstream3.ui.settings -import android.app.UiModeManager -import android.content.Context -import android.content.res.Configuration -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -16,16 +12,20 @@ import androidx.core.view.updateLayoutParams import androidx.fragment.app.Fragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import androidx.preference.PreferenceManager import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar -import com.lagradost.cloudstream3.AcraApplication.Companion.context +import com.lagradost.cloudstream3.BuildConfig import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.ui.home.HomeFragment -import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.utils.UIHelper +import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.toPx @@ -33,10 +33,6 @@ import java.io.File class SettingsFragment : Fragment() { companion object { - var beneneCount = 0 - - private var isTv: Boolean = false - private var isTrueTv: Boolean = false fun PreferenceFragmentCompat?.getPref(id: Int): Preference? { if (this == null) return null @@ -53,12 +49,12 @@ class SettingsFragment : Fragment() { * On TV you cannot properly scroll to the bottom of settings, this fixes that. * */ fun PreferenceFragmentCompat.setPaddingBottom() { - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { listView?.setPadding(0, 0, 0, 100.toPx) } } fun PreferenceFragmentCompat.setToolBarScrollFlags() { - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { val settingsAppbar = view?.findViewById(R.id.settings_toolbar) settingsAppbar?.updateLayoutParams { @@ -67,7 +63,7 @@ class SettingsFragment : Fragment() { } } fun Fragment?.setToolBarScrollFlags() { - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { val settingsAppbar = this?.view?.findViewById(R.id.settings_toolbar) settingsAppbar?.updateLayoutParams { @@ -86,7 +82,7 @@ class SettingsFragment : Fragment() { activity?.onBackPressedDispatcher?.onBackPressed() } } - fixPaddingStatusbar(settingsToolbar) + UIHelper.fixPaddingStatusbar(settingsToolbar) } fun Fragment?.setUpToolbar(@StringRes title: Int) { @@ -101,7 +97,7 @@ class SettingsFragment : Fragment() { activity?.onBackPressedDispatcher?.onBackPressed() } } - fixPaddingStatusbar(settingsToolbar) + UIHelper.fixPaddingStatusbar(settingsToolbar) } fun getFolderSize(dir: File): Long { @@ -117,60 +113,7 @@ class SettingsFragment : Fragment() { return size } - - private fun Context.getLayoutInt(): Int { - val settingsManager = PreferenceManager.getDefaultSharedPreferences(this) - return settingsManager.getInt(this.getString(R.string.app_layout_key), -1) - } - - private fun Context.isTvSettings(): Boolean { - var value = getLayoutInt() - if (value == -1) { - value = if (isAutoTv()) 1 else 0 - } - return value == 1 || value == 2 - } - - private fun Context.isTrueTvSettings(): Boolean { - var value = getLayoutInt() - if (value == -1) { - value = if (isAutoTv()) 1 else 0 - } - return value == 1 - } - - fun Context.updateTv() { - isTrueTv = isTrueTvSettings() - isTv = isTvSettings() - } - - fun isTrueTvSettings(): Boolean { - return isTrueTv - } - - fun isTvSettings(): Boolean { - return isTv - } - - fun Context.isEmulatorSettings(): Boolean { - return getLayoutInt() == 2 - } - - // phone exclusive - fun isTruePhone(): Boolean { - return !isTrueTvSettings() && !isTvSettings() && context?.isEmulatorSettings() != true - } - - private fun Context.isAutoTv(): Boolean { - val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager? - // AFT = Fire TV - val model = Build.MODEL.lowercase() - return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains( - "AFT" - ) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast") - } } - override fun onDestroyView() { binding = null super.onDestroyView() @@ -195,8 +138,6 @@ class SettingsFragment : Fragment() { // used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}") - val isTrueTv = isTrueTvSettings() - for (syncApi in accountManagers) { val login = syncApi.loginInfo() val pic = login?.profilePicture ?: continue @@ -224,7 +165,7 @@ class SettingsFragment : Fragment() { setOnClickListener { navigate(navigationId) } - if (isTrueTv) { + if (isLayout(TV)) { isFocusable = true isFocusableInTouchMode = true } @@ -232,9 +173,20 @@ class SettingsFragment : Fragment() { } // Default focus on TV - if (isTrueTv) { + if (isLayout(TV)) { settingsGeneral.requestFocus() } } + + val appVersion = getString(R.string.app_version) + val commitInfo = getString(R.string.commit_hash) + val buildDate = BuildConfig.BUILDDATE + + binding?.buildDate?.text = buildDate + + binding?.appVersionInfo?.setOnLongClickListener{ + clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo") + true + } } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt index abd7fb9a..6cf00375 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsGeneral.kt @@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.ui.EasterEggMonke +import com.lagradost.cloudstream3.ui.settings.Globals.beneneCount 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.setToolBarScrollFlags @@ -378,30 +379,30 @@ class SettingsGeneral : PreferenceFragmentCompat() { } try { - SettingsFragment.beneneCount = + beneneCount = settingsManager.getInt(getString(R.string.benene_count), 0) getPref(R.string.benene_count)?.let { pref -> pref.summary = - if (SettingsFragment.beneneCount <= 0) getString(R.string.benene_count_text_none) else getString( + if (beneneCount <= 0) getString(R.string.benene_count_text_none) else getString( R.string.benene_count_text ).format( - SettingsFragment.beneneCount + beneneCount ) pref.setOnPreferenceClickListener { try { - SettingsFragment.beneneCount++ - if (SettingsFragment.beneneCount%20 == 0) { + beneneCount++ + if (beneneCount%20 == 0) { val intent = Intent(context, EasterEggMonke::class.java) startActivity(intent) } settingsManager.edit().putInt( getString(R.string.benene_count), - SettingsFragment.beneneCount + beneneCount ) .apply() it.summary = - getString(R.string.benene_count_text).format(SettingsFragment.beneneCount) + getString(R.string.benene_count_text).format(beneneCount) } catch (e: Exception) { logError(e) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt index 63053236..cc14e761 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/SettingsUI.kt @@ -9,11 +9,11 @@ import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.search.SearchResultBuilder +import com.lagradost.cloudstream3.ui.settings.Globals.updateTv 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.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog 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 9f72c1d5..fb24c185 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 @@ -1,10 +1,6 @@ package com.lagradost.cloudstream3.ui.settings -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context import android.os.Bundle -import android.os.TransactionTooLargeException import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -20,6 +16,7 @@ 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.result.txt 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.setToolBarScrollFlags @@ -30,6 +27,7 @@ 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.clipboardHelper import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.VideoDownloadManager @@ -117,22 +115,15 @@ class SettingsUpdates : PreferenceFragmentCompat() { binding.text1.text = text binding.copyBtt.setOnClickListener { - // Can crash on too much text - try { - val serviceClipboard = - (activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?) - ?: return@setOnClickListener - val clip = ClipData.newPlainText("logcat", text) - serviceClipboard.setPrimaryClip(clip) - dialog.dismissSafe(activity) - } catch (e: TransactionTooLargeException) { - showToast(R.string.clipboard_too_large) - } + clipboardHelper(txt("Logcat"), text) + dialog.dismissSafe(activity) } + binding.clearBtt.setOnClickListener { Runtime.getRuntime().exec("logcat -c") dialog.dismissSafe(activity) } + binding.saveBtt.setOnClickListener { var fileStream: OutputStream? = null try { @@ -153,9 +144,11 @@ class SettingsUpdates : PreferenceFragmentCompat() { fileStream?.closeQuietly() } } + binding.closeBtt.setOnClickListener { dialog.dismissSafe(activity) } + return@setOnPreferenceClickListener true } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt index f0b8a0bd..ebd3260f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/ExtensionsFragment.kt @@ -29,7 +29,8 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.result.setText -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog @@ -97,7 +98,7 @@ class ExtensionsFragment : Fragment() { nextLeft = R.id.nav_rail_view ) - if (!isTrueTvSettings()) + if (!isLayout(TV)) binding?.addRepoButton?.let { button -> button.post { setPadding( @@ -286,7 +287,7 @@ class ExtensionsFragment : Fragment() { } } - val isTv = isTrueTvSettings() + val isTv = isLayout(TV) binding?.apply { addRepoButton.isGone = isTv addRepoButtonImageviewHolder.isVisible = isTv diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt index c3fb4fc2..04da30c7 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginAdapter.kt @@ -17,7 +17,8 @@ import com.lagradost.cloudstream3.plugins.PluginManager import com.lagradost.cloudstream3.plugins.VotingApi.getVotes import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.txt -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.AppUtils.html import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.main @@ -44,7 +45,7 @@ class PluginAdapter( private val plugins: MutableList = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item + val layout = if(isLayout(TV)) R.layout.repository_item_tv else R.layout.repository_item val inflated = LayoutInflater.from(parent.context).inflate(layout, parent, false) return PluginViewHolder( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt index c5256ffa..acfbc584 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/PluginsFragment.kt @@ -17,7 +17,9 @@ import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.setLinearListLayout -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.settings.appLanguages @@ -155,7 +157,7 @@ class PluginsFragment : Fragment() { pluginViewModel.handlePluginAction(activity, url, it, isLocal) } - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { // Scrolling down does not reveal the whole RecyclerView on TV, add to bypass that. binding?.pluginRecyclerView?.setPadding(0, 0, 0, 200.toPx) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt index 7ac7cbb2..faf6d38b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/extensions/RepoAdapter.kt @@ -1,22 +1,18 @@ package com.lagradost.cloudstream3.ui.settings.extensions -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.os.Build import android.view.LayoutInflater import android.view.ViewGroup -import android.widget.Toast import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import androidx.viewbinding.ViewBinding -import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout +import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper class RepoAdapter( val isSetup: Boolean, @@ -28,7 +24,7 @@ class RepoAdapter( private val repositories: MutableList = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val layout = if (isTrueTvSettings()) RepositoryItemTvBinding.inflate( + val layout = if (isLayout(TV)) RepositoryItemTvBinding.inflate( LayoutInflater.from(parent.context), parent, false @@ -121,13 +117,9 @@ class RepoAdapter( } repositoryItemRoot.setOnLongClickListener { - val clipboardManager = - activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager? - clipboardManager?.setPrimaryClip(ClipData.newPlainText("RepoUrl", repositoryData.url)) - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { - showToast(R.string.copyRepoUrl, Toast.LENGTH_SHORT) - } - return@setOnLongClickListener true + val shareableRepoData = "${repositoryData.name} : \n ${repositoryData.url}" + clipboardHelper(txt(R.string.repo_copy_label), shareableRepoData) + true } mainText.text = repositoryData.name diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt index 3fbd1131..7878afaa 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/settings/testing/TestFragment.kt @@ -11,7 +11,8 @@ import com.lagradost.cloudstream3.databinding.FragmentTestingBinding import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observeNullable -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar @@ -62,7 +63,7 @@ class TestFragment : Fragment() { } } - if (isTrueTvSettings()) { + if (isLayout(TV)) { providerTest.playPauseButton?.isFocusableInTouchMode = true providerTest.playPauseButton?.requestFocus() } @@ -75,7 +76,7 @@ class TestFragment : Fragment() { fun focusRecyclerView() { // Hack to make it possible to focus the recyclerview. - if (isTrueTvSettings()) { + if (isLayout(TV)) { providerTestRecyclerView.requestFocus() providerTestAppbar.setExpanded(false, true) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt index 71fac2ed..bb9558b8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/ChromecastSubtitlesFragment.kt @@ -13,8 +13,8 @@ import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import androidx.fragment.app.Fragment -import com.fasterxml.jackson.annotation.JsonProperty import androidx.media3.common.text.Cue +import com.fasterxml.jackson.annotation.JsonProperty import com.google.android.gms.cast.TextTrackStyle import com.google.android.gms.cast.TextTrackStyle.* import com.jaredrummler.android.colorpicker.ColorPickerDialog @@ -24,7 +24,9 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.ChromecastSubtitleSettingsBinding -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -173,7 +175,7 @@ class ChromecastSubtitlesFragment : Fragment() { state = getCurrentSavedStyle() context?.updateState() - val isTvSettings = isTvSettings() + val isTvSettings = isLayout(TV or EMULATOR) fun View.setFocusableInTv() { this.isFocusableInTouchMode = isTvSettings diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt index 83521873..1466afed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/subtitles/SubtitlesFragment.kt @@ -28,7 +28,9 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.DataStore.setKey import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog @@ -252,7 +254,7 @@ class SubtitlesFragment : Fragment() { state = getCurrentSavedStyle() context?.updateState() - val isTvTrueSettings = isTrueTvSettings() + val isTvTrueSettings = isLayout(TV) fun View.setFocusableInTv() { this.isFocusableInTouchMode = isTvTrueSettings diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt index 1be966b6..ff27b192 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/AppUtils.kt @@ -61,8 +61,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStri import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.ui.WebviewFragment import com.lagradost.cloudstream3.ui.result.ResultFragment -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Companion.downloadAll import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.utils.Coroutines.ioSafe @@ -78,7 +77,6 @@ import okhttp3.Cache import java.io.* import java.net.URL import java.net.URLDecoder -import kotlin.system.measureTimeMillis object AppUtils { fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) { @@ -583,7 +581,7 @@ object AppUtils { //private val viewModel: ResultViewModel by activityViewModels() private fun getResultsId(): Int { - return if (isTvSettings()) { + return if (Globals.isLayout(Globals.TV or Globals.EMULATOR)) { R.id.global_to_navigation_results_tv } else { R.id.global_to_navigation_results_phone @@ -707,7 +705,7 @@ object AppUtils { * Sets the focus to the negative button when in TV and Emulator layout. **/ fun AlertDialog.setDefaultFocus(buttonFocus: Int = DialogInterface.BUTTON_NEGATIVE) { - if (!isTvSettings()) return + if (!Globals.isLayout(Globals.TV or Globals.EMULATOR)) return this.getButton(buttonFocus).run { isFocusableInTouchMode = true requestFocus() 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 d3c97cd1..87d17a2b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/BackupUtils.kt @@ -32,7 +32,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.mapper -import com.lagradost.cloudstream3.utils.DataStore.setKeyRaw import com.lagradost.cloudstream3.utils.UIHelper.checkWrite import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.VideoDownloadManager.setupStream @@ -256,8 +255,12 @@ object BackupUtils { map: Map?, isEditingAppSettings: Boolean = false ) { - map?.filter { it.key.isTransferable() }?.forEach { - setKeyRaw(it.key, it.value, isEditingAppSettings) + val editor = DataStore.editor(this, isEditingAppSettings) + map?.forEach { + if (it.key.isTransferable()) { + editor.setKeyRaw(it.key, it.value) + } } + editor.apply() } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt index 01a64d7d..19c817b9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/DataStore.kt @@ -50,6 +50,28 @@ class PreferenceDelegate( } } +/** When inserting many keys use this function, this is because apply for every key is very expensive on memory */ +data class Editor( + val editor : SharedPreferences.Editor +) { + /** Always remember to call apply after */ + fun setKeyRaw(path: String, value: T) { + when (value) { + is Boolean -> editor.putBoolean(path, value) + is Int -> editor.putInt(path, value) + is String -> editor.putString(path, value) + is Float -> editor.putFloat(path, value) + is Long -> editor.putLong(path, value) + (value as? Set != null) -> editor.putStringSet(path, value as Set) + } + } + + fun apply() { + editor.apply() + System.gc() + } +} + object DataStore { val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule()) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() @@ -66,22 +88,10 @@ object DataStore { return "${folder}/${path}" } - fun Context.setKeyRaw(path: String, value: T, isEditingAppSettings: Boolean = false) { - try { - val editor: SharedPreferences.Editor = - if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit() - when (value) { - is Boolean -> editor.putBoolean(path, value) - is Int -> editor.putInt(path, value) - is String -> editor.putString(path, value) - is Float -> editor.putFloat(path, value) - is Long -> editor.putLong(path, value) - (value as? Set != null) -> editor.putStringSet(path, value as Set) - } - editor.apply() - } catch (e: Exception) { - logError(e) - } + fun editor(context : Context, isEditingAppSettings: Boolean = false) : Editor { + val editor: SharedPreferences.Editor = + if (isEditingAppSettings) context.getDefaultSharedPrefs().edit() else context.getSharedPrefs().edit() + return Editor(editor) } fun Context.getDefaultSharedPrefs(): SharedPreferences { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt index f34e7238..70edf80c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/SingleSelectionHelper.kt @@ -21,7 +21,9 @@ import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding import com.lagradost.cloudstream3.databinding.OptionsPopupTvBinding -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.TV +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.setImage @@ -54,7 +56,7 @@ object SingleSelectionHelper { ) { if (this == null) return - if (isTvSettings()) { + if (isLayout(TV or EMULATOR)) { val binding = OptionsPopupTvBinding.inflate(layoutInflater) val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom) .setView(binding.root) 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 6e925d15..eedb626a 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/UIHelper.kt @@ -5,6 +5,8 @@ import android.annotation.SuppressLint import android.app.Activity import android.app.AppOpsManager import android.app.Dialog +import android.content.ClipData +import android.content.ClipboardManager import android.content.Context import android.content.pm.PackageManager import android.content.res.Configuration @@ -14,12 +16,15 @@ import android.graphics.Color import android.graphics.drawable.Drawable import android.os.Build import android.os.Bundle +import android.os.TransactionTooLargeException +import android.util.Log import android.view.* import android.view.ViewGroup.MarginLayoutParams import android.view.inputmethod.InputMethodManager import android.widget.ImageView import android.widget.ListAdapter import android.widget.ListView +import android.widget.Toast.LENGTH_LONG import androidx.annotation.AttrRes import androidx.annotation.ColorInt import androidx.annotation.DrawableRes @@ -30,14 +35,12 @@ import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.widget.PopupMenu import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService import androidx.core.graphics.alpha import androidx.core.graphics.blue import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.graphics.green import androidx.core.graphics.red -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat import androidx.core.view.marginBottom import androidx.core.view.marginLeft import androidx.core.view.marginRight @@ -58,17 +61,20 @@ import com.bumptech.glide.request.target.Target import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipGroup +import com.lagradost.cloudstream3.AcraApplication.Companion.context import com.lagradost.cloudstream3.CommonActivity.activity -import com.lagradost.cloudstream3.MainActivity +import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.ui.result.UiImage -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings -import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings +import com.lagradost.cloudstream3.ui.result.UiText +import com.lagradost.cloudstream3.ui.result.txt +import com.lagradost.cloudstream3.ui.settings.Globals +import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR +import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import jp.wasabeef.glide.transformations.BlurTransformation import kotlin.math.roundToInt - object UIHelper { val Int.toPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt() val Float.toPx: Float get() = (this * Resources.getSystem().displayMetrics.density) @@ -123,6 +129,35 @@ object UIHelper { ) } + fun clipboardHelper(label: UiText, text: CharSequence) { + val ctx = context ?: return + try { + ctx.let { + val clip = ClipData.newPlainText(label.asString(ctx), text) + val labelSuffix = txt(R.string.toast_copied).asString(ctx) + ctx.getSystemService()?.setPrimaryClip(clip) + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + showToast("${label.asString(ctx)} $labelSuffix") + } + } + } catch (t: Throwable) { + Log.e("ClipboardService", "$t") + when (t) { + is SecurityException -> { + showToast(R.string.clipboard_permission_error) + } + + is TransactionTooLargeException -> { + showToast(R.string.clipboard_too_large) + } + + else -> { + showToast(R.string.clipboard_unknown_error, LENGTH_LONG) + } + } + } + } /** * Sets ListView height dynamically based on the height of the items. @@ -434,7 +469,7 @@ object UIHelper { } fun Context.getStatusBarHeight(): Int { - if (isTvSettings()) { + if (isLayout(Globals.TV or EMULATOR)) { return 0 } @@ -536,7 +571,7 @@ object UIHelper { (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) //} - changeStatusBarState(isEmulatorSettings()) + changeStatusBarState(isLayout(EMULATOR)) } fun Context.shouldShowPIPMode(isInPlayer: Boolean): Boolean { diff --git a/app/src/main/res/layout/bottom_resultview_preview.xml b/app/src/main/res/layout/bottom_resultview_preview.xml index 4a64114e..3372fe7b 100644 --- a/app/src/main/res/layout/bottom_resultview_preview.xml +++ b/app/src/main/res/layout/bottom_resultview_preview.xml @@ -41,22 +41,34 @@ android:layout_marginStart="10dp" android:orientation="vertical"> - + tools:text="The Perfect Run" /> - + - + + + android:layout_height="wrap_content" + android:orientation="horizontal" + android:padding="7dp"> + tools:visibility="visible" /> + tools:visibility="visible" /> diff --git a/app/src/main/res/layout/fragment_result.xml b/app/src/main/res/layout/fragment_result.xml index 045b9cc2..22d2e52f 100644 --- a/app/src/main/res/layout/fragment_result.xml +++ b/app/src/main/res/layout/fragment_result.xml @@ -409,8 +409,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:ellipsize="end" + android:maxLines="10" android:foreground="@drawable/outline_drawable" - android:maxLength="1000" android:nextFocusUp="@id/result_back" android:nextFocusDown="@id/result_bookmark_Button" android:paddingTop="5dp" diff --git a/app/src/main/res/layout/fragment_result_tv.xml b/app/src/main/res/layout/fragment_result_tv.xml index ba8b728e..2ec2ae0a 100644 --- a/app/src/main/res/layout/fragment_result_tv.xml +++ b/app/src/main/res/layout/fragment_result_tv.xml @@ -271,7 +271,9 @@ https://developer.android.com/design/ui/tv/samples/jet-fit android:layout_height="wrap_content" android:layout_marginEnd="8dp" android:id="@+id/result_play_movie" - android:orientation="vertical"> + android:orientation="vertical" + android:visibility="gone" + tools:visibility="visible"> + android:orientation="vertical" + android:visibility="gone" + tools:visibility="visible"> + + + + diff --git a/app/src/main/res/layout/repository_item_tv.xml b/app/src/main/res/layout/repository_item_tv.xml index fbe18199..7ef3b04d 100644 --- a/app/src/main/res/layout/repository_item_tv.xml +++ b/app/src/main/res/layout/repository_item_tv.xml @@ -6,7 +6,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/outline_drawable" - android:nextFocusRight="@id/action_button" + android:nextFocusRight="@id/action_settings" android:orientation="horizontal" android:clickable="true" android:focusable="true" @@ -117,6 +117,9 @@ android:background="@drawable/outline_drawable" android:contentDescription="@string/title_settings" android:visibility="gone" + android:focusable="true" + android:nextFocusLeft="@id/repository_item_root" + android:nextFocusRight="@id/action_button" app:srcCompat="@drawable/ic_baseline_tune_24" tools:visibility="visible" /> @@ -130,7 +133,7 @@ android:clickable="true" android:contentDescription="@string/download" android:focusable="true" - android:nextFocusLeft="@id/repository_item_root" + android:nextFocusLeft="@id/action_settings" android:padding="12dp" tools:src="@drawable/ic_baseline_add_24" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b4f9d14d..b5dae57b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -174,8 +174,8 @@ Close Clear Save - Title copied! - Repo URL copied! + Repository name and URL + copied! New episode notification Search in other extensions Show recommendations @@ -647,6 +647,8 @@ History Show skip popups for opening/ending Too much text. Unable to save to clipboard. + Error accessing Clipboard, Please try again. + Error copying, Please copy logcat and contact app support. Mark as watched Remove from watched Are you sure you want to exit\? diff --git a/build.gradle.kts b/build.gradle.kts index d2959529..06af44d0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ buildscript { google() mavenCentral() } + dependencies { classpath("com.android.tools.build:gradle:8.2.1") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")