Merge branch 'recloudstream:master' into master

This commit is contained in:
Kai Kitching 2024-03-21 13:24:47 -07:00 committed by GitHub
commit 96fa3de474
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
61 changed files with 1628 additions and 1308 deletions

View File

@ -60,7 +60,7 @@ android {
targetSdk = 33 /* Android 14 is Fu*ked targetSdk = 33 /* Android 14 is Fu*ked
^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/ ^ https://developer.android.com/about/versions/14/behavior-changes-14#safer-dynamic-code-loading*/
versionCode = 63 versionCode = 63
versionName = "4.3.1" versionName = "4.3.2"
resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}") resValue("string", "app_version", "${defaultConfig.versionName}${versionNameSuffix ?: ""}")
resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "") resValue("string", "commit_hash", "git rev-parse --short HEAD".execute() ?: "")
@ -163,15 +163,15 @@ dependencies {
// Android Core & Lifecycle // Android Core & Lifecycle
implementation("androidx.core:core-ktx:1.12.0") implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.appcompat:appcompat:1.6.1") 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-livedata-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-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 // Design & UI
implementation("jp.wasabeef:glide-transformations:4.3.0") implementation("jp.wasabeef:glide-transformations:4.3.0")
implementation("androidx.preference:preference-ktx:1.2.1") 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.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")

View File

@ -11,7 +11,9 @@ import androidx.fragment.app.FragmentActivity
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall
import com.lagradost.cloudstream3.plugins.PluginManager 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.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread import com.lagradost.cloudstream3.utils.Coroutines.runOnMainThread
import com.lagradost.cloudstream3.utils.DataStore.getKey import com.lagradost.cloudstream3.utils.DataStore.getKey
@ -31,7 +33,6 @@ import org.acra.sender.ReportSenderFactory
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.PrintStream import java.io.PrintStream
import java.lang.Exception
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.concurrent.thread import kotlin.concurrent.thread
import kotlin.system.exitProcess import kotlin.system.exitProcess
@ -211,7 +212,7 @@ class AcraApplication : Application() {
fun openBrowser(url: String, activity: FragmentActivity?) { fun openBrowser(url: String, activity: FragmentActivity?) {
openBrowser( openBrowser(
url, url,
isTvSettings(), isLayout(TV or EMULATOR),
activity?.supportFragmentManager?.fragments?.lastOrNull() activity?.supportFragmentManager?.fragments?.lastOrNull()
) )
} }

View File

@ -11,11 +11,9 @@ import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.view.Gravity import android.view.Gravity
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.NO_ID import android.view.View.NO_ID
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts 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.google.android.material.navigationrail.NavigationRailView
import com.lagradost.cloudstream3.AcraApplication.Companion.getKey import com.lagradost.cloudstream3.AcraApplication.Companion.getKey
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
import com.lagradost.cloudstream3.databinding.ToastBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.player.PlayerEventType import com.lagradost.cloudstream3.ui.player.PlayerEventType
import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.result.UiText 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.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
@ -99,8 +98,7 @@ object CommonActivity {
var playerEventListener: ((PlayerEventType) -> Unit)? = null var playerEventListener: ((PlayerEventType) -> Unit)? = null
var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null var keyEventListener: ((Pair<KeyEvent?, Boolean>) -> Boolean)? = null
private var currentToast: Toast? = null
var currentToast: Toast? = null
fun showToast(@StringRes message: Int, duration: Int? = null) { fun showToast(@StringRes message: Int, duration: Int? = null) {
val act = activity ?: return val act = activity ?: return
@ -156,25 +154,19 @@ object CommonActivity {
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }
try { try {
val inflater = val binding = ToastBinding.inflate(act.layoutInflater)
act.getSystemService(AppCompatActivity.LAYOUT_INFLATER_SERVICE) as LayoutInflater binding.text.text = message.trim()
val layout: View = inflater.inflate(
R.layout.toast,
act.findViewById<View>(R.id.toast_layout_root) as ViewGroup?
)
val text = layout.findViewById(R.id.text) as TextView
text.text = message.trim()
// custom toasts are deprecated and won't appear when cs3 sets minSDK to api30 (A11)
val toast = Toast(act) val toast = Toast(act)
toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
toast.duration = duration ?: Toast.LENGTH_SHORT toast.duration = duration ?: Toast.LENGTH_SHORT
toast.view = layout toast.setGravity(Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM, 0, 5.toPx)
//https://github.com/PureWriter/ToastCompat toast.view = binding.root
toast.show()
currentToast = toast currentToast = toast
toast.show()
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }

View File

@ -18,6 +18,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklAp
import com.lagradost.cloudstream3.syncproviders.SyncIdName import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.syncproviders.providers.SimklApi import com.lagradost.cloudstream3.syncproviders.providers.SimklApi
import com.lagradost.cloudstream3.ui.player.SubtitleData import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.mainWork import com.lagradost.cloudstream3.utils.Coroutines.mainWork
@ -119,7 +120,8 @@ object APIHolder {
} }
fun LoadResponse.getId(): Int { 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)
} }
/** /**

View File

@ -86,6 +86,7 @@ import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins import com.lagradost.cloudstream3.plugins.PluginManager.loadAllOnlinePlugins
import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin import com.lagradost.cloudstream3.plugins.PluginManager.loadSinglePlugin
import com.lagradost.cloudstream3.receivers.VideoDownloadRestartReceiver 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.OAuth2Apis
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appString 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.result.txt
import com.lagradost.cloudstream3.ui.search.SearchFragment import com.lagradost.cloudstream3.ui.search.SearchFragment
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTruePhone import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
import com.lagradost.cloudstream3.ui.settings.SettingsGeneral import com.lagradost.cloudstream3.ui.settings.SettingsGeneral
import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY import com.lagradost.cloudstream3.ui.setup.HAS_DONE_SETUP_KEY
import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions import com.lagradost.cloudstream3.ui.setup.SetupFragmentExtensions
@ -290,7 +291,8 @@ var app = Requests(responseParser = object : ResponseParser {
defaultHeaders = mapOf("user-agent" to USER_AGENT) defaultHeaders = mapOf("user-agent" to USER_AGENT)
} }
class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAuthenticator.BiometricAuthCallback { class MainActivity : AppCompatActivity(), ColorPickerDialogListener,
BiometricAuthenticator.BiometricAuthCallback {
companion object { companion object {
const val TAG = "MAINACT" const val TAG = "MAINACT"
const val ANIMATED_OUTLINE: Boolean = false 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 // kinda shitty solution, but cant com main->home otherwise for popups
val bookmarksUpdatedEvent = Event<Boolean>() val bookmarksUpdatedEvent = Event<Boolean>()
/** /**
* Used by DataStoreHelper to fully reload home when switching accounts * Used by DataStoreHelper to fully reload home when switching accounts
*/ */
val reloadHomeEvent = Event<Boolean>() val reloadHomeEvent = Event<Boolean>()
/** /**
* Used by DataStoreHelper to fully reload library when switching accounts * Used by DataStoreHelper to fully reload library when switching accounts
*/ */
@ -467,7 +471,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
} }
var lastPopup: SearchResponse? = null var lastPopup: SearchResponse? = null
fun loadPopup(result: SearchResponse, load : Boolean = true) { fun loadPopup(result: SearchResponse, load: Boolean = true) {
lastPopup = result lastPopup = result
val syncName = syncViewModel.syncName(result.apiName) val syncName = syncViewModel.syncName(result.apiName)
@ -488,8 +492,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
.contains(DubStatus.Dubbed) .contains(DubStatus.Dubbed)
) DubStatus.Dubbed else DubStatus.Subbed, null ) DubStatus.Dubbed else DubStatus.Subbed, null
) )
}else { } else {
viewModel.loadSmall(this,result) viewModel.loadSmall(this, result)
} }
} }
@ -554,7 +558,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
binding?.navHostFragment?.apply { binding?.navHostFragment?.apply {
val params = layoutParams as ConstraintLayout.LayoutParams val params = layoutParams as ConstraintLayout.LayoutParams
val push = 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()) { if (!this.isLtr()) {
params.setMargins( params.setMargins(
@ -581,7 +585,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
} }
Configuration.ORIENTATION_PORTRAIT -> { Configuration.ORIENTATION_PORTRAIT -> {
isTvSettings() isLayout(TV or EMULATOR)
} }
else -> { else -> {
@ -787,9 +791,10 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
} }
lateinit var viewModel: ResultViewModel2 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*/ /** 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? { override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
viewModel = viewModel =
ViewModelProvider(this)[ResultViewModel2::class.java] ViewModelProvider(this)[ResultViewModel2::class.java]
@ -1105,8 +1110,8 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
} }
} }
private fun centerView(view : View?) { private fun centerView(view: View?) {
if(view == null) return if (view == null) return
try { try {
Log.v(TAG, "centerView: $view") Log.v(TAG, "centerView: $view")
val r = Rect(0, 0, 0, 0) 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 // just in case, MAIN SHOULD *NEVER* BOOT LOOP CRASH
binding = try { binding = try {
if (isTvSettings()) { if (isLayout(TV or EMULATOR)) {
val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false) val newLocalBinding = ActivityMainTvBinding.inflate(layoutInflater, null, false)
setContentView(newLocalBinding.root) setContentView(newLocalBinding.root)
if (isTrueTvSettings() && ANIMATED_OUTLINE) { if (isLayout(TV) && ANIMATED_OUTLINE) {
TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline) TvFocus.focusOutline = WeakReference(newLocalBinding.focusOutline)
newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener { newLocalBinding.root.viewTreeObserver.addOnScrollChangedListener {
TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true) TvFocus.updateFocusView(TvFocus.lastFocus.get(), same = true)
@ -1188,7 +1193,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
newLocalBinding.focusOutline.isVisible = false newLocalBinding.focusOutline.isVisible = false
} }
if(isTrueTvSettings()) { if (isLayout(TV)) {
// Put here any button you don't want focusing it to center the view // Put here any button you don't want focusing it to center the view
val exceptionButtons = listOf( val exceptionButtons = listOf(
R.id.home_preview_play_btt, R.id.home_preview_play_btt,
@ -1205,7 +1210,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
R.id.result_search_Button, R.id.result_search_Button,
R.id.result_episodes_show_button, R.id.result_episodes_show_button,
) )
newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus -> newLocalBinding.root.viewTreeObserver.addOnGlobalFocusChangeListener { _, newFocus ->
if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener if (exceptionButtons.contains(newFocus?.id)) return@addOnGlobalFocusChangeListener
centerView(newFocus) centerView(newFocus)
@ -1223,18 +1228,21 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
null null
} }
changeStatusBarState(isEmulatorSettings()) changeStatusBarState(isLayout(EMULATOR))
/** Biometric stuff for users without accounts **/ /** Biometric stuff for users without accounts **/
val authEnabled = settingsManager.getBoolean(getString(R.string.biometric_key), false) 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)) { if (deviceHasPasswordPinLock(this)) {
startBiometricAuthentication(this, R.string.biometric_authentication_title, false) startBiometricAuthentication(this, R.string.biometric_authentication_title, false)
BiometricAuthenticator.promptInfo?.let { BiometricAuthenticator.promptInfo?.let { promt ->
BiometricAuthenticator.biometricPrompt?.authenticate(it) BiometricAuthenticator.biometricPrompt?.authenticate(promt)
} }
// hide background while authenticating, Sorry moms & dads 🙏 // hide background while authenticating, Sorry moms & dads 🙏
@ -1326,7 +1334,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
} }
fun setUserData(status : Resource<SyncAPI.AbstractSyncStatus>?) { fun setUserData(status: Resource<SyncAPI.AbstractSyncStatus>?) {
if (isLocalList) return if (isLocalList) return
bottomPreviewBinding?.apply { bottomPreviewBinding?.apply {
when (status) { when (status) {
@ -1351,7 +1359,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
} }
} }
fun setWatchStatus(state : WatchType?) { fun setWatchStatus(state: WatchType?) {
if (!isLocalList || state == null) return if (!isLocalList || state == null) return
bottomPreviewBinding?.resultviewPreviewBookmark?.apply { bottomPreviewBinding?.resultviewPreviewBookmark?.apply {
@ -1360,13 +1368,42 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
} }
} }
observe(viewModel.watchStatus) { state -> fun setSubscribeStatus(state: Boolean?) {
setWatchStatus(state) bottomPreviewBinding?.resultviewPreviewSubscribe?.apply {
} if (state != null) {
observe(syncViewModel.userData) { status -> val drawable = if (state) {
setUserData(status) 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 -> observeNullable(viewModel.page) { resource ->
if (resource == null) { if (resource == null) {
hidePreviewPopupDialog() hidePreviewPopupDialog()
@ -1408,6 +1445,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
setUserData(syncViewModel.userData.value) setUserData(syncViewModel.userData.value)
setWatchStatus(viewModel.watchStatus.value) setWatchStatus(viewModel.watchStatus.value)
setSubscribeStatus(viewModel.subscribeStatus.value)
resultviewPreviewBookmark.setOnClickListener { resultviewPreviewBookmark.setOnClickListener {
//viewModel.updateWatchStatus(WatchType.PLANTOWATCH) //viewModel.updateWatchStatus(WatchType.PLANTOWATCH)
@ -1426,7 +1464,9 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
) )
} }
} else { } 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( this@MainActivity.showBottomDialog(
SyncWatchType.values().map { getString(it.stringRes) }.toList(), SyncWatchType.values().map { getString(it.stringRes) }.toList(),
@ -1453,7 +1493,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
resultviewPreviewFavorite.setImageResource(drawable) resultviewPreviewFavorite.setImageResource(drawable)
} }
resultviewPreviewFavorite.setOnClickListener{ resultviewPreviewFavorite.setOnClickListener {
viewModel.toggleFavoriteStatus(this@MainActivity) { newStatus: Boolean? -> viewModel.toggleFavoriteStatus(this@MainActivity) { newStatus: Boolean? ->
if (newStatus == null) return@toggleFavoriteStatus 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 -> resultviewPreviewDescription.setOnClickListener { view ->
view.context?.let { ctx -> view.context?.let { ctx ->
val builder: AlertDialog.Builder = 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)) { if (navDestination.matchDestination(R.id.navigation_home)) {
attachBackPressedCallback() attachBackPressedCallback()
} else detachBackPressedCallback() } else detachBackPressedCallback()
@ -1580,7 +1620,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener, BiometricAu
itemRippleColor = rippleColor itemRippleColor = rippleColor
itemActiveIndicatorColor = rippleColor itemActiveIndicatorColor = rippleColor
setupWithNavController(navController) setupWithNavController(navController)
if (isTvSettings()) { if (isLayout(TV or EMULATOR)) {
background?.alpha = 200 background?.alpha = 200
} else { } else {
background?.alpha = 255 background?.alpha = 255

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.services package com.lagradost.cloudstream3.services
import android.annotation.SuppressLint
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
@ -12,7 +13,7 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.R 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.plugins.PluginManager
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel 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 { override suspend fun doWork(): Result {
try {
// println("Update subscriptions!") // println("Update subscriptions!")
context.createNotificationChannel( context.createNotificationChannel(
SUBSCRIPTION_CHANNEL_ID, SUBSCRIPTION_CHANNEL_ID,
SUBSCRIPTION_CHANNEL_NAME, SUBSCRIPTION_CHANNEL_NAME,
SUBSCRIPTION_CHANNEL_DESCRIPTION SUBSCRIPTION_CHANNEL_DESCRIPTION
)
setForeground(
ForegroundInfo(
SUBSCRIPTION_NOTIFICATION_ID,
progressNotificationBuilder.build()
) )
)
val subscriptions = getAllSubscriptions() setForeground(
ForegroundInfo(
SUBSCRIPTION_NOTIFICATION_ID,
progressNotificationBuilder.build()
)
)
if (subscriptions.isEmpty()) { val subscriptions = getAllSubscriptions()
WorkManager.getInstance(context).cancelWorkById(this.id)
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() 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()
} }
} }

View File

@ -8,7 +8,8 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.library.ListSorting import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt 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.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
@ -71,9 +72,9 @@ class LocalList : SyncAPI {
}?.distinctBy { it.first } ?: return null }?.distinctBy { it.first } ?: return null
val list = ioWork { 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 // None is not something to display
it.stringRes to emptyList<SyncAPI.LibraryItem>() it.stringRes to emptyList<SyncAPI.LibraryItem>()
} + mapOf( } + mapOf(

View File

@ -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<T>(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<Int, HashMap<Int, Any?>>()
}
abstract class NoStateAdapter<T : Any>(fragment: Fragment) : BaseAdapter<T, Any>(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<T> = BaseDiffCallback()
) : RecyclerView.Adapter<ViewHolderState<S>>() {
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<T> = 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<T>?) {
// 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<S>, item: T, position: Int) =
onBindContent(holder, item, position)
open fun onBindContent(holder: ViewHolderState<S>, item: T, position: Int) = Unit
open fun onBindFooter(holder: ViewHolderState<S>) = Unit
open fun onBindHeader(holder: ViewHolderState<S>) = Unit
open fun onCreateContent(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
open fun onCreateFooter(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
open fun onCreateHeader(parent: ViewGroup): ViewHolderState<S> = throw NotImplementedError()
override fun onViewAttachedToWindow(holder: ViewHolderState<S>) {
holder.onViewAttachedToWindow()
}
override fun onViewDetachedFromWindow(holder: ViewHolderState<S>) {
holder.onViewDetachedFromWindow()
}
fun save(recyclerView: RecyclerView) {
for (child in recyclerView.children) {
val holder =
recyclerView.findContainingViewHolder(child) as? ViewHolderState<S> ?: continue
setState(holder)
}
}
fun clear() {
stateViewModel.layoutManagerStates[id]?.clear()
}
private fun getState(holder: ViewHolderState<S>): S? =
stateViewModel.layoutManagerStates[id]?.get(holder.absoluteAdapterPosition) as? S
private fun setState(holder: ViewHolderState<S>) {
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<S>) {
setState(holder)
holder.onViewRecycled()
super.onViewRecycled(holder)
}
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolderState<S> {
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<S>,
position: Int,
payloads: MutableList<Any>
) {
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<S>, 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<T : Any>(
val itemSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() },
val contentSame: (T, T) -> Boolean = { a, b -> a.hashCode() == b.hashCode() }
) : DiffUtil.ItemCallback<T>() {
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()
}

View File

@ -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)
}
}

View File

@ -12,7 +12,9 @@ import com.lagradost.cloudstream3.databinding.AccountListItemBinding
import com.lagradost.cloudstream3.databinding.AccountListItemEditBinding import com.lagradost.cloudstream3.databinding.AccountListItemEditBinding
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountEditDialog import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountEditDialog
import com.lagradost.cloudstream3.ui.result.setImage 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.DataStoreHelper
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -38,7 +40,7 @@ class AccountAdapter(
is AccountListItemBinding -> binding.apply { is AccountListItemBinding -> binding.apply {
if (account == null) return@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 val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
@ -80,7 +82,7 @@ class AccountAdapter(
is AccountListItemEditBinding -> binding.apply { is AccountListItemEditBinding -> binding.apply {
if (account == null) return@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 val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex

View File

@ -18,8 +18,10 @@ import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.AutofitRecyclerView 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_EDIT_ACCOUNT
import com.lagradost.cloudstream3.ui.account.AccountAdapter.Companion.VIEW_TYPE_SELECT_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.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings 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
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock import com.lagradost.cloudstream3.utils.BiometricAuthenticator.deviceHasPasswordPinLock
import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication import com.lagradost.cloudstream3.utils.BiometricAuthenticator.startBiometricAuthentication
@ -54,7 +56,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
fun askBiometricAuth() { fun askBiometricAuth() {
if (isTruePhone() && authEnabled) { if (isLayout(PHONE) && authEnabled) {
if (deviceHasPasswordPinLock(this)) { if (deviceHasPasswordPinLock(this)) {
startBiometricAuthentication( startBiometricAuthentication(
this, this,
@ -62,8 +64,8 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
false false
) )
BiometricAuthenticator.promptInfo?.let { BiometricAuthenticator.promptInfo?.let { promt ->
BiometricAuthenticator.biometricPrompt?.authenticate(it) BiometricAuthenticator.biometricPrompt?.authenticate(promt)
} }
} }
} }
@ -127,7 +129,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
recyclerView.adapter = adapter recyclerView.adapter = adapter
if (isTvSettings()) { if (isLayout(TV or EMULATOR)) {
binding.editAccountButton.setBackgroundResource( binding.editAccountButton.setBackgroundResource(
R.drawable.player_button_tv_attr_no_bg R.drawable.player_button_tv_attr_no_bg
) )
@ -168,7 +170,7 @@ class AccountSelectActivity : AppCompatActivity(), BiometricAuthenticator.Biomet
viewModel.toggleIsEditing() viewModel.toggleIsEditing()
} }
if (isTvSettings()) { if (isLayout(TV or EMULATOR)) {
recyclerView.spanCount = if (liveAccounts.count() + 1 <= 6) { recyclerView.spanCount = if (liveAccounts.count() + 1 <= 6) {
liveAccounts.count() + 1 liveAccounts.count() + 1
} else 6 } else 6

View File

@ -5,6 +5,7 @@ import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.format.Formatter.formatShortFileSize
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -13,17 +14,25 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R 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.isMovieType
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick 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.GeneratorPlayer
import com.lagradost.cloudstream3.ui.player.LinkGenerator 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.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE 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.UIHelper.navigate
import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper
import com.lagradost.cloudstream3.utils.VideoDownloadManager 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 import java.net.URI
@ -200,7 +200,7 @@ class DownloadFragment : Fragment() {
} }
// Should be visible in emulator layout // Should be visible in emulator layout
binding?.downloadStreamButton?.isGone = isTrueTvSettings() binding?.downloadStreamButton?.isGone = isLayout(TV)
binding?.downloadStreamButton?.setOnClickListener { binding?.downloadStreamButton?.setOnClickListener {
val dialog = val dialog =
Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom) Dialog(it.context ?: return@setOnClickListener, R.style.AlertDialogCustom)

View File

@ -2,16 +2,19 @@ package com.lagradost.cloudstream3.ui.download.button
import android.content.Context import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Looper
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.animation.AnimationUtils import android.view.animation.AnimationUtils
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.MainThread
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.lagradost.cloudstream3.R 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_DELETE_FILE
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_DOWNLOAD 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.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 */ /** Also sets currentStatus */
override fun setStatus(status: DownloadStatusTell?) { override fun setStatus(status: DownloadStatusTell?) {
currentStatus = status currentStatus = status
//progressBar.isVisible = // runs on the main thread, but also instant if it already is
// status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error if (Looper.myLooper() == Looper.getMainLooper()) {
//progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete try {
progressBarBackground.post { setStatusInternal(status)
val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading } catch (t : Throwable) {
if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { logError(t) // just in case setStatusInternal throws because thread
val animation = AnimationUtils.loadAnimation(context, waitingAnimation) progressBarBackground.post {
progressBarBackground.startAnimation(animation) setStatusInternal(status)
} else { }
progressBarBackground.clearAnimation()
} }
} else {
val progressDrawable = progressBarBackground.post {
if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline setStatusInternal(status)
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
} }
} }

View File

@ -2,31 +2,58 @@ package com.lagradost.cloudstream3.ui.home
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding 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.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder 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.IsBottomLayout
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
class HomeChildItemAdapter( class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState<Boolean>(view) {
val cardList: MutableList<SearchResponse>, /*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 nextFocusUp: Int? = null,
private val nextFocusDown: Int? = null, private val nextFocusDown: Int? = null,
private val clickCallback: (SearchClickCallback) -> Unit, private val clickCallback: (SearchClickCallback) -> Unit,
) : ) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() { BaseAdapter<SearchResponse, Boolean>(fragment, id) {
var isHorizontal: Boolean = false var isHorizontal: Boolean = false
var hasNext: Boolean = false var hasNext: Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { override fun onCreateContent(parent: ViewGroup): ViewHolderState<Boolean> {
val expanded = parent.context.IsBottomLayout() val expanded = parent.context.IsBottomLayout()
/* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid /* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid
@ -39,164 +66,78 @@ class HomeChildItemAdapter(
parent, parent,
false false
) else HomeResultGridBinding.inflate(inflater, parent, false) ) else HomeResultGridBinding.inflate(inflater, parent, false)
return HomeScrollViewHolderState(binding)
}
override fun onBindContent(
holder: ViewHolderState<Boolean>,
item: SearchResponse,
position: Int
) {
when (val binding = holder.view) {
is HomeResultGridBinding -> {
binding.backgroundCard.apply {
val min = 114.toPx
val max = 180.toPx
return CardViewHolder( layoutParams =
binding, layoutParams.apply {
clickCallback, width = if (!isHorizontal) {
itemCount, 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, nextFocusUp,
nextFocusDown, 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<SearchResponse>) {
val diffResult = DiffUtil.calculateDiff(
HomeChildDiffCallback(this.cardList, newList)
) )
cardList.clear() holder.itemView.tag = position
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)
}
} }
} }
class HomeChildDiffCallback(
private val oldList: List<SearchResponse>,
private val newList: List<SearchResponse>
) :
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
}

View File

@ -42,8 +42,10 @@ import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLine
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.* import com.lagradost.cloudstream3.ui.search.*
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings 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.isRecyclerScrollable
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.ownHide import com.lagradost.cloudstream3.utils.AppUtils.ownHide
@ -311,7 +313,7 @@ class HomeFragment : Fragment() {
button?.isVisible = isValid button?.isVisible = isValid
button?.isChecked = isValid && selectedTypes.any { types.contains(it) } button?.isChecked = isValid && selectedTypes.any { types.contains(it) }
button?.isFocusable = true button?.isFocusable = true
if (isTrueTvSettings()) { if (isLayout(TV)) {
button?.isFocusableInTouchMode = true button?.isFocusableInTouchMode = true
} }
@ -435,7 +437,7 @@ class HomeFragment : Fragment() {
bottomSheetDialog?.ownShow() bottomSheetDialog?.ownShow()
val layout = 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) val root = inflater.inflate(layout, container, false)
binding = try { binding = try {
FragmentHomeBinding.bind(root) FragmentHomeBinding.bind(root)
@ -449,6 +451,7 @@ class HomeFragment : Fragment() {
} }
override fun onDestroyView() { override fun onDestroyView() {
bottomSheetDialog?.ownHide() bottomSheetDialog?.ownHide()
binding = null binding = null
super.onDestroyView() super.onDestroyView()
@ -485,6 +488,10 @@ class HomeFragment : Fragment() {
private var bottomSheetDialog: BottomSheetDialog? = null 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") @SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -505,15 +512,14 @@ class HomeFragment : Fragment() {
activity.loadSearchResult(listHomepageItems.random()) activity.loadSearchResult(listHomepageItems.random())
} }
} }
homeMasterAdapter = HomeParentItemAdapterPreview(
homeMasterRecycler.adapter = fragment = this@HomeFragment,
HomeParentItemAdapterPreview( homeViewModel,
mutableListOf(), )
homeViewModel homeMasterRecycler.adapter = homeMasterAdapter
)
//fixPaddingStatusbar(homeLoadingStatusbar) //fixPaddingStatusbar(homeLoadingStatusbar)
homeApiFab.isVisible = !isTvSettings() homeApiFab.isVisible = isLayout(PHONE)
homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() { homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@ -521,7 +527,7 @@ class HomeFragment : Fragment() {
homeApiFab.shrink() // hide homeApiFab.shrink() // hide
homeRandom.shrink() homeRandom.shrink()
} else if (dy < -5) { } else if (dy < -5) {
if (!isTvSettings()) { if (isLayout(PHONE)) {
homeApiFab.extend() // show homeApiFab.extend() // show
homeRandom.extend() homeRandom.extend()
} }
@ -540,7 +546,7 @@ class HomeFragment : Fragment() {
settingsManager.getBoolean( settingsManager.getBoolean(
getString(R.string.random_button_key), getString(R.string.random_button_key),
false false
) && !isTvSettings() ) && isLayout(PHONE)
binding?.homeRandom?.visibility = View.GONE binding?.homeRandom?.visibility = View.GONE
} }
@ -560,10 +566,11 @@ class HomeFragment : Fragment() {
val mutableListOfResponse = mutableListOf<SearchResponse>() val mutableListOfResponse = mutableListOf<SearchResponse>()
listHomepageItems.clear() listHomepageItems.clear()
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList( (homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(d.values.map {
d.values.toMutableList(), it.copy(
homeMasterRecycler list = it.list.copy(list = it.list.list.toMutableList())
) )
}.toMutableList())
homeLoading.isVisible = false homeLoading.isVisible = false
homeLoadingError.isVisible = false homeLoadingError.isVisible = false
@ -612,7 +619,7 @@ class HomeFragment : Fragment() {
} }
is Resource.Loading -> { is Resource.Loading -> {
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf()) (homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(listOf())
homeLoadingShimmer.startShimmer() homeLoadingShimmer.startShimmer()
homeLoading.isVisible = true homeLoading.isVisible = true
homeLoadingError.isVisible = false homeLoadingError.isVisible = false

View File

@ -1,22 +1,27 @@
package com.lagradost.cloudstream3.ui.home package com.lagradost.cloudstream3.ui.home
import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListUpdateCallback
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.HomePageList import com.lagradost.cloudstream3.HomePageList
import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.HomepageParentBinding 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.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
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.isRecyclerScrollable import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
class LoadClickCallback( class LoadClickCallback(
@ -27,193 +32,85 @@ class LoadClickCallback(
) )
open class ParentItemAdapter( open class ParentItemAdapter(
private var items: MutableList<HomeViewModel.ExpandableHomepageList>, open val fragment: Fragment,
//private val viewModel: HomeViewModel, id: Int,
private val clickCallback: (SearchClickCallback) -> Unit, private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit, private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
private val expandCallback: ((String) -> Unit)? = null, private val expandCallback: ((String) -> Unit)? = null,
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : BaseAdapter<HomeViewModel.ExpandableHomepageList, Bundle>(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { fragment,
id,
val layoutResId = when { diffCallback = BaseDiffCallback(
isTrueTvSettings() -> R.layout.homepage_parent_tv itemSame = { a, b -> a.list.name == b.list.name },
parent.context.isEmulatorSettings() -> R.layout.homepage_parent_emulator contentSame = { a, b ->
else -> R.layout.homepage_parent a.list.list == b.list.list
}
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<HomePageList>) {
updateList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
.toMutableList())
}
@JvmName("updateListExpandableHomepageList")
fun updateList(
newList: MutableList<HomeViewModel.ExpandableHomepageList>,
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)
}
}
}) })
) {
//diffResult.dispatchUpdatesTo(this) data class ParentItemHolder(val binding: ViewBinding) : ViewHolderState<Bundle>(binding) {
} override fun save(): Bundle = Bundle().apply {
val recyclerView = (binding as? HomepageParentBinding)?.homeChildRecyclerview
class ParentViewHolder putParcelable(
constructor( "value",
val binding: HomepageParentBinding, recyclerView?.layoutManager?.onSaveInstanceState()
// val viewModel: HomeViewModel, )
private val clickCallback: (SearchClickCallback) -> Unit, (recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView)
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,
)
}
} }
fun bind(expand: HomeViewModel.ExpandableHomepageList) { override fun restore(state: Bundle) {
val info = expand.list (binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState(
recyclerView.adapter = HomeChildItemAdapter( state.getParcelable("value")
info.list.toMutableList(), )
}
}
override fun onUpdateContent(
holder: ViewHolderState<Bundle>,
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<Bundle>,
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, clickCallback = clickCallback,
nextFocusUp = recyclerView.nextFocusUpId, nextFocusUp = homeChildRecyclerview.nextFocusUpId,
nextFocusDown = recyclerView.nextFocusDownId, nextFocusDown = homeChildRecyclerview.nextFocusDownId,
).apply { ).apply {
isHorizontal = info.isHorizontalImages isHorizontal = info.isHorizontalImages
hasNext = expand.hasNext hasNext = item.hasNext
submitList(item.list.list)
} }
recyclerView.setLinearListLayout( homeChildRecyclerview.setLinearListLayout(
isHorizontal = true, isHorizontal = true,
nextLeft = startFocus, nextLeft = startFocus,
nextRight = endFocus, nextRight = endFocus,
) )
title.text = info.name homeChildMoreInfo.text = info.name
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { homeChildRecyclerview.addOnScrollListener(object :
RecyclerView.OnScrollListener() {
var expandCount = 0 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) super.onScrollStateChanged(recyclerView, newState)
val adapter = recyclerView.adapter val adapter = recyclerView.adapter
@ -237,27 +134,35 @@ open class ParentItemAdapter(
}) })
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged() //(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
if (!isTrueTvSettings()) { if (isLayout(PHONE)) {
title.setOnClickListener { homeChildMoreInfo.setOnClickListener {
moreInfoClickCallback.invoke(expand) moreInfoClickCallback.invoke(item)
} }
} }
} }
} }
}
class SearchDiffCallback( override fun onCreateContent(parent: ViewGroup): ParentItemHolder {
private val oldList: List<HomeViewModel.ExpandableHomepageList>, val layoutResId = when {
private val newList: List<HomeViewModel.ExpandableHomepageList> isLayout(TV) -> R.layout.homepage_parent_tv
) : isLayout(EMULATOR) -> R.layout.homepage_parent_emulator
DiffUtil.Callback() { else -> R.layout.homepage_parent
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = }
oldList[oldItemPosition].list.name == newList[newItemPosition].list.name
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 = fun updateList(newList: List<HomePageList>) {
oldList[oldItemPosition] == newList[newItemPosition] submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
.toMutableList())
}
} }

View File

@ -1,5 +1,7 @@
package com.lagradost.cloudstream3.ui.home package com.lagradost.cloudstream3.ui.home
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -7,6 +9,7 @@ import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeLifecycleOwner
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding 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.Resource
import com.lagradost.cloudstream3.mvvm.debugException import com.lagradost.cloudstream3.mvvm.debugException
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.ViewHolderState
import com.lagradost.cloudstream3.ui.WatchType import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage 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_LOAD
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_SHOW_METADATA
import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings 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.DataStoreHelper
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showOptionSelectStringRes 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 import com.lagradost.cloudstream3.utils.UIHelper.populateChips
class HomeParentItemAdapterPreview( class HomeParentItemAdapterPreview(
items: MutableList<HomeViewModel.ExpandableHomepageList>, override val fragment: Fragment,
private val viewModel: HomeViewModel, private val viewModel: HomeViewModel,
) : ParentItemAdapter(items, clickCallback = { ) : ParentItemAdapter(fragment, id = "HomeParentItemAdapterPreview".hashCode(),
viewModel.click(it) clickCallback = {
}, moreInfoClickCallback = { viewModel.click(it)
viewModel.popup(it) }, moreInfoClickCallback = {
}, expandCallback = { viewModel.popup(it)
viewModel.expand(it) }, expandCallback = {
}) { viewModel.expand(it)
val headItems = 1 }) {
override val headers = 1
override fun onCreateHeader(parent: ViewGroup): ViewHolderState<Bundle> {
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 { if (binding is FragmentHomeHeadTvBinding && isLayout(EMULATOR)) {
private const val VIEW_TYPE_HEADER = 2 binding.homeBookmarkParentItemMoreInfo.isVisible = true
private const val VIEW_TYPE_ITEM = 1
}
override fun getItemViewType(position: Int) = when (position) { val marginInDp = 50
0 -> VIEW_TYPE_HEADER val density = binding.horizontalScrollChips.context.resources.displayMetrics.density
else -> VIEW_TYPE_ITEM val marginInPixels = (marginInDp * density).toInt()
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams
when (holder) { params.marginEnd = marginInPixels
is HeaderViewHolder -> {} binding.horizontalScrollChips.layoutParams = params
else -> super.onBindViewHolder(holder, position - headItems) 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 { override fun onBindHeader(holder: ViewHolderState<Bundle>) {
return when (viewType) { (holder as? HeaderViewHolder)?.bind()
VIEW_TYPE_HEADER -> { }
val inflater = LayoutInflater.from(parent.context)
val binding = if (isTvSettings()) FragmentHomeHeadTvBinding.inflate(
inflater,
parent,
false
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
if (binding is FragmentHomeHeadTvBinding && parent.context.isEmulatorSettings()) { private class HeaderViewHolder(
binding.homeBookmarkParentItemMoreInfo.isVisible = true val binding: ViewBinding, val viewModel: HomeViewModel, fragment: Fragment,
) :
ViewHolderState<Bundle>(binding) {
val marginInDp = 50 override fun save(): Bundle =
val density = binding.horizontalScrollChips.context.resources.displayMetrics.density Bundle().apply {
val marginInPixels = (marginInDp * density).toInt() putParcelable(
"resumeRecyclerView",
val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams resumeRecyclerView.layoutManager?.onSaveInstanceState()
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,
) )
putParcelable(
"bookmarkRecyclerView",
bookmarkRecyclerView.layoutManager?.onSaveInstanceState()
)
//putInt("previewViewpager", previewViewpager.currentItem)
} }
VIEW_TYPE_ITEM -> super.onCreateViewHolder(parent, viewType) override fun restore(state: Bundle) {
else -> error("Unhandled viewType=$viewType") state.getParcelable<Parcelable>("resumeRecyclerView")?.let { recycle ->
} resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
}
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()
} }
state.getParcelable<Parcelable>("bookmarkRecyclerView")?.let { recycle ->
else -> super.onViewDetachedFromWindow(holder) bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
}
}
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
when (holder) {
is HeaderViewHolder -> {
holder.onViewAttachedToWindow()
} }
//state.getInt("previewViewpager").let { recycle ->
else -> super.onViewAttachedToWindow(holder) // previewViewpager.setCurrentItem(recycle,true)
//}
} }
}
class HeaderViewHolder val previewAdapter = HomeScrollAdapter(fragment = fragment)
constructor( private val resumeAdapter = HomeChildItemAdapter(
val binding: ViewBinding, fragment,
val viewModel: HomeViewModel, id = "resumeAdapter".hashCode(),
) : RecyclerView.ViewHolder(binding.root) {
private var previewAdapter: HomeScrollAdapter = HomeScrollAdapter()
private var resumeAdapter: HomeChildItemAdapter = HomeChildItemAdapter(
ArrayList(),
nextFocusUp = itemView.nextFocusUpId, nextFocusUp = itemView.nextFocusUpId,
nextFocusDown = itemView.nextFocusDownId nextFocusDown = itemView.nextFocusDownId
) { callback -> ) { callback ->
@ -207,8 +186,9 @@ class HomeParentItemAdapterPreview(
} }
} }
} }
private var bookmarkAdapter: HomeChildItemAdapter = HomeChildItemAdapter( private val bookmarkAdapter = HomeChildItemAdapter(
ArrayList(), fragment,
id = "bookmarkAdapter".hashCode(),
nextFocusUp = itemView.nextFocusUpId, nextFocusUp = itemView.nextFocusUpId,
nextFocusDown = itemView.nextFocusDownId nextFocusDown = itemView.nextFocusDownId
) { callback -> ) { callback ->
@ -217,7 +197,10 @@ class HomeParentItemAdapterPreview(
return@HomeChildItemAdapter 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.context?.getActivity()?.showOptionSelectStringRes(
callback.view, callback.view,
@ -267,7 +250,6 @@ class HomeParentItemAdapterPreview(
*/ */
} }
private val previewViewpager: ViewPager2 = private val previewViewpager: ViewPager2 =
itemView.findViewById(R.id.home_preview_viewpager) itemView.findViewById(R.id.home_preview_viewpager)
@ -275,38 +257,24 @@ class HomeParentItemAdapterPreview(
itemView.findViewById(R.id.home_preview_viewpager_text) itemView.findViewById(R.id.home_preview_viewpager_text)
// private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview) // private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview)
private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder) private val resumeHolder: View = itemView.findViewById(R.id.home_watch_holder)
private var resumeRecyclerView: RecyclerView = private val resumeRecyclerView: RecyclerView =
itemView.findViewById(R.id.home_watch_child_recyclerview) itemView.findViewById(R.id.home_watch_child_recyclerview)
private var bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder) private val bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder)
private var bookmarkRecyclerView: RecyclerView = private val bookmarkRecyclerView: RecyclerView =
itemView.findViewById(R.id.home_bookmarked_child_recyclerview) itemView.findViewById(R.id.home_bookmarked_child_recyclerview)
private var homeAccount: View? = private val homeAccount: View? = itemView.findViewById(R.id.home_preview_switch_account)
itemView.findViewById(R.id.home_preview_switch_account) private val alternativeHomeAccount: View? =
private var alternativeHomeAccount: View? =
itemView.findViewById(R.id.alternative_switch_account) 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 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) { fun onSelect(item: LoadResponse, position: Int) {
(binding as? FragmentHomeHeadTvBinding)?.apply { (binding as? FragmentHomeHeadTvBinding)?.apply {
homePreviewDescription.isGone = homePreviewDescription.isGone =
@ -379,14 +347,14 @@ class HomeParentItemAdapterPreview(
homePreviewBookmark.setOnClickListener { fab -> homePreviewBookmark.setOnClickListener { fab ->
fab.context.getActivity()?.showBottomDialog( fab.context.getActivity()?.showBottomDialog(
WatchType.values() WatchType.entries
.map { fab.context.getString(it.stringRes) } .map { fab.context.getString(it.stringRes) }
.toList(), .toList(),
DataStoreHelper.getResultWatchState(id).ordinal, DataStoreHelper.getResultWatchState(id).ordinal,
fab.context.getString(R.string.action_add_to_bookmarks), fab.context.getString(R.string.action_add_to_bookmarks),
showApply = false, showApply = false,
{}) { {}) {
val newValue = WatchType.values()[it] val newValue = WatchType.entries[it]
ResultViewModel2().updateWatchStatus( ResultViewModel2().updateWatchStatus(
newValue, newValue,
@ -411,38 +379,22 @@ class HomeParentItemAdapterPreview(
} }
} }
fun onViewDetachedFromWindow() { private val previewCallback: ViewPager2.OnPageChangeCallback =
previewViewpager.unregisterOnPageChangeCallback(previewCallback) object : ViewPager2.OnPageChangeCallback() {
} override fun onPageSelected(position: Int) {
previewAdapter.apply {
fun onViewAttachedToWindow() { if (position >= itemCount - 1 && hasMoreItems) {
previewViewpager.registerOnPageChangeCallback(previewCallback) hasMoreItems = false // don't make two requests
viewModel.loadMoreHomeScrollResponses()
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() val item = previewAdapter.getItemOrNull(position) ?: return
onSelect(item, position)
} }
} ?: debugException { "Expected findViewTreeLifecycleOwner" } }
override fun onViewDetachedFromWindow() {
previewViewpager.unregisterOnPageChangeCallback(previewCallback)
} }
private val toggleList = listOf<Pair<Chip, WatchType>>( private val toggleList = listOf<Pair<Chip, WatchType>>(
@ -455,6 +407,8 @@ class HomeParentItemAdapterPreview(
private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder) private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder)
fun bind() = Unit
init { init {
previewViewpager.setPageTransformer(HomeScrollTransformer()) previewViewpager.setPageTransformer(HomeScrollTransformer())
@ -561,7 +515,9 @@ class HomeParentItemAdapterPreview(
when (preview) { when (preview) {
is Resource.Success -> { is Resource.Success -> {
if (!previewAdapter.setItems( previewAdapter.submitList(preview.value.second)
previewAdapter.hasMoreItems = preview.value.first
/*if (!.setItems(
preview.value.second, preview.value.second,
preview.value.first preview.value.first
) )
@ -573,15 +529,16 @@ class HomeParentItemAdapterPreview(
previewViewpager.fakeDragBy(1f) previewViewpager.fakeDragBy(1f)
previewViewpager.endFakeDrag() previewViewpager.endFakeDrag()
previewCallback.onPageSelected(0) previewCallback.onPageSelected(0)
previewViewpager.isVisible = true
previewViewpagerText.isVisible = true
alternativeAccountPadding?.isVisible = false
//previewHeader.isVisible = true //previewHeader.isVisible = true
} }*/
previewViewpager.isVisible = true
previewViewpagerText.isVisible = true
alternativeAccountPadding?.isVisible = false
} }
else -> { else -> {
previewAdapter.setItems(listOf(), false) previewAdapter.submitList(listOf())
previewViewpager.setCurrentItem(0, false) previewViewpager.setCurrentItem(0, false)
previewViewpager.isVisible = false previewViewpager.isVisible = false
previewViewpagerText.isVisible = false previewViewpagerText.isVisible = false
@ -593,12 +550,12 @@ class HomeParentItemAdapterPreview(
private fun updateResume(resumeWatching: List<SearchResponse>) { private fun updateResume(resumeWatching: List<SearchResponse>) {
resumeHolder.isVisible = resumeWatching.isNotEmpty() resumeHolder.isVisible = resumeWatching.isNotEmpty()
resumeAdapter.updateList(resumeWatching) resumeAdapter.submitList(resumeWatching)
if ( if (
binding is FragmentHomeHeadBinding || binding is FragmentHomeHeadBinding ||
binding is FragmentHomeHeadTvBinding && binding is FragmentHomeHeadTvBinding &&
binding.root.context.isEmulatorSettings() isLayout(EMULATOR)
) { ) {
val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle val title = (binding as? FragmentHomeHeadBinding)?.homeWatchParentItemTitle
?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle ?: (binding as? FragmentHomeHeadTvBinding)?.homeWatchParentItemTitle
@ -623,12 +580,12 @@ class HomeParentItemAdapterPreview(
private fun updateBookmarks(data: Pair<Boolean, List<SearchResponse>>) { private fun updateBookmarks(data: Pair<Boolean, List<SearchResponse>>) {
val (visible, list) = data val (visible, list) = data
bookmarkHolder.isVisible = visible bookmarkHolder.isVisible = visible
bookmarkAdapter.updateList(list) bookmarkAdapter.submitList(list)
if ( if (
binding is FragmentHomeHeadBinding || binding is FragmentHomeHeadBinding ||
binding is FragmentHomeHeadTvBinding && binding is FragmentHomeHeadTvBinding &&
binding.root.context.isEmulatorSettings() isLayout(EMULATOR)
) { ) {
val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle val title = (binding as? FragmentHomeHeadBinding)?.homeBookmarkParentItemTitle
?: (binding as? FragmentHomeHeadTvBinding)?.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" }
}
} }
} }

View File

@ -4,112 +4,61 @@ import android.content.res.Configuration
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.recyclerview.widget.DiffUtil import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding
import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding 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 import com.lagradost.cloudstream3.utils.UIHelper.setImage
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() { class HomeScrollAdapter(
private var items: MutableList<LoadResponse> = mutableListOf() fragment: Fragment
) : NoStateAdapter<LoadResponse>(fragment) {
var hasMoreItems: Boolean = false var hasMoreItems: Boolean = false
fun getItem(position: Int): LoadResponse? { override fun onCreateContent(parent: ViewGroup): ViewHolderState<Any> {
return items.getOrNull(position)
}
fun setItems(newItems: List<LoadResponse>, 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 {
val inflater = LayoutInflater.from(parent.context) val inflater = LayoutInflater.from(parent.context)
val binding = if (isTvSettings()) { val binding = if (isLayout(TV or EMULATOR)) {
HomeScrollViewTvBinding.inflate(inflater, parent, false) HomeScrollViewTvBinding.inflate(inflater, parent, false)
} else { } else {
HomeScrollViewBinding.inflate(inflater, parent, false) HomeScrollViewBinding.inflate(inflater, parent, false)
} }
return CardViewHolder( return ViewHolderState(binding)
binding,
//forceHorizontalPosters
)
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onBindContent(
when (holder) { holder: ViewHolderState<Any>,
is CardViewHolder -> { item: LoadResponse,
holder.bind(items[position]) 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<LoadResponse>,
private val newList: List<LoadResponse>
) :
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
}
} }

View File

@ -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.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchHelper 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.addProgramsToContinueWatching
import com.lagradost.cloudstream3.utils.AppUtils.loadResult import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -52,6 +53,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.EnumSet import java.util.EnumSet
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.collections.set import kotlin.collections.set
class HomeViewModel : ViewModel() { class HomeViewModel : ViewModel() {
@ -124,7 +126,7 @@ class HomeViewModel : ViewModel() {
private val _resumeWatching = MutableLiveData<List<SearchResponse>>() private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>() private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>()
private val previewResponses = mutableListOf<LoadResponse>() private val previewResponses = CopyOnWriteArrayList<LoadResponse>()
private val previewResponsesAdded = mutableSetOf<String>() private val previewResponsesAdded = mutableSetOf<String>()
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
@ -132,7 +134,7 @@ class HomeViewModel : ViewModel() {
private fun loadResumeWatching() = viewModelScope.launchSafe { private fun loadResumeWatching() = viewModelScope.launchSafe {
val resumeWatchingResult = getResumeWatching() 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 { ioSafe {
// this WILL crash on non tvs, so keep this inside a try catch // this WILL crash on non tvs, so keep this inside a try catch
activity?.addProgramsToContinueWatching(resumeWatchingResult) activity?.addProgramsToContinueWatching(resumeWatchingResult)
@ -326,7 +328,13 @@ class HomeViewModel : ViewModel() {
val filteredList = val filteredList =
context?.filterHomePageListByFilmQuality(list) ?: list context?.filterHomePageListByFilmQuality(list) ?: list
expandable[list.name] = 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 = val currentList =
items.shuffled().filter { it.list.isNotEmpty() } items.shuffled().filter { it.list.isNotEmpty() }
.flatMap { it.list } .flatMap { it.list }
.distinctBy { it.url } .distinctBy { it.url }.toList()
.toList()
if (currentList.isNotEmpty()) { if (currentList.isNotEmpty()) {
val randomItems = val randomItems =

View File

@ -49,7 +49,10 @@ import com.lagradost.cloudstream3.ui.quicksearch.QuickSearchFragment
import com.lagradost.cloudstream3.ui.result.txt import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD 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.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.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity 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.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.math.abs import kotlin.math.abs
const val LIBRARY_FOLDER = "library_folder" const val LIBRARY_FOLDER = "library_folder"
@ -101,7 +105,7 @@ class LibraryFragment : Fragment() {
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View { ): View {
val layout = 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) val root = inflater.inflate(layout, container, false)
binding = try { binding = try {
FragmentLibraryBinding.bind(root) FragmentLibraryBinding.bind(root)
@ -160,7 +164,8 @@ class LibraryFragment : Fragment() {
} }
// Set the color for the search exit icon to the correct theme text color // Set the color for the search exit icon to the correct theme text color
val searchExitIcon = binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn) val searchExitIcon =
binding?.mainSearch?.findViewById<ImageView>(androidx.appcompat.R.id.search_close_btn)
val searchExitIconColor = TypedValue() val searchExitIconColor = TypedValue()
activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true) activity?.theme?.resolveAttribute(android.R.attr.textColor, searchExitIconColor, true)
@ -220,7 +225,7 @@ class LibraryFragment : Fragment() {
settingsManager.getBoolean( settingsManager.getBoolean(
getString(R.string.random_button_key), getString(R.string.random_button_key),
false false
) && !SettingsFragment.isTvSettings() ) && isLayout(PHONE)
binding?.libraryRandom?.visibility = View.GONE binding?.libraryRandom?.visibility = View.GONE
} }
@ -228,7 +233,7 @@ class LibraryFragment : Fragment() {
if (listLibraryItems.isNotEmpty()) { if (listLibraryItems.isNotEmpty()) {
val listLibraryItem = listLibraryItems.random() val listLibraryItem = listLibraryItems.random()
libraryViewModel.currentSyncApi?.syncIdName?.let { 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?.setPageTransformer(LibraryScrollTransformer())
binding?.viewpager?.adapter = binding?.viewpager?.adapter = ViewpagerAdapter(
binding?.viewpager?.adapter ?: ViewpagerAdapter( fragment = this,
mutableListOf(), { isScrollingDown: Boolean ->
{ isScrollingDown: Boolean -> if (isScrollingDown) {
if (isScrollingDown) { binding?.sortFab?.shrink()
binding?.sortFab?.shrink() binding?.libraryRandom?.shrink()
binding?.libraryRandom?.shrink() } else {
} else { binding?.sortFab?.extend()
binding?.sortFab?.extend() binding?.libraryRandom?.extend()
binding?.libraryRandom?.extend() }
} }) callback@{ searchClickCallback ->
}) callback@{ searchClickCallback -> // To prevent future accidents
// To prevent future accidents debugAssert({
debugAssert({ searchClickCallback.card !is SyncAPI.LibraryItem
searchClickCallback.card !is SyncAPI.LibraryItem }, {
}, { "searchClickCallback ${searchClickCallback.card} is not a LibraryItem"
"searchClickCallback ${searchClickCallback.card} is not a LibraryItem" })
})
val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId val syncId = (searchClickCallback.card as SyncAPI.LibraryItem).syncId
val syncName = val syncName =
libraryViewModel.currentSyncApi?.syncIdName ?: return@callback libraryViewModel.currentSyncApi?.syncIdName ?: return@callback
when (searchClickCallback.action) { when (searchClickCallback.action) {
SEARCH_ACTION_SHOW_METADATA -> { SEARCH_ACTION_SHOW_METADATA -> {
(activity as? MainActivity)?.loadPopup(searchClickCallback.card, load = false) (activity as? MainActivity)?.loadPopup(
searchClickCallback.card,
load = false
)
/*activity?.showPluginSelectionDialog( /*activity?.showPluginSelectionDialog(
syncId, syncId,
syncName, syncName,
searchClickCallback.card.apiName searchClickCallback.card.apiName
)*/ )*/
} }
SEARCH_ACTION_LOAD -> { SEARCH_ACTION_LOAD -> {
loadLibraryItem(syncName, syncId, searchClickCallback.card) loadLibraryItem(syncName, syncId, searchClickCallback.card)
}
} }
} }
}
binding?.apply { binding?.apply {
viewpager.offscreenPageLimit = 2 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 //fix focus on the viewpager itself
(viewpager.getChildAt(0) as RecyclerView).apply { (viewpager.getChildAt(0) as RecyclerView).apply {
tag = "tv_no_focus_tag" tag = "tv_no_focus_tag"
@ -398,10 +409,10 @@ class LibraryFragment : Fragment() {
} }
// Using notifyItemRangeChanged keeps the animations when sorting // Using notifyItemRangeChanged keeps the animations when sorting
viewpager.adapter?.notifyItemRangeChanged( /*viewpager.adapter?.notifyItemRangeChanged(
0, 0,
viewpager.adapter?.itemCount ?: 0 viewpager.adapter?.itemCount ?: 0
) )*/
libraryViewModel.currentPage.value?.let { page -> libraryViewModel.currentPage.value?.let { page ->
binding?.viewpager?.setCurrentItem(page, false) binding?.viewpager?.setCurrentItem(page, false)
@ -459,12 +470,14 @@ class LibraryFragment : Fragment() {
} }
}.attach() }.attach()
binding?.libraryTabLayout?.addOnTabSelectedListener(object: TabLayout.OnTabSelectedListener { binding?.libraryTabLayout?.addOnTabSelectedListener(object :
TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) { override fun onTabSelected(tab: TabLayout.Tab?) {
binding?.libraryTabLayout?.selectedTabPosition?.let { page -> binding?.libraryTabLayout?.selectedTabPosition?.let { page ->
libraryViewModel.switchPage(page) libraryViewModel.switchPage(page)
} }
} }
override fun onTabUnselected(tab: TabLayout.Tab?) = Unit override fun onTabUnselected(tab: TabLayout.Tab?) = Unit
override fun onTabReselected(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) { override fun onConfigurationChanged(newConfig: Configuration) {
(binding?.viewpager?.adapter as? ViewpagerAdapter)?.rebind() binding?.viewpager?.adapter?.notifyDataSetChanged()
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
} }

View File

@ -113,7 +113,7 @@ class LibraryViewModel : ViewModel() {
} }
val desiredSortingMethod = val desiredSortingMethod =
ListSorting.values().getOrNull(DataStoreHelper.librarySortingMode) ListSorting.entries.getOrNull(DataStoreHelper.librarySortingMode)
if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) { if (desiredSortingMethod != null && library.supportedListSorting.contains(desiredSortingMethod)) {
sort(desiredSortingMethod, null, pages) sort(desiredSortingMethod, null, pages)
} else { } else {

View File

@ -1,104 +1,123 @@
package com.lagradost.cloudstream3.ui.library package com.lagradost.cloudstream3.ui.library
import android.os.Build import android.os.Build
import android.util.Log import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.doOnAttach import androidx.core.view.doOnAttach
import androidx.recyclerview.widget.RecyclerView import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView.OnFlingListener import androidx.recyclerview.widget.RecyclerView.OnFlingListener
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding import com.lagradost.cloudstream3.databinding.LibraryViewpagerPageBinding
import com.lagradost.cloudstream3.syncproviders.SyncAPI 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.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 import com.lagradost.cloudstream3.utils.UIHelper.getSpanCount
class ViewpagerAdapterViewHolderState(val binding: LibraryViewpagerPageBinding) :
ViewHolderState<Bundle>(binding) {
override fun save(): Bundle =
Bundle().apply {
putParcelable(
"pageRecyclerview",
binding.pageRecyclerview.layoutManager?.onSaveInstanceState()
)
}
override fun restore(state: Bundle) {
state.getParcelable<Parcelable>("pageRecyclerview")?.let { recycle ->
binding.pageRecyclerview.layoutManager?.onRestoreInstanceState(recycle)
}
}
}
class ViewpagerAdapter( class ViewpagerAdapter(
var pages: List<SyncAPI.Page>, fragment: Fragment,
val scrollCallback: (isScrollingDown: Boolean) -> Unit, val scrollCallback: (isScrollingDown: Boolean) -> Unit,
val clickCallback: (SearchClickCallback) -> Unit val clickCallback: (SearchClickCallback) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { ) : BaseAdapter<SyncAPI.Page, Bundle>(fragment,
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { id = "ViewpagerAdapter".hashCode(),
return PageViewHolder( 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<Bundle> {
return ViewpagerAdapterViewHolderState(
LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false) LibraryViewpagerPageBinding.inflate(LayoutInflater.from(parent.context), parent, false)
) )
} }
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { override fun onUpdateContent(
when (holder) { holder: ViewHolderState<Bundle>,
is PageViewHolder -> { item: SyncAPI.Page,
holder.bind(pages[position], position, unbound.remove(position)) position: Int
} ) {
} val binding = holder.view
if (binding !is LibraryViewpagerPageBinding) return
(binding.pageRecyclerview.adapter as? PageAdapter)?.updateList(item.items)
} }
private val unbound = mutableSetOf<Int>() override fun onBindContent(holder: ViewHolderState<Bundle>, item: SyncAPI.Page, position: Int) {
val binding = holder.view
if (binding !is LibraryViewpagerPageBinding) return
/** binding.pageRecyclerview.tag = position
* Used to mark all pages for re-binding and forces all items to be refreshed binding.pageRecyclerview.apply {
* Without this the pages will still use the same adapters spanCount =
**/ binding.root.context.getSpanCount() ?: 3
fun rebind() { if (adapter == null) { // || rebind
unbound.addAll(0..pages.size) // Only add the items after it has been attached since the items rely on ItemWidth
this.notifyItemRangeChanged(0, pages.size) // Which is only determined after the recyclerview is attached.
} // If this fails then item height becomes 0 when there is only one item
doOnAttach {
inner class PageViewHolder(private val binding: LibraryViewpagerPageBinding) : adapter = PageAdapter(
RecyclerView.ViewHolder(binding.root) { item.items.toMutableList(),
fun bind(page: SyncAPI.Page, position: Int, rebind: Boolean) { this,
binding.pageRecyclerview.tag = position clickCallback
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)
} }
} else {
(adapter as? PageAdapter)?.updateList(item.items)
// scrollToPosition(0)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
val diff = scrollY - oldScrollY val diff = scrollY - oldScrollY
//Expand the top Appbar based on scroll direction up/down, simulate phone behavior //Expand the top Appbar based on scroll direction up/down, simulate phone behavior
if (SettingsFragment.isTvSettings()) { if (isLayout(TV or EMULATOR)) {
binding.root.rootView.findViewById<AppBarLayout>(R.id.search_bar) binding.root.rootView.findViewById<AppBarLayout>(R.id.search_bar)
.apply { .apply {
if (diff <= 0) if (diff <= 0)
setExpanded(true) setExpanded(true)
else else
setExpanded(false) setExpanded(false)
} }
}
if (diff == 0) return@setOnScrollChangeListener
scrollCallback.invoke(diff > 0)
} }
} else { if (diff == 0) return@setOnScrollChangeListener
onFlingListener = object : OnFlingListener() {
override fun onFling(velocityX: Int, velocityY: Int): Boolean { scrollCallback.invoke(diff > 0)
scrollCallback.invoke(velocityY > 0) }
return false } 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
}
} }

View File

@ -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.player.source_priority.QualityDataHelper
import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt 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.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
@ -77,7 +81,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
private var isVerticalOrientation: Boolean = false private var isVerticalOrientation: Boolean = false
protected open var lockRotation = true protected open var lockRotation = true
protected open var isFullScreenPlayer = true protected open var isFullScreenPlayer = true
protected open var isTv = false
protected var playerBinding: PlayerCustomLayoutBinding? = null protected var playerBinding: PlayerCustomLayoutBinding? = null
private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false) private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false)
@ -1204,7 +1207,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
// netflix capture back and hide ~monke // netflix capture back and hide ~monke
KeyEvent.KEYCODE_BACK -> { KeyEvent.KEYCODE_BACK -> {
if (isShowing && isTv) { if (isShowing && isLayout(TV or EMULATOR)) {
onClickChange() onClickChange()
return true return true
} }
@ -1514,7 +1517,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
} }
} }
// cs3 is peak media center // cs3 is peak media center
setRemainingTimeCounter(durationMode || SettingsFragment.isTrueTvSettings()) setRemainingTimeCounter(durationMode || Globals.isLayout(Globals.TV))
playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ -> playerBinding?.exoPosition?.doOnTextChanged { _, _, _, _ ->
updateRemainingTime() updateRemainingTime()
} }

View File

@ -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.QualityDataHelper
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
import com.lagradost.cloudstream3.ui.result.* 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.SUBTITLE_AUTO_SELECT_KEY
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1 import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageISO639_1
import com.lagradost.cloudstream3.utils.* import com.lagradost.cloudstream3.utils.*
@ -1275,8 +1278,7 @@ class GeneratorPlayer : FullScreenPlayer() {
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? { ): View? {
// this is used instead of layout-television to follow the settings and some TV devices are not classified as TV for some reason // 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 (isLayout(TV or EMULATOR)) R.layout.fragment_player_tv else R.layout.fragment_player
layout = if (isTv) R.layout.fragment_player_tv else R.layout.fragment_player
viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java] viewModel = ViewModelProvider(this)[PlayerGeneratorViewModel::class.java]
sync = ViewModelProvider(this)[SyncViewModel::class.java] sync = ViewModelProvider(this)[SyncViewModel::class.java]

View File

@ -9,6 +9,9 @@ import android.util.Log
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.core.graphics.scale import androidx.core.graphics.scale
import com.lagradost.cloudstream3.mvvm.logError 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.ui.settings.SettingsFragment
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
@ -63,7 +66,7 @@ interface IPreviewGenerator {
companion object { companion object {
fun new(): IPreviewGenerator { fun new(): IPreviewGenerator {
/** because TV has low ram + not show we disable this for now */ /** because TV has low ram + not show we disable this for now */
return if (SettingsFragment.isTrueTvSettings()) { return if (isLayout(TV)) {
empty() empty()
} else { } else {
PreviewGenerator() PreviewGenerator()

View File

@ -34,7 +34,8 @@ import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchClickCallback import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.search.SearchViewModel 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.AppUtils.ownShow
import com.lagradost.cloudstream3.utils.UIHelper import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
@ -173,7 +174,7 @@ class QuickSearchFragment : Fragment() {
} }
} else { } else {
binding?.quickSearchMasterRecycler?.adapter = binding?.quickSearchMasterRecycler?.adapter =
ParentItemAdapter(mutableListOf(), { callback -> ParentItemAdapter(fragment = this, id = "quickSearchMasterRecycler".hashCode(), { callback ->
SearchHelper.handleSearchClickCallback(callback) SearchHelper.handleSearchClickCallback(callback)
//when (callback.action) { //when (callback.action) {
//SEARCH_ACTION_LOAD -> { //SEARCH_ACTION_LOAD -> {
@ -277,7 +278,7 @@ class QuickSearchFragment : Fragment() {
activity?.popCurrentPage() activity?.popCurrentPage()
} }
if (isTrueTvSettings()) { if (isLayout(TV)) {
binding?.quickSearch?.requestFocus() binding?.quickSearch?.requestFocus()
} }

View File

@ -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_DOWNLOAD
import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_LONG_CLICK
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings 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.AppUtils.html
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
@ -172,15 +174,13 @@ class EpisodeAdapter(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(card: ResultEpisode) { fun bind(card: ResultEpisode) {
localCard = card localCard = card
val setWidth = 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.episodeLinHolder.layoutParams.width = setWidth
binding.episodeHolderLarge.layoutParams.width = setWidth binding.episodeHolderLarge.layoutParams.width = setWidth
binding.episodeHolder.layoutParams.width = setWidth binding.episodeHolder.layoutParams.width = setWidth
val isTrueTv = isTrueTvSettings()
binding.apply { binding.apply {
downloadButton.isVisible = hasDownloadSupport downloadButton.isVisible = hasDownloadSupport
@ -246,12 +246,21 @@ class EpisodeAdapter(
episodeDescript.apply { episodeDescript.apply {
text = card.description.html() text = card.description.html()
isGone = text.isNullOrBlank() isGone = text.isNullOrBlank()
var isExpanded = false
setOnClickListener { 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 { episodePoster.setOnClickListener {
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
} }
@ -266,7 +275,7 @@ class EpisodeAdapter(
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
} }
if (isTrueTv) { if (isLayout(TV)) {
itemView.isFocusable = true itemView.isFocusable = true
itemView.isFocusableInTouchMode = true itemView.isFocusableInTouchMode = true
//itemView.touchscreenBlocksFocus = false //itemView.touchscreenBlocksFocus = false
@ -291,11 +300,9 @@ class EpisodeAdapter(
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(card: ResultEpisode) { fun bind(card: ResultEpisode) {
val isTrueTv = isTrueTvSettings()
binding.episodeHolder.layoutParams.apply { binding.episodeHolder.layoutParams.apply {
width = 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 { binding.apply {
@ -352,7 +359,7 @@ class EpisodeAdapter(
clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card)) clickCallback.invoke(EpisodeClickEvent(ACTION_CLICK_DEFAULT, card))
} }
if (isTrueTv) { if (isLayout(TV)) {
itemView.isFocusable = true itemView.isFocusable = true
itemView.isFocusableInTouchMode = true itemView.isFocusableInTouchMode = true
//itemView.touchscreenBlocksFocus = false //itemView.touchscreenBlocksFocus = false

View File

@ -5,7 +5,8 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.lagradost.cloudstream3.databinding.ResultMiniImageBinding 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<Int>(context, resource) { class ImageAdapter(context: Context, val resource: Int) : ArrayAdapter<Int>(context, resource) {
@ -83,7 +84,7 @@ class ImageAdapter(
this.nextFocusUpId = nextFocusUp this.nextFocusUpId = nextFocusUp
} }
if (clickCallback != null) { if (clickCallback != null) {
if (isTrueTvSettings()) { if (isLayout(TV)) {
isClickable = true isClickable = true
isLongClickable = true isLongClickable = true
isFocusable = true isFocusable = true

View File

@ -2,9 +2,6 @@ package com.lagradost.cloudstream3.ui.result
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Rect 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
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.CommonActivity import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.DubStatus import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent 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.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull 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.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant 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.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper 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.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
@ -688,14 +683,15 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultNextAiringTime.setText(d.nextAiringDate) resultNextAiringTime.setText(d.nextAiringDate)
resultPoster.setImage(d.posterImage) resultPoster.setImage(d.posterImage)
resultPosterBackground.setImage(d.posterBackgroundImage) resultPosterBackground.setImage(d.posterBackgroundImage)
resultDescription.setTextHtml(d.plotText)
resultDescription.setOnClickListener { var isExpanded = false
activity?.let { activity -> resultDescription.apply {
activity.showBottomDialogText( setTextHtml(d.plotText)
d.titleText.asString(activity), setOnClickListener {
d.plotText.asString(activity).html(), isExpanded = !isExpanded
{} maxLines = if (isExpanded) {
) Integer.MAX_VALUE
} else 10
} }
} }
@ -758,14 +754,8 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure
resultTitle.setOnLongClickListener { resultTitle.setOnLongClickListener {
val titleToCopy = resultTitle.text clipboardHelper(txt(R.string.title), resultTitle.text)
val clipboardManager = true
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
} }
} }
} }
@ -901,14 +891,6 @@ open class ResultFragmentPhone : FullScreenPlayer() {
observe(viewModel.recommendations) { recommendations -> observe(viewModel.recommendations) { recommendations ->
setRecommendations(recommendations, null) setRecommendations(recommendations, null)
} }
observe(viewModel.episodeSynopsis) { description ->
activity?.let { activity ->
activity.showBottomDialogText(
activity.getString(R.string.synopsis),
description.html()
) { viewModel.releaseEpisodeSynopsis() }
}
}
context?.let { ctx -> context?.let { ctx ->
val arrayAdapter = ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice) val arrayAdapter = ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)
/* /*

View File

@ -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.SEARCH_ACTION_FOCUSED
import com.lagradost.cloudstream3.ui.search.SearchAdapter import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper 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.html
import com.lagradost.cloudstream3.utils.AppUtils.isRtl import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.AppUtils.loadCache import com.lagradost.cloudstream3.utils.AppUtils.loadCache
@ -308,9 +311,10 @@ class ResultFragmentTv : Fragment() {
resultEpisodesShowButton to resultEpisodesShowText resultEpisodesShowButton to resultEpisodesShowText
).forEach { (button , text) -> ).forEach { (button , text) ->
button.setOnFocusChangeListener { _, hasFocus -> button.setOnFocusChangeListener { view, hasFocus ->
if (!hasFocus) { if (!hasFocus) {
text.isSelected = false text.isSelected = false
if (view.id == R.id.result_episodes_show_button) toggleEpisodes(false)
return@setOnFocusChangeListener return@setOnFocusChangeListener
} }
@ -376,10 +380,6 @@ class ResultFragmentTv : Fragment() {
resultMetaSite.isFocusable = false resultMetaSite.isFocusable = false
//resultReloadConnectionOpenInBrowser.setOnClickListener {view ->
// view.context?.openBrowser(storedData?.url ?: return@setOnClickListener, fallbackWebview = true)
//}
resultSeasonSelection.setAdapter() resultSeasonSelection.setAdapter()
resultRangeSelection.setAdapter() resultRangeSelection.setAdapter()
resultDubSelection.setAdapter() resultDubSelection.setAdapter()
@ -457,11 +457,12 @@ class ResultFragmentTv : Fragment() {
observeNullable(viewModel.resumeWatching) { resume -> observeNullable(viewModel.resumeWatching) { resume ->
binding?.apply { binding?.apply {
// > resultResumeSeries is visible when not null
if (resume == null) { if (resume == null) {
resultResumeSeries.isVisible = false
return@observeNullable return@observeNullable
} }
resultResumeSeries.isVisible = true
resultPlayMovie.isVisible = false
resultPlaySeries.isVisible = false
// show progress no matter if series or movie // show progress no matter if series or movie
resume.progress?.let { progress -> resume.progress?.let { progress ->
@ -476,10 +477,6 @@ class ResultFragmentTv : Fragment() {
resultResumeProgressHolder.isVisible = false resultResumeProgressHolder.isVisible = false
} }
resultPlayMovie.isVisible = false
resultPlaySeries.isVisible = false
resultResumeSeries.isVisible = true
focusPlayButton() focusPlayButton()
// Stops last button right focus if it is a movie // Stops last button right focus if it is a movie
if (resume.isMovie) if (resume.isMovie)
@ -490,7 +487,7 @@ class ResultFragmentTv : Fragment() {
resume.isMovie -> context?.getString(R.string.resume) resume.isMovie -> context?.getString(R.string.resume)
resume.result.season != null -> resume.result.season != null ->
"${getString(R.string.season_short)}${resume.result.season}:${getString(R.string.episode_short)}${resume.result.episode}" "${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 { resultResumeSeriesButton.setOnClickListener {
@ -603,7 +600,7 @@ class ResultFragmentTv : Fragment() {
} }
observeNullable(viewModel.subscribeStatus) { isSubscribed -> observeNullable(viewModel.subscribeStatus) { isSubscribed ->
binding?.resultSubscribe?.isVisible = isSubscribed != null && requireContext().isEmulatorSettings() binding?.resultSubscribe?.isVisible = isSubscribed != null && isLayout(EMULATOR)
binding?.resultSubscribeButton?.apply { binding?.resultSubscribeButton?.apply {
if (isSubscribed == null) return@observeNullable if (isSubscribed == null) return@observeNullable
@ -646,15 +643,14 @@ class ResultFragmentTv : Fragment() {
} }
observeNullable(viewModel.movie) { data -> observeNullable(viewModel.movie) { data ->
if (data == null) return@observeNullable if (data == null ) {
return@observeNullable
}
binding?.apply { binding?.apply {
resultPlayMovie.isVisible = (data is Resource.Success) && !comingSoon
resultPlaySeries.isVisible = false
resultEpisodesShow.isVisible = false
(data as? Resource.Success)?.value?.let { (text, ep) -> (data as? Resource.Success)?.value?.let { (text, ep) ->
//resultPlayMovieText.setText(text)
resultPlayMovieButton.setOnClickListener { resultPlayMovieButton.setOnClickListener {
viewModel.handleAction( viewModel.handleAction(
EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep) EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep)
@ -666,14 +662,17 @@ class ResultFragmentTv : Fragment() {
) )
return@setOnLongClickListener true return@setOnLongClickListener true
} }
//focusPlayButton()
resultPlayMovieButton.requestFocus() resultPlayMovie.isVisible = !comingSoon && resultResumeSeries.isGone
if (comingSoon)
resultBookmarkButton.requestFocus()
else
resultPlayMovieButton.requestFocus()
// Stops last button right focus // Stops last button right focus
resultSearchButton.nextFocusRightId = R.id.result_search_Button resultSearchButton.nextFocusRightId = R.id.result_search_Button
} }
} }
//focusPlayButton()
} }
observeNullable(viewModel.selectPopup) { popup -> observeNullable(viewModel.selectPopup) { popup ->
@ -754,16 +753,19 @@ class ResultFragmentTv : Fragment() {
observe(viewModel.recommendations) { recommendations -> observe(viewModel.recommendations) { recommendations ->
setRecommendations(recommendations, null) setRecommendations(recommendations, null)
} }
observe(viewModel.episodeSynopsis) { description ->
view.context?.let { ctx -> if (isLayout(TV)) {
val builder: AlertDialog.Builder = observe(viewModel.episodeSynopsis) { description ->
AlertDialog.Builder(ctx, R.style.AlertDialogCustom) view.context?.let { ctx ->
builder.setMessage(description.html()) val builder: AlertDialog.Builder =
.setTitle(R.string.synopsis) AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
.setOnDismissListener { builder.setMessage(description.html())
viewModel.releaseEpisodeSynopsis() .setTitle(R.string.synopsis)
} .setOnDismissListener {
.show() viewModel.releaseEpisodeSynopsis()
}
.show()
}
} }
} }
@ -774,16 +776,14 @@ class ResultFragmentTv : Fragment() {
binding?.apply { binding?.apply {
resultPlayMovie.isVisible = false if (comingSoon)
resultPlaySeries.isVisible = true && !comingSoon resultBookmarkButton.requestFocus()
resultEpisodes.isVisible = true && !comingSoon
resultEpisodesShow.isVisible = true && !comingSoon
// resultEpisodeLoading.isVisible = episodes is Resource.Loading // resultEpisodeLoading.isVisible = episodes is Resource.Loading
if (episodes is Resource.Success) { if (episodes is Resource.Success) {
val first = episodes.value.firstOrNull() val first = episodes.value.firstOrNull()
if (first != null) { if (first != null) {
resultPlaySeriesText.text = //"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" resultPlaySeriesText.text =
when { when {
first.season != null -> first.season != null ->
"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}" "${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}"
@ -805,8 +805,9 @@ class ResultFragmentTv : Fragment() {
} }
if (!hasLoadedEpisodesOnce) { if (!hasLoadedEpisodesOnce) {
hasLoadedEpisodesOnce = true hasLoadedEpisodesOnce = true
focusPlayButton() resultPlaySeries.isVisible = resultResumeSeries.isGone && !comingSoon
resultPlaySeries.requestFocus() resultEpisodesShow.isVisible = true && !comingSoon
resultPlaySeriesButton.requestFocus()
} }
} }
@ -874,14 +875,25 @@ class ResultFragmentTv : Fragment() {
resultNextAiring.setText(d.nextAiringEpisode) resultNextAiring.setText(d.nextAiringEpisode)
resultNextAiringTime.setText(d.nextAiringDate) resultNextAiringTime.setText(d.nextAiringDate)
resultPoster.setImage(d.posterImage) resultPoster.setImage(d.posterImage)
resultDescription.setTextHtml(d.plotText)
resultDescription.setOnClickListener { view -> var isExpanded = false
view.context?.let { ctx -> resultDescription.apply {
val builder: AlertDialog.Builder = setTextHtml(d.plotText)
AlertDialog.Builder(ctx, R.style.AlertDialogCustom) setOnClickListener {
builder.setMessage(d.plotText.asString(ctx).html()) if (isLayout(EMULATOR)) {
.setTitle(d.plotHeaderText.asString(ctx)) isExpanded = !isExpanded
.show() 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 comingSoon = d.comingSoon
resultTvComingSoon.isVisible = d.comingSoon resultTvComingSoon.isVisible = d.comingSoon
resultPlayMovie.isGone = d.comingSoon
resultPlaySeries.isGone = d.comingSoon
resultDataHolder.isGone = d.comingSoon
UIHelper.populateChips(resultTag, d.tags) UIHelper.populateChips(resultTag, d.tags)
resultCastItems.isGone = d.actors.isNullOrEmpty() resultCastItems.isGone = d.actors.isNullOrEmpty()

View File

@ -81,12 +81,12 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.setResultWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData import com.lagradost.cloudstream3.utils.DataStoreHelper.setSubscribedData
import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState import com.lagradost.cloudstream3.utils.DataStoreHelper.setVideoWatchState
import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData import com.lagradost.cloudstream3.utils.DataStoreHelper.updateSubscribedData
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
import com.lagradost.cloudstream3.utils.UIHelper.navigate import com.lagradost.cloudstream3.utils.UIHelper.navigate
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** This starts at 1 */ /** This starts at 1 */
data class EpisodeRange( data class EpisodeRange(
// used to index data // used to index data
@ -928,15 +928,20 @@ class ResultViewModel2 : ViewModel() {
) { ) {
val isSubscribed = _subscribeStatus.value ?: return val isSubscribed = _subscribeStatus.value ?: return
val response = currentResponse ?: return val response = currentResponse ?: return
if (response !is EpisodeResponse) return
val currentId = currentId ?: 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) { if (isSubscribed) {
removeSubscribedData(currentId) removeSubscribedData(currentId)
statusChangedCallback?.invoke(false) statusChangedCallback?.invoke(false)
_subscribeStatus.postValue(false) _subscribeStatus.postValue(if (response is EpisodeResponse) false else null)
MainActivity.reloadLibraryEvent(true)
} else { } else {
if (response !is EpisodeResponse) {
return
}
checkAndWarnDuplicates( checkAndWarnDuplicates(
context, context,
LibraryListType.SUBSCRIPTIONS, LibraryListType.SUBSCRIPTIONS,
@ -981,8 +986,8 @@ class ResultViewModel2 : ViewModel() {
) )
_subscribeStatus.postValue(true) _subscribeStatus.postValue(true)
statusChangedCallback?.invoke(true) statusChangedCallback?.invoke(true)
MainActivity.reloadLibraryEvent(true)
} }
} }
} }
@ -1693,14 +1698,8 @@ class ResultViewModel2 : ViewModel() {
LoadType.ExternalApp, LoadType.ExternalApp,
txt(R.string.episode_action_copy_link) txt(R.string.episode_action_copy_link)
) { (result, index) -> ) { (result, index) ->
val act = activity ?: return@acquireSingleLink
val serviceClipboard =
(act.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?)
?: return@acquireSingleLink
val link = result.links[index] val link = result.links[index]
val clip = ClipData.newPlainText(link.name, link.url) clipboardHelper(txt(link.name), link.url)
serviceClipboard.setPrimaryClip(clip)
showToast(R.string.copy_link_toast, Toast.LENGTH_SHORT)
} }
} }
@ -2052,12 +2051,15 @@ class ResultViewModel2 : ViewModel() {
} }
private fun postSubscription(loadResponse: LoadResponse) { private fun postSubscription(loadResponse: LoadResponse) {
val id = loadResponse.getId()
val data = getSubscribedData(id)
if (loadResponse.isEpisodeBased()) { if (loadResponse.isEpisodeBased()) {
val id = loadResponse.getId()
val data = getSubscribedData(id)
updateSubscribedData(id, data, loadResponse as? EpisodeResponse) updateSubscribedData(id, data, loadResponse as? EpisodeResponse)
val isSubscribed = data != null _subscribeStatus.postValue(data != null)
_subscribeStatus.postValue(isSubscribed) }
// 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<String, String>? = null, override var posterHeaders: Map<String, String>? = null,
override var backgroundPosterUrl: String? = null, override var backgroundPosterUrl: String? = null,
override var contentRating: String? = null, override var contentRating: String? = null,
val id : Int?,
) : LoadResponse ) : LoadResponse
fun loadSmall(activity: Activity?, searchResponse : SearchResponse) = ioSafe { 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 api = APIHolder.getApiFromNameNull(searchResponse.apiName) ?: APIHolder.getApiFromUrlNull(searchResponse.url) ?: APIRepository.noneApi
val repo = APIRepository(api) val repo = APIRepository(api)
val response = LoadResponseFromSearch(name = searchResponse.name, url = searchResponse.url, apiName = api.name, type = searchResponse.type ?: TvType.Others, 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) { if (searchResponse is SyncAPI.LibraryItem) {
this.plot = searchResponse.plot this.plot = searchResponse.plot
this.rating = searchResponse.personalRating?.times(100) ?: searchResponse.rating this.rating = searchResponse.personalRating?.times(100) ?: searchResponse.rating
@ -2612,12 +2615,14 @@ class ResultViewModel2 : ViewModel() {
this.tags = searchResponse.tags this.tags = searchResponse.tags
} }
} }
val mainId = searchResponse.id ?: response.getId() val mainId = response.getId()
postSuccessful( postSuccessful(
loadResponse = response, loadResponse = response,
mainId = mainId, mainId = mainId,
apiRepository = repo, updateEpisodes = false, updateFillers = false) apiRepository = repo,
updateEpisodes = false,
updateFillers = false)
} }
fun load( fun load(

View File

@ -6,7 +6,8 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.databinding.ResultSelectionBinding 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<UiText?, Any> typealias SelectData = Pair<UiText?, Any>
@ -72,8 +73,7 @@ class SelectAdaptor(val callback: (Any) -> Unit) : RecyclerView.Adapter<Recycler
fun bind( fun bind(
data: SelectData, isSelected: Boolean, callback: (Any) -> Unit data: SelectData, isSelected: Boolean, callback: (Any) -> Unit
) { ) {
val isTrueTv = isTrueTvSettings() if (isLayout(TV)) {
if (isTrueTv) {
item.isFocusable = true item.isFocusable = true
item.isFocusableInTouchMode = true item.isFocusableInTouchMode = true
} }

View File

@ -19,6 +19,13 @@ sealed class UiText {
data class DynamicString(val value: String) : UiText() { data class DynamicString(val value: String) : UiText() {
override fun toString(): String = value 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( class StringResource(
@ -27,6 +34,16 @@ sealed class UiText {
) : UiText() { ) : UiText() {
override fun toString(): String = override fun toString(): String =
"resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}" "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? { fun asStringNull(context: Context?): String? {

View File

@ -46,6 +46,7 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.APIRepository 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
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan 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.home.ParentItemAdapter
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings 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.ownHide
import com.lagradost.cloudstream3.utils.AppUtils.ownShow import com.lagradost.cloudstream3.utils.AppUtils.ownShow
import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus import com.lagradost.cloudstream3.utils.AppUtils.setDefaultFocus
@ -107,13 +109,16 @@ class SearchFragment : Fragment() {
) )
bottomSheetDialog?.ownShow() bottomSheetDialog?.ownShow()
val layout = if (isTvSettings()) R.layout.fragment_search_tv else R.layout.fragment_search
val root = inflater.inflate(layout, container, false) binding = try {
// TODO TRYCATCH val layout = if (isLayout(TV or EMULATOR)) R.layout.fragment_search_tv else R.layout.fragment_search
binding = FragmentSearchBinding.bind(root) val root = inflater.inflate(layout, container, false)
FragmentSearchBinding.bind(root)
} catch (t : Throwable) {
FragmentSearchBinding.inflate(inflater)
}
return root return binding?.root
} }
private fun fixGrid() { private fun fixGrid() {
@ -157,7 +162,8 @@ class SearchFragment : Fragment() {
**/ **/
fun search(query: String?) { fun search(query: String?) {
if (query == null) return if (query == null) return
// don't resume state from prev search
(binding?.searchMasterRecycler?.adapter as? BaseAdapter<*,*>)?.clear()
context?.let { ctx -> context?.let { ctx ->
val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW } val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }
.map { it.ordinal.toString() }.toSet() .map { it.ordinal.toString() }.toSet()
@ -369,7 +375,7 @@ class SearchFragment : Fragment() {
selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList() selectedSearchTypes = DataStoreHelper.searchPreferenceTags.toMutableList()
if (isTrueTvSettings()) { if (isLayout(TV)) {
binding?.searchFilter?.isFocusable = true binding?.searchFilter?.isFocusable = true
binding?.searchFilter?.isFocusableInTouchMode = true binding?.searchFilter?.isFocusableInTouchMode = true
} }
@ -502,8 +508,8 @@ class SearchFragment : Fragment() {
}*/ }*/
//main_search.onActionViewExpanded()*/ //main_search.onActionViewExpanded()*/
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> = val masterAdapter =
ParentItemAdapter(mutableListOf(), { callback -> ParentItemAdapter(fragment = this, id = "masterAdapter".hashCode(), { callback ->
SearchHelper.handleSearchClickCallback(callback) SearchHelper.handleSearchClickCallback(callback)
}, { item -> }, { item ->
bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = { bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = {

View File

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.ui.search package com.lagradost.cloudstream3.ui.search
import android.app.Activity
import android.widget.Toast import android.widget.Toast
import com.lagradost.cloudstream3.CommonActivity.activity import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.showToast 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.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP 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.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.VideoDownloadHelper import com.lagradost.cloudstream3.utils.VideoDownloadHelper
@ -56,7 +56,7 @@ object SearchHelper {
} }
} }
SEARCH_ACTION_SHOW_METADATA -> { 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 { (activity as? MainActivity?)?.apply {
loadPopup(callback.card) loadPopup(callback.card)
} ?: kotlin.run { } ?: kotlin.run {

View File

@ -17,7 +17,8 @@ import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.SearchResponse import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.isMovieType import com.lagradost.cloudstream3.isMovieType
import com.lagradost.cloudstream3.syncproviders.SyncAPI 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.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStoreHelper import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
@ -164,7 +165,7 @@ object SearchResultBuilder {
bg.isFocusable = false bg.isFocusable = false
bg.isFocusableInTouchMode = false bg.isFocusableInTouchMode = false
if(!isTrueTvSettings()) { if(!isLayout(TV)) {
bg.setOnClickListener { bg.setOnClickListener {
click(it) click(it)
} }
@ -207,7 +208,7 @@ object SearchResultBuilder {
*/ */
if (isTrueTvSettings()) { if (isLayout(TV)) {
// bg.isFocusable = true // bg.isFocusable = true
// bg.isFocusableInTouchMode = true // bg.isFocusableInTouchMode = true
// bg.touchscreenBlocksFocus = false // bg.touchscreenBlocksFocus = false

View File

@ -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
}
}

View File

@ -29,8 +29,10 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.simklAp
import com.lagradost.cloudstream3.syncproviders.AuthAPI import com.lagradost.cloudstream3.syncproviders.AuthAPI
import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI import com.lagradost.cloudstream3.syncproviders.InAppAuthAPI
import com.lagradost.cloudstream3.syncproviders.OAuth2API 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.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.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags 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.setUpToolbar
@ -76,7 +78,7 @@ class SettingsAccount : PreferenceFragmentCompat() {
showAccountSwitch(activity, api) showAccountSwitch(activity, api)
} }
if (isTvSettings()) { if (isLayout(TV or EMULATOR)) {
binding.accountSwitchAccount.requestFocus() binding.accountSwitchAccount.requestFocus()
} }
} }
@ -140,7 +142,7 @@ class SettingsAccount : PreferenceFragmentCompat() {
binding.loginUsernameInput to api.requiresUsername binding.loginUsernameInput to api.requiresUsername
) )
if (isTvSettings()) { if (isLayout(TV or EMULATOR)) {
visibilityMap.forEach { (input, isVisible) -> visibilityMap.forEach { (input, isVisible) ->
input.isVisible = isVisible input.isVisible = isVisible

View File

@ -1,9 +1,5 @@
package com.lagradost.cloudstream3.ui.settings 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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -16,16 +12,20 @@ import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar 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.R
import com.lagradost.cloudstream3.databinding.MainSettingsBinding import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.ui.home.HomeFragment 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.navigate
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx import com.lagradost.cloudstream3.utils.UIHelper.toPx
@ -33,10 +33,6 @@ import java.io.File
class SettingsFragment : Fragment() { class SettingsFragment : Fragment() {
companion object { companion object {
var beneneCount = 0
private var isTv: Boolean = false
private var isTrueTv: Boolean = false
fun PreferenceFragmentCompat?.getPref(id: Int): Preference? { fun PreferenceFragmentCompat?.getPref(id: Int): Preference? {
if (this == null) return null 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. * On TV you cannot properly scroll to the bottom of settings, this fixes that.
* */ * */
fun PreferenceFragmentCompat.setPaddingBottom() { fun PreferenceFragmentCompat.setPaddingBottom() {
if (isTvSettings()) { if (isLayout(TV or EMULATOR)) {
listView?.setPadding(0, 0, 0, 100.toPx) listView?.setPadding(0, 0, 0, 100.toPx)
} }
} }
fun PreferenceFragmentCompat.setToolBarScrollFlags() { fun PreferenceFragmentCompat.setToolBarScrollFlags() {
if (isTvSettings()) { if (isLayout(TV or EMULATOR)) {
val settingsAppbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) val settingsAppbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar)
settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> { settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> {
@ -67,7 +63,7 @@ class SettingsFragment : Fragment() {
} }
} }
fun Fragment?.setToolBarScrollFlags() { fun Fragment?.setToolBarScrollFlags() {
if (isTvSettings()) { if (isLayout(TV or EMULATOR)) {
val settingsAppbar = this?.view?.findViewById<MaterialToolbar>(R.id.settings_toolbar) val settingsAppbar = this?.view?.findViewById<MaterialToolbar>(R.id.settings_toolbar)
settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> { settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> {
@ -86,7 +82,7 @@ class SettingsFragment : Fragment() {
activity?.onBackPressedDispatcher?.onBackPressed() activity?.onBackPressedDispatcher?.onBackPressed()
} }
} }
fixPaddingStatusbar(settingsToolbar) UIHelper.fixPaddingStatusbar(settingsToolbar)
} }
fun Fragment?.setUpToolbar(@StringRes title: Int) { fun Fragment?.setUpToolbar(@StringRes title: Int) {
@ -101,7 +97,7 @@ class SettingsFragment : Fragment() {
activity?.onBackPressedDispatcher?.onBackPressed() activity?.onBackPressedDispatcher?.onBackPressed()
} }
} }
fixPaddingStatusbar(settingsToolbar) UIHelper.fixPaddingStatusbar(settingsToolbar)
} }
fun getFolderSize(dir: File): Long { fun getFolderSize(dir: File): Long {
@ -117,60 +113,7 @@ class SettingsFragment : Fragment() {
return size 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() { override fun onDestroyView() {
binding = null binding = null
super.onDestroyView() super.onDestroyView()
@ -195,8 +138,6 @@ class SettingsFragment : Fragment() {
// used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}") // used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}")
val isTrueTv = isTrueTvSettings()
for (syncApi in accountManagers) { for (syncApi in accountManagers) {
val login = syncApi.loginInfo() val login = syncApi.loginInfo()
val pic = login?.profilePicture ?: continue val pic = login?.profilePicture ?: continue
@ -224,7 +165,7 @@ class SettingsFragment : Fragment() {
setOnClickListener { setOnClickListener {
navigate(navigationId) navigate(navigationId)
} }
if (isTrueTv) { if (isLayout(TV)) {
isFocusable = true isFocusable = true
isFocusableInTouchMode = true isFocusableInTouchMode = true
} }
@ -232,9 +173,20 @@ class SettingsFragment : Fragment() {
} }
// Default focus on TV // Default focus on TV
if (isTrueTv) { if (isLayout(TV)) {
settingsGeneral.requestFocus() 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
}
} }
} }

View File

@ -27,6 +27,7 @@ import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.ui.EasterEggMonke 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.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom 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.setToolBarScrollFlags
@ -378,30 +379,30 @@ class SettingsGeneral : PreferenceFragmentCompat() {
} }
try { try {
SettingsFragment.beneneCount = beneneCount =
settingsManager.getInt(getString(R.string.benene_count), 0) settingsManager.getInt(getString(R.string.benene_count), 0)
getPref(R.string.benene_count)?.let { pref -> getPref(R.string.benene_count)?.let { pref ->
pref.summary = 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 R.string.benene_count_text
).format( ).format(
SettingsFragment.beneneCount beneneCount
) )
pref.setOnPreferenceClickListener { pref.setOnPreferenceClickListener {
try { try {
SettingsFragment.beneneCount++ beneneCount++
if (SettingsFragment.beneneCount%20 == 0) { if (beneneCount%20 == 0) {
val intent = Intent(context, EasterEggMonke::class.java) val intent = Intent(context, EasterEggMonke::class.java)
startActivity(intent) startActivity(intent)
} }
settingsManager.edit().putInt( settingsManager.edit().putInt(
getString(R.string.benene_count), getString(R.string.benene_count),
SettingsFragment.beneneCount beneneCount
) )
.apply() .apply()
it.summary = it.summary =
getString(R.string.benene_count_text).format(SettingsFragment.beneneCount) getString(R.string.benene_count_text).format(beneneCount)
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
} }

View File

@ -9,11 +9,11 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchQuality import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder 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.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom 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.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar 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.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog

View File

@ -1,10 +1,6 @@
package com.lagradost.cloudstream3.ui.settings 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.Bundle
import android.os.TransactionTooLargeException
import android.view.View import android.view.View
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog 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.mvvm.logError
import com.lagradost.cloudstream3.network.initClient import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.services.BackupWorkManager 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.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom 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.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.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog 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.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.VideoDownloadManager import com.lagradost.cloudstream3.utils.VideoDownloadManager
@ -117,22 +115,15 @@ class SettingsUpdates : PreferenceFragmentCompat() {
binding.text1.text = text binding.text1.text = text
binding.copyBtt.setOnClickListener { binding.copyBtt.setOnClickListener {
// Can crash on too much text clipboardHelper(txt("Logcat"), text)
try { dialog.dismissSafe(activity)
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)
}
} }
binding.clearBtt.setOnClickListener { binding.clearBtt.setOnClickListener {
Runtime.getRuntime().exec("logcat -c") Runtime.getRuntime().exec("logcat -c")
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }
binding.saveBtt.setOnClickListener { binding.saveBtt.setOnClickListener {
var fileStream: OutputStream? = null var fileStream: OutputStream? = null
try { try {
@ -153,9 +144,11 @@ class SettingsUpdates : PreferenceFragmentCompat() {
fileStream?.closeQuietly() fileStream?.closeQuietly()
} }
} }
binding.closeBtt.setOnClickListener { binding.closeBtt.setOnClickListener {
dialog.dismissSafe(activity) dialog.dismissSafe(activity)
} }
return@setOnPreferenceClickListener true return@setOnPreferenceClickListener true
} }

View File

@ -29,7 +29,8 @@ import com.lagradost.cloudstream3.plugins.RepositoryManager
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout import com.lagradost.cloudstream3.ui.result.setLinearListLayout
import com.lagradost.cloudstream3.ui.result.setText 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.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog import com.lagradost.cloudstream3.utils.AppUtils.downloadAllPluginsDialog
@ -97,7 +98,7 @@ class ExtensionsFragment : Fragment() {
nextLeft = R.id.nav_rail_view nextLeft = R.id.nav_rail_view
) )
if (!isTrueTvSettings()) if (!isLayout(TV))
binding?.addRepoButton?.let { button -> binding?.addRepoButton?.let { button ->
button.post { button.post {
setPadding( setPadding(
@ -286,7 +287,7 @@ class ExtensionsFragment : Fragment() {
} }
} }
val isTv = isTrueTvSettings() val isTv = isLayout(TV)
binding?.apply { binding?.apply {
addRepoButton.isGone = isTv addRepoButton.isGone = isTv
addRepoButtonImageviewHolder.isVisible = isTv addRepoButtonImageviewHolder.isVisible = isTv

View File

@ -17,7 +17,8 @@ import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
import com.lagradost.cloudstream3.ui.result.setText import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt 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.AppUtils.html
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main import com.lagradost.cloudstream3.utils.Coroutines.main
@ -44,7 +45,7 @@ class PluginAdapter(
private val plugins: MutableList<PluginViewData> = mutableListOf() private val plugins: MutableList<PluginViewData> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 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) val inflated = LayoutInflater.from(parent.context).inflate(layout, parent, false)
return PluginViewHolder( return PluginViewHolder(

View File

@ -17,7 +17,9 @@ import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
import com.lagradost.cloudstream3.ui.result.setLinearListLayout 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.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.ui.settings.appLanguages import com.lagradost.cloudstream3.ui.settings.appLanguages
@ -155,7 +157,7 @@ class PluginsFragment : Fragment() {
pluginViewModel.handlePluginAction(activity, url, it, isLocal) 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. // Scrolling down does not reveal the whole RecyclerView on TV, add to bypass that.
binding?.pluginRecyclerView?.setPadding(0, 0, 0, 200.toPx) binding?.pluginRecyclerView?.setPadding(0, 0, 0, 200.toPx)
} }

View File

@ -1,22 +1,18 @@
package com.lagradost.cloudstream3.ui.settings.extensions 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.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding 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.R
import com.lagradost.cloudstream3.databinding.RepositoryItemBinding import com.lagradost.cloudstream3.databinding.RepositoryItemBinding
import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding import com.lagradost.cloudstream3.databinding.RepositoryItemTvBinding
import com.lagradost.cloudstream3.plugins.RepositoryManager.PREBUILT_REPOSITORIES 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( class RepoAdapter(
val isSetup: Boolean, val isSetup: Boolean,
@ -28,7 +24,7 @@ class RepoAdapter(
private val repositories: MutableList<RepositoryData> = mutableListOf() private val repositories: MutableList<RepositoryData> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { 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), LayoutInflater.from(parent.context),
parent, parent,
false false
@ -121,13 +117,9 @@ class RepoAdapter(
} }
repositoryItemRoot.setOnLongClickListener { repositoryItemRoot.setOnLongClickListener {
val clipboardManager = val shareableRepoData = "${repositoryData.name} : \n ${repositoryData.url}"
activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager? clipboardHelper(txt(R.string.repo_copy_label), shareableRepoData)
clipboardManager?.setPrimaryClip(ClipData.newPlainText("RepoUrl", repositoryData.url)) true
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
showToast(R.string.copyRepoUrl, Toast.LENGTH_SHORT)
}
return@setOnLongClickListener true
} }
mainText.text = repositoryData.name mainText.text = repositoryData.name

View File

@ -11,7 +11,8 @@ import com.lagradost.cloudstream3.databinding.FragmentTestingBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.mvvm.observe import com.lagradost.cloudstream3.mvvm.observe
import com.lagradost.cloudstream3.mvvm.observeNullable 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.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar 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?.isFocusableInTouchMode = true
providerTest.playPauseButton?.requestFocus() providerTest.playPauseButton?.requestFocus()
} }
@ -75,7 +76,7 @@ class TestFragment : Fragment() {
fun focusRecyclerView() { fun focusRecyclerView() {
// Hack to make it possible to focus the recyclerview. // Hack to make it possible to focus the recyclerview.
if (isTrueTvSettings()) { if (isLayout(TV)) {
providerTestRecyclerView.requestFocus() providerTestRecyclerView.requestFocus()
providerTestAppbar.setExpanded(false, true) providerTestAppbar.setExpanded(false, true)
} }

View File

@ -13,8 +13,8 @@ import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.fasterxml.jackson.annotation.JsonProperty
import androidx.media3.common.text.Cue 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.google.android.gms.cast.TextTrackStyle.* import com.google.android.gms.cast.TextTrackStyle.*
import com.jaredrummler.android.colorpicker.ColorPickerDialog 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.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.ChromecastSubtitleSettingsBinding 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.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
@ -173,7 +175,7 @@ class ChromecastSubtitlesFragment : Fragment() {
state = getCurrentSavedStyle() state = getCurrentSavedStyle()
context?.updateState() context?.updateState()
val isTvSettings = isTvSettings() val isTvSettings = isLayout(TV or EMULATOR)
fun View.setFocusableInTv() { fun View.setFocusableInTv() {
this.isFocusableInTouchMode = isTvSettings this.isFocusableInTouchMode = isTvSettings

View File

@ -28,7 +28,9 @@ import com.lagradost.cloudstream3.CommonActivity.onDialogDismissedEvent
import com.lagradost.cloudstream3.CommonActivity.showToast import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.SubtitleSettingsBinding 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.DataStore.setKey
import com.lagradost.cloudstream3.utils.Event import com.lagradost.cloudstream3.utils.Event
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
@ -252,7 +254,7 @@ class SubtitlesFragment : Fragment() {
state = getCurrentSavedStyle() state = getCurrentSavedStyle()
context?.updateState() context?.updateState()
val isTvTrueSettings = isTrueTvSettings() val isTvTrueSettings = isLayout(TV)
fun View.setFocusableInTv() { fun View.setFocusableInTv() {
this.isFocusableInTouchMode = isTvTrueSettings this.isFocusableInTouchMode = isTvTrueSettings

View File

@ -61,8 +61,7 @@ import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.appStri
import com.lagradost.cloudstream3.syncproviders.providers.Kitsu import com.lagradost.cloudstream3.syncproviders.providers.Kitsu
import com.lagradost.cloudstream3.ui.WebviewFragment import com.lagradost.cloudstream3.ui.WebviewFragment
import com.lagradost.cloudstream3.ui.result.ResultFragment import com.lagradost.cloudstream3.ui.result.ResultFragment
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Companion.downloadAll import com.lagradost.cloudstream3.ui.settings.extensions.PluginsViewModel.Companion.downloadAll
import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData import com.lagradost.cloudstream3.ui.settings.extensions.RepositoryData
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -78,7 +77,6 @@ import okhttp3.Cache
import java.io.* import java.io.*
import java.net.URL import java.net.URL
import java.net.URLDecoder import java.net.URLDecoder
import kotlin.system.measureTimeMillis
object AppUtils { object AppUtils {
fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) { fun RecyclerView.setMaxViewPoolSize(maxViewTypeId: Int, maxPoolSize: Int) {
@ -583,7 +581,7 @@ object AppUtils {
//private val viewModel: ResultViewModel by activityViewModels() //private val viewModel: ResultViewModel by activityViewModels()
private fun getResultsId(): Int { private fun getResultsId(): Int {
return if (isTvSettings()) { return if (Globals.isLayout(Globals.TV or Globals.EMULATOR)) {
R.id.global_to_navigation_results_tv R.id.global_to_navigation_results_tv
} else { } else {
R.id.global_to_navigation_results_phone 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. * Sets the focus to the negative button when in TV and Emulator layout.
**/ **/
fun AlertDialog.setDefaultFocus(buttonFocus: Int = DialogInterface.BUTTON_NEGATIVE) { fun AlertDialog.setDefaultFocus(buttonFocus: Int = DialogInterface.BUTTON_NEGATIVE) {
if (!isTvSettings()) return if (!Globals.isLayout(Globals.TV or Globals.EMULATOR)) return
this.getButton(buttonFocus).run { this.getButton(buttonFocus).run {
isFocusableInTouchMode = true isFocusableInTouchMode = true
requestFocus() requestFocus()

View File

@ -32,7 +32,6 @@ import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs
import com.lagradost.cloudstream3.utils.DataStore.mapper 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.checkWrite
import com.lagradost.cloudstream3.utils.UIHelper.requestRW import com.lagradost.cloudstream3.utils.UIHelper.requestRW
import com.lagradost.cloudstream3.utils.VideoDownloadManager.setupStream import com.lagradost.cloudstream3.utils.VideoDownloadManager.setupStream
@ -256,8 +255,12 @@ object BackupUtils {
map: Map<String, T>?, map: Map<String, T>?,
isEditingAppSettings: Boolean = false isEditingAppSettings: Boolean = false
) { ) {
map?.filter { it.key.isTransferable() }?.forEach { val editor = DataStore.editor(this, isEditingAppSettings)
setKeyRaw(it.key, it.value, isEditingAppSettings) map?.forEach {
if (it.key.isTransferable()) {
editor.setKeyRaw(it.key, it.value)
}
} }
editor.apply()
} }
} }

View File

@ -50,6 +50,28 @@ class PreferenceDelegate<T : Any>(
} }
} }
/** 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<T> 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<String> != null) -> editor.putStringSet(path, value as Set<String>)
}
}
fun apply() {
editor.apply()
System.gc()
}
}
object DataStore { object DataStore {
val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule()) val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
@ -66,22 +88,10 @@ object DataStore {
return "${folder}/${path}" return "${folder}/${path}"
} }
fun <T> Context.setKeyRaw(path: String, value: T, isEditingAppSettings: Boolean = false) { fun editor(context : Context, isEditingAppSettings: Boolean = false) : Editor {
try { val editor: SharedPreferences.Editor =
val editor: SharedPreferences.Editor = if (isEditingAppSettings) context.getDefaultSharedPrefs().edit() else context.getSharedPrefs().edit()
if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit() return Editor(editor)
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<String> != null) -> editor.putStringSet(path, value as Set<String>)
}
editor.apply()
} catch (e: Exception) {
logError(e)
}
} }
fun Context.getDefaultSharedPrefs(): SharedPreferences { fun Context.getDefaultSharedPrefs(): SharedPreferences {

View File

@ -21,7 +21,9 @@ import com.lagradost.cloudstream3.databinding.BottomInputDialogBinding
import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding import com.lagradost.cloudstream3.databinding.BottomSelectionDialogBinding
import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding import com.lagradost.cloudstream3.databinding.BottomTextDialogBinding
import com.lagradost.cloudstream3.databinding.OptionsPopupTvBinding 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.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIconsAndNoStringRes
import com.lagradost.cloudstream3.utils.UIHelper.setImage import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -54,7 +56,7 @@ object SingleSelectionHelper {
) { ) {
if (this == null) return if (this == null) return
if (isTvSettings()) { if (isLayout(TV or EMULATOR)) {
val binding = OptionsPopupTvBinding.inflate(layoutInflater) val binding = OptionsPopupTvBinding.inflate(layoutInflater)
val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom) val dialog = AlertDialog.Builder(this, R.style.AlertDialogCustom)
.setView(binding.root) .setView(binding.root)

View File

@ -5,6 +5,8 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.AppOpsManager import android.app.AppOpsManager
import android.app.Dialog import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
@ -14,12 +16,15 @@ import android.graphics.Color
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.TransactionTooLargeException
import android.util.Log
import android.view.* import android.view.*
import android.view.ViewGroup.MarginLayoutParams import android.view.ViewGroup.MarginLayoutParams
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.ImageView import android.widget.ImageView
import android.widget.ListAdapter import android.widget.ListAdapter
import android.widget.ListView import android.widget.ListView
import android.widget.Toast.LENGTH_LONG
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
@ -30,14 +35,12 @@ import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import androidx.core.graphics.alpha import androidx.core.graphics.alpha
import androidx.core.graphics.blue import androidx.core.graphics.blue
import androidx.core.graphics.drawable.toBitmapOrNull import androidx.core.graphics.drawable.toBitmapOrNull
import androidx.core.graphics.green import androidx.core.graphics.green
import androidx.core.graphics.red 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.marginBottom
import androidx.core.view.marginLeft import androidx.core.view.marginLeft
import androidx.core.view.marginRight 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.Chip
import com.google.android.material.chip.ChipDrawable import com.google.android.material.chip.ChipDrawable
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.CommonActivity.activity 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.R
import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.result.UiImage import com.lagradost.cloudstream3.ui.result.UiImage
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings import com.lagradost.cloudstream3.ui.result.UiText
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings 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 jp.wasabeef.glide.transformations.BlurTransformation
import kotlin.math.roundToInt import kotlin.math.roundToInt
object UIHelper { object UIHelper {
val Int.toPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt() val Int.toPx: Int get() = (this * Resources.getSystem().displayMetrics.density).toInt()
val Float.toPx: Float get() = (this * Resources.getSystem().displayMetrics.density) 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<ClipboardManager>()?.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. * Sets ListView height dynamically based on the height of the items.
@ -434,7 +469,7 @@ object UIHelper {
} }
fun Context.getStatusBarHeight(): Int { fun Context.getStatusBarHeight(): Int {
if (isTvSettings()) { if (isLayout(Globals.TV or EMULATOR)) {
return 0 return 0
} }
@ -536,7 +571,7 @@ object UIHelper {
(View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN) (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
//} //}
changeStatusBarState(isEmulatorSettings()) changeStatusBarState(isLayout(EMULATOR))
} }
fun Context.shouldShowPIPMode(isInPlayer: Boolean): Boolean { fun Context.shouldShowPIPMode(isInPlayer: Boolean): Boolean {

View File

@ -41,22 +41,34 @@
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:orientation="vertical"> android:orientation="vertical">
<FrameLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<TextView <TextView
android:id="@+id/resultview_preview_title" android:id="@+id/resultview_preview_title"
android:layout_width="wrap_content" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:layout_weight="1"
android:textColor="?attr/textColor" android:textColor="?attr/textColor"
android:textSize="16sp" android:textSize="16sp"
android:layout_gravity="start|center_vertical"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginEnd="25dp" tools:text="The Perfect Run" />
tools:text="The Perfect Run">
</TextView> <ImageView
android:id="@+id/resultview_preview_subscribe"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_gravity="end|center_vertical"
android:layout_margin="5dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/subscribe_tooltip"
android:elevation="10dp"
android:nextFocusDown="@id/resultview_preview_bookmark"
android:src="@drawable/baseline_notifications_none_24"
app:tint="?attr/textColor" />
<ImageView <ImageView
android:id="@+id/resultview_preview_favorite" android:id="@+id/resultview_preview_favorite"
@ -66,11 +78,13 @@
android:layout_margin="5dp" android:layout_margin="5dp"
android:background="?android:attr/selectableItemBackgroundBorderless" android:background="?android:attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/favorite"
android:elevation="10dp" android:elevation="10dp"
android:nextFocusDown="@id/resultview_preview_bookmark" android:nextFocusDown="@id/resultview_preview_bookmark"
android:src="@drawable/ic_baseline_favorite_border_24" android:src="@drawable/ic_baseline_favorite_border_24"
app:tint="?attr/textColor" /> app:tint="?attr/textColor" />
</FrameLayout>
</LinearLayout>
<com.lagradost.cloudstream3.widget.FlowLayout <com.lagradost.cloudstream3.widget.FlowLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@ -134,38 +148,38 @@
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
android:padding="7dp"
android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="7dp">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/resultview_preview_bookmark" android:id="@+id/resultview_preview_bookmark"
style="@style/BlackButton"
android:layout_width="50dp"
android:layout_weight="1" android:layout_weight="1"
android:nextFocusUp="@id/resultview_preview_favorite"
android:nextFocusRight="@id/resultview_preview_more_info" android:nextFocusRight="@id/resultview_preview_more_info"
tools:visibility="visible" android:nextFocusUp="@id/resultview_preview_favorite"
app:icon="@drawable/ic_baseline_bookmark_24" app:icon="@drawable/ic_baseline_bookmark_24"
tools:text="Bookmark" tools:text="Bookmark"
style="@style/BlackButton"
android:layout_width="50dp" /> tools:visibility="visible" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/resultview_preview_more_info" android:id="@+id/resultview_preview_more_info"
style="@style/WhiteButton"
android:layout_width="50dp"
android:layout_weight="1" android:layout_weight="1"
android:nextFocusUp="@id/resultview_preview_favorite"
android:nextFocusLeft="@id/resultview_preview_bookmark" android:nextFocusLeft="@id/resultview_preview_bookmark"
tools:visibility="visible" android:nextFocusUp="@id/resultview_preview_favorite"
app:icon="@drawable/ic_baseline_open_in_new_24"
android:text="@string/home_more_info" android:text="@string/home_more_info"
style="@style/WhiteButton" app:icon="@drawable/ic_baseline_open_in_new_24"
android:layout_width="50dp" /> tools:visibility="visible" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -409,8 +409,8 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="10"
android:foreground="@drawable/outline_drawable" android:foreground="@drawable/outline_drawable"
android:maxLength="1000"
android:nextFocusUp="@id/result_back" android:nextFocusUp="@id/result_back"
android:nextFocusDown="@id/result_bookmark_Button" android:nextFocusDown="@id/result_bookmark_Button"
android:paddingTop="5dp" android:paddingTop="5dp"

View File

@ -271,7 +271,9 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:id="@+id/result_play_movie" android:id="@+id/result_play_movie"
android:orientation="vertical"> android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/result_play_movie_button" android:id="@+id/result_play_movie_button"
@ -323,7 +325,9 @@ https://developer.android.com/design/ui/tv/samples/jet-fit
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:orientation="vertical"> android:orientation="vertical"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/result_resume_series_button" android:id="@+id/result_resume_series_button"

View File

@ -105,9 +105,10 @@
android:text="@string/extensions" /> android:text="@string/extensions" />
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:id="@+id/app_version_info"
android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center_horizontal" android:layout_gravity="center"
android:orientation="horizontal"> android:orientation="horizontal">
<TextView <TextView
@ -119,7 +120,7 @@
android:textColor="?attr/textColor" /> android:textColor="?attr/textColor" />
<TextView <TextView
android:id="@+id/textView3" android:id="@+id/delimiter0"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:gravity="center" android:gravity="center"
@ -135,6 +136,25 @@
android:padding="10dp" android:padding="10dp"
android:text="@string/commit_hash" android:text="@string/commit_hash"
android:textColor="?attr/textColor" /> android:textColor="?attr/textColor" />
<TextView
android:id="@+id/delimiter1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="10dp"
android:text="•"
android:textColor="?attr/textColor" />
<TextView
android:id="@+id/build_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="10dp"
android:textColor="?attr/textColor" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -6,7 +6,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/outline_drawable" android:background="@drawable/outline_drawable"
android:nextFocusRight="@id/action_button" android:nextFocusRight="@id/action_settings"
android:orientation="horizontal" android:orientation="horizontal"
android:clickable="true" android:clickable="true"
android:focusable="true" android:focusable="true"
@ -117,6 +117,9 @@
android:background="@drawable/outline_drawable" android:background="@drawable/outline_drawable"
android:contentDescription="@string/title_settings" android:contentDescription="@string/title_settings"
android:visibility="gone" android:visibility="gone"
android:focusable="true"
android:nextFocusLeft="@id/repository_item_root"
android:nextFocusRight="@id/action_button"
app:srcCompat="@drawable/ic_baseline_tune_24" app:srcCompat="@drawable/ic_baseline_tune_24"
tools:visibility="visible" /> tools:visibility="visible" />
@ -130,7 +133,7 @@
android:clickable="true" android:clickable="true"
android:contentDescription="@string/download" android:contentDescription="@string/download"
android:focusable="true" android:focusable="true"
android:nextFocusLeft="@id/repository_item_root" android:nextFocusLeft="@id/action_settings"
android:padding="12dp" android:padding="12dp"
tools:src="@drawable/ic_baseline_add_24" /> tools:src="@drawable/ic_baseline_add_24" />

View File

@ -174,8 +174,8 @@
<string name="sort_close">Close</string> <string name="sort_close">Close</string>
<string name="sort_clear">Clear</string> <string name="sort_clear">Clear</string>
<string name="sort_save">Save</string> <string name="sort_save">Save</string>
<string name="copyTitle">Title copied!</string> <string name="repo_copy_label">Repository name and URL</string>
<string name="copyRepoUrl">Repo URL copied!</string> <string name="toast_copied">copied!</string>
<string name="subscribe_tooltip">New episode notification</string> <string name="subscribe_tooltip">New episode notification</string>
<string name="result_search_tooltip">Search in other extensions</string> <string name="result_search_tooltip">Search in other extensions</string>
<string name="recommendations_tooltip">Show recommendations</string> <string name="recommendations_tooltip">Show recommendations</string>
@ -647,6 +647,8 @@
<string name="history">History</string> <string name="history">History</string>
<string name="enable_skip_op_from_database_des">Show skip popups for opening/ending</string> <string name="enable_skip_op_from_database_des">Show skip popups for opening/ending</string>
<string name="clipboard_too_large">Too much text. Unable to save to clipboard.</string> <string name="clipboard_too_large">Too much text. Unable to save to clipboard.</string>
<string name="clipboard_permission_error">Error accessing Clipboard, Please try again.</string>
<string name="clipboard_unknown_error">Error copying, Please copy logcat and contact app support.</string>
<string name="action_mark_as_watched">Mark as watched</string> <string name="action_mark_as_watched">Mark as watched</string>
<string name="action_remove_from_watched">Remove from watched</string> <string name="action_remove_from_watched">Remove from watched</string>
<string name="confirm_exit_dialog">Are you sure you want to exit\?</string> <string name="confirm_exit_dialog">Are you sure you want to exit\?</string>

View File

@ -4,6 +4,7 @@ buildscript {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath("com.android.tools.build:gradle:8.2.1") classpath("com.android.tools.build:gradle:8.2.1")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")