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

View File

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

View File

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

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.providers.SimklApi
import com.lagradost.cloudstream3.ui.player.SubtitleData
import com.lagradost.cloudstream3.ui.result.ResultViewModel2
import com.lagradost.cloudstream3.utils.*
import com.lagradost.cloudstream3.utils.AppUtils.toJson
import com.lagradost.cloudstream3.utils.Coroutines.mainWork
@ -119,7 +120,8 @@ object APIHolder {
}
fun LoadResponse.getId(): Int {
return getLoadResponseIdFromUrl(url, apiName)
// this fixes an issue with outdated api as getLoadResponseIdFromUrl might be fucked
return (if (this is ResultViewModel2.LoadResponseFromSearch) this.id else null) ?: getLoadResponseIdFromUrl(url, apiName)
}
/**

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

View File

@ -1,5 +1,6 @@
package com.lagradost.cloudstream3.services
import android.annotation.SuppressLint
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
@ -12,7 +13,7 @@ import com.lagradost.cloudstream3.*
import com.lagradost.cloudstream3.APIHolder.getApiDubstatusSettings
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.mvvm.safeApiCall
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.utils.AppUtils.createNotificationChannel
@ -97,128 +98,138 @@ class SubscriptionWorkManager(val context: Context, workerParams: WorkerParamete
)
}
@SuppressLint("UnspecifiedImmutableFlag")
override suspend fun doWork(): Result {
try {
// println("Update subscriptions!")
context.createNotificationChannel(
SUBSCRIPTION_CHANNEL_ID,
SUBSCRIPTION_CHANNEL_NAME,
SUBSCRIPTION_CHANNEL_DESCRIPTION
)
setForeground(
ForegroundInfo(
SUBSCRIPTION_NOTIFICATION_ID,
progressNotificationBuilder.build()
context.createNotificationChannel(
SUBSCRIPTION_CHANNEL_ID,
SUBSCRIPTION_CHANNEL_NAME,
SUBSCRIPTION_CHANNEL_DESCRIPTION
)
)
val subscriptions = getAllSubscriptions()
setForeground(
ForegroundInfo(
SUBSCRIPTION_NOTIFICATION_ID,
progressNotificationBuilder.build()
)
)
if (subscriptions.isEmpty()) {
WorkManager.getInstance(context).cancelWorkById(this.id)
val subscriptions = getAllSubscriptions()
if (subscriptions.isEmpty()) {
WorkManager.getInstance(context).cancelWorkById(this.id)
return Result.success()
}
val max = subscriptions.size
var progress = 0
updateProgress(max, progress, true)
// We need all plugins loaded.
PluginManager.loadAllOnlinePlugins(context)
PluginManager.loadAllLocalPlugins(context, false)
subscriptions.apmap { savedData ->
try {
val id = savedData.id ?: return@apmap null
val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
// Reasonable timeout to prevent having this worker run forever.
val response = withTimeoutOrNull(60_000) {
api.load(savedData.url) as? EpisodeResponse
} ?: return@apmap null
val dubPreference =
getDub(id) ?: if (
context.getApiDubstatusSettings().contains(DubStatus.Dubbed)
) {
DubStatus.Dubbed
} else {
DubStatus.Subbed
}
val latestEpisodes = response.getLatestEpisodes()
val latestPreferredEpisode = latestEpisodes[dubPreference]
val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
val latestSeenEpisode =
savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE
val shouldUpdate = latestPreferredEpisode > latestSeenEpisode
shouldUpdate to latestPreferredEpisode
} else {
val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE
val latestSeenEpisode =
savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE
val shouldUpdate = latestEpisode > latestSeenEpisode
shouldUpdate to latestEpisode
}
DataStoreHelper.updateSubscribedData(
id,
savedData,
response
)
if (shouldUpdate) {
val updateHeader = savedData.name
val updateDescription = txt(
R.string.subscription_episode_released,
latestEpisode,
savedData.name
).asString(context)
val intent = Intent(context, MainActivity::class.java).apply {
data = savedData.url.toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
} else {
PendingIntent.getActivity(context, 0, intent, 0)
}
val poster = ioWork {
savedData.posterUrl?.let { url ->
context.getImageBitmapFromUrl(
url,
savedData.posterHeaders
)
}
}
val updateNotification =
updateNotificationBuilder.setContentTitle(updateHeader)
.setContentText(updateDescription)
.setContentIntent(pendingIntent)
.setLargeIcon(poster)
.build()
notificationManager.notify(id, updateNotification)
}
// You can probably get some issues here since this is async but it does not matter much.
updateProgress(max, ++progress, false)
} catch (t: Throwable) {
logError(t)
}
}
return Result.success()
} catch (t: Throwable) {
logError(t)
// ye, while this is not correct, but because gods know why android just crashes
// and this causes major battery usage as it retries it inf times. This is better, just
// in case android decides to be android and fuck us
return Result.success()
}
val max = subscriptions.size
var progress = 0
updateProgress(max, progress, true)
// We need all plugins loaded.
PluginManager.loadAllOnlinePlugins(context)
PluginManager.loadAllLocalPlugins(context, false)
subscriptions.apmap { savedData ->
try {
val id = savedData.id ?: return@apmap null
val api = getApiFromNameNull(savedData.apiName) ?: return@apmap null
// Reasonable timeout to prevent having this worker run forever.
val response = withTimeoutOrNull(60_000) {
api.load(savedData.url) as? EpisodeResponse
} ?: return@apmap null
val dubPreference =
getDub(id) ?: if (
context.getApiDubstatusSettings().contains(DubStatus.Dubbed)
) {
DubStatus.Dubbed
} else {
DubStatus.Subbed
}
val latestEpisodes = response.getLatestEpisodes()
val latestPreferredEpisode = latestEpisodes[dubPreference]
val (shouldUpdate, latestEpisode) = if (latestPreferredEpisode != null) {
val latestSeenEpisode =
savedData.lastSeenEpisodeCount[dubPreference] ?: Int.MIN_VALUE
val shouldUpdate = latestPreferredEpisode > latestSeenEpisode
shouldUpdate to latestPreferredEpisode
} else {
val latestEpisode = latestEpisodes[DubStatus.None] ?: Int.MIN_VALUE
val latestSeenEpisode =
savedData.lastSeenEpisodeCount[DubStatus.None] ?: Int.MIN_VALUE
val shouldUpdate = latestEpisode > latestSeenEpisode
shouldUpdate to latestEpisode
}
DataStoreHelper.updateSubscribedData(
id,
savedData,
response
)
if (shouldUpdate) {
val updateHeader = savedData.name
val updateDescription = txt(
R.string.subscription_episode_released,
latestEpisode,
savedData.name
).asString(context)
val intent = Intent(context, MainActivity::class.java).apply {
data = savedData.url.toUri()
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
val pendingIntent =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
} else {
PendingIntent.getActivity(context, 0, intent, 0)
}
val poster = ioWork {
savedData.posterUrl?.let { url ->
context.getImageBitmapFromUrl(
url,
savedData.posterHeaders
)
}
}
val updateNotification =
updateNotificationBuilder.setContentTitle(updateHeader)
.setContentText(updateDescription)
.setContentIntent(pendingIntent)
.setLargeIcon(poster)
.build()
notificationManager.notify(id, updateNotification)
}
// You can probably get some issues here since this is async but it does not matter much.
updateProgress(max, ++progress, false)
} catch (_: Throwable) {
}
}
return Result.success()
}
}

View File

@ -8,7 +8,8 @@ import com.lagradost.cloudstream3.syncproviders.SyncIdName
import com.lagradost.cloudstream3.ui.WatchType
import com.lagradost.cloudstream3.ui.library.ListSorting
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.Coroutines.ioWork
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllFavorites
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
@ -71,9 +72,9 @@ class LocalList : SyncAPI {
}?.distinctBy { it.first } ?: return null
val list = ioWork {
val isTrueTv = isTrueTvSettings()
val isTrueTv = isLayout(TV)
val baseMap = WatchType.values().filter { it != WatchType.NONE }.associate {
val baseMap = WatchType.entries.filter { it != WatchType.NONE }.associate {
// None is not something to display
it.stringRes to emptyList<SyncAPI.LibraryItem>()
} + 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.ui.account.AccountHelper.showAccountEditDialog
import com.lagradost.cloudstream3.ui.result.setImage
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.UIHelper.setImage
@ -38,7 +40,7 @@ class AccountAdapter(
is AccountListItemBinding -> binding.apply {
if (account == null) return@apply
val isTv = isTvSettings() || !root.isInTouchMode
val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex
@ -80,7 +82,7 @@ class AccountAdapter(
is AccountListItemEditBinding -> binding.apply {
if (account == null) return@apply
val isTv = isTvSettings() || !root.isInTouchMode
val isTv = isLayout(TV or EMULATOR) || !root.isInTouchMode
val isLastUsedAccount = account.keyIndex == DataStoreHelper.selectedKeyIndex

View File

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

View File

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

View File

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

View File

@ -2,31 +2,58 @@ package com.lagradost.cloudstream3.ui.home
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchResponse
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding
import com.lagradost.cloudstream3.ui.BaseAdapter
import com.lagradost.cloudstream3.ui.ViewHolderState
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
import com.lagradost.cloudstream3.utils.UIHelper.toPx
class HomeChildItemAdapter(
val cardList: MutableList<SearchResponse>,
class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState<Boolean>(view) {
/*private fun recursive(view : View) : Boolean {
if (view.isFocused) {
println("VIEW: $view | id=${view.id}")
}
return (view as? ViewGroup)?.children?.any { recursive(it) } ?: false
}*/
// very shitty that we cant store the state when the view clears,
// but this is because the focus clears before the view is removed
// so we have to manually store it
var wasFocused: Boolean = false
override fun save(): Boolean = wasFocused
override fun restore(state: Boolean) {
if (state) {
wasFocused = false
// only refocus if tv
if(isLayout(TV)) {
itemView.requestFocus()
}
}
}
}
class HomeChildItemAdapter(
fragment: Fragment,
id: Int,
private val nextFocusUp: Int? = null,
private val nextFocusDown: Int? = null,
private val clickCallback: (SearchClickCallback) -> Unit,
) :
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
BaseAdapter<SearchResponse, Boolean>(fragment, id) {
var isHorizontal: Boolean = false
var hasNext: Boolean = false
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Boolean> {
val expanded = parent.context.IsBottomLayout()
/* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid
@ -39,164 +66,78 @@ class HomeChildItemAdapter(
parent,
false
) else HomeResultGridBinding.inflate(inflater, parent, false)
return HomeScrollViewHolderState(binding)
}
override fun onBindContent(
holder: ViewHolderState<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(
binding,
clickCallback,
itemCount,
layoutParams =
layoutParams.apply {
width = if (!isHorizontal) {
min
} else {
max
}
height = if (!isHorizontal) {
max
} else {
min
}
}
}
}
is HomeResultGridExpandedBinding -> {
binding.backgroundCard.apply {
val min = 114.toPx
val max = 180.toPx
layoutParams =
layoutParams.apply {
width = if (!isHorizontal) {
min
} else {
max
}
height = if (!isHorizontal) {
max
} else {
min
}
}
}
if (position == 0) { // to fix tv
binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view
}
}
}
SearchResultBuilder.bind(
clickCallback = { click ->
// ok, so here we hijack the callback to fix the focus
when (click.action) {
SEARCH_ACTION_LOAD -> (holder as? HomeScrollViewHolderState)?.wasFocused = true
}
clickCallback(click)
},
item,
position,
holder.itemView,
null, // nextFocusBehavior,
nextFocusUp,
nextFocusDown,
isHorizontal,
parent.isRtl()
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is CardViewHolder -> {
holder.itemCount = itemCount // i know ugly af
holder.bind(cardList[position], position)
}
}
}
override fun getItemCount(): Int {
return cardList.size
}
override fun getItemId(position: Int): Long {
return (cardList[position].id ?: position).toLong()
}
fun updateList(newList: List<SearchResponse>) {
val diffResult = DiffUtil.calculateDiff(
HomeChildDiffCallback(this.cardList, newList)
nextFocusDown
)
cardList.clear()
cardList.addAll(newList)
diffResult.dispatchUpdatesTo(this)
}
class CardViewHolder
constructor(
val binding: ViewBinding,
private val clickCallback: (SearchClickCallback) -> Unit,
var itemCount: Int,
private val nextFocusUp: Int? = null,
private val nextFocusDown: Int? = null,
private val isHorizontal: Boolean = false,
private val isRtl: Boolean
) :
RecyclerView.ViewHolder(binding.root) {
fun bind(card: SearchResponse, position: Int) {
// TV focus fixing
/*val nextFocusBehavior = when (position) {
0 -> true
itemCount - 1 -> false
else -> null
}
if (position == 0) { // to fix tv
if (isRtl) {
itemView.nextFocusRightId = R.id.nav_rail_view
itemView.nextFocusLeftId = -1
}
else {
itemView.nextFocusLeftId = R.id.nav_rail_view
itemView.nextFocusRightId = -1
}
} else {
itemView.nextFocusRightId = -1
itemView.nextFocusLeftId = -1
}*/
when (binding) {
is HomeResultGridBinding -> {
binding.backgroundCard.apply {
val min = 114.toPx
val max = 180.toPx
layoutParams =
layoutParams.apply {
width = if (!isHorizontal) {
min
} else {
max
}
height = if (!isHorizontal) {
max
} else {
min
}
}
}
}
is HomeResultGridExpandedBinding -> {
binding.backgroundCard.apply {
val min = 114.toPx
val max = 180.toPx
layoutParams =
layoutParams.apply {
width = if (!isHorizontal) {
min
} else {
max
}
height = if (!isHorizontal) {
max
} else {
min
}
}
}
if (position == 0) { // to fix tv
binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view
}
}
}
SearchResultBuilder.bind(
clickCallback,
card,
position,
itemView,
null, // nextFocusBehavior,
nextFocusUp,
nextFocusDown
)
itemView.tag = position
//val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f)
//ani.fillAfter = true
//ani.duration = 200
//itemView.startAnimation(ani)
}
holder.itemView.tag = position
}
}
class HomeChildDiffCallback(
private val oldList: List<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.search.*
import com.lagradost.cloudstream3.ui.search.SearchHelper.handleSearchClickCallback
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.isRecyclerScrollable
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.ownHide
@ -311,7 +313,7 @@ class HomeFragment : Fragment() {
button?.isVisible = isValid
button?.isChecked = isValid && selectedTypes.any { types.contains(it) }
button?.isFocusable = true
if (isTrueTvSettings()) {
if (isLayout(TV)) {
button?.isFocusableInTouchMode = true
}
@ -435,7 +437,7 @@ class HomeFragment : Fragment() {
bottomSheetDialog?.ownShow()
val layout =
if (isTvSettings()) R.layout.fragment_home_tv else R.layout.fragment_home
if (isLayout(TV or EMULATOR)) R.layout.fragment_home_tv else R.layout.fragment_home
val root = inflater.inflate(layout, container, false)
binding = try {
FragmentHomeBinding.bind(root)
@ -449,6 +451,7 @@ class HomeFragment : Fragment() {
}
override fun onDestroyView() {
bottomSheetDialog?.ownHide()
binding = null
super.onDestroyView()
@ -485,6 +488,10 @@ class HomeFragment : Fragment() {
private var bottomSheetDialog: BottomSheetDialog? = null
// https://github.com/vivchar/RendererRecyclerViewAdapter/blob/185251ee9d94fb6eb3e063b00d646b745186c365/example/src/main/java/com/github/vivchar/example/pages/github/GithubFragment.kt#L32
// cry about it, but this is android we are talking about, we cant do the most simple shit without making a global variable
private var instanceState: Bundle = Bundle()
private var homeMasterAdapter: HomeParentItemAdapterPreview? = null
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -505,15 +512,14 @@ class HomeFragment : Fragment() {
activity.loadSearchResult(listHomepageItems.random())
}
}
homeMasterRecycler.adapter =
HomeParentItemAdapterPreview(
mutableListOf(),
homeViewModel
)
homeMasterAdapter = HomeParentItemAdapterPreview(
fragment = this@HomeFragment,
homeViewModel,
)
homeMasterRecycler.adapter = homeMasterAdapter
//fixPaddingStatusbar(homeLoadingStatusbar)
homeApiFab.isVisible = !isTvSettings()
homeApiFab.isVisible = isLayout(PHONE)
homeMasterRecycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
@ -521,7 +527,7 @@ class HomeFragment : Fragment() {
homeApiFab.shrink() // hide
homeRandom.shrink()
} else if (dy < -5) {
if (!isTvSettings()) {
if (isLayout(PHONE)) {
homeApiFab.extend() // show
homeRandom.extend()
}
@ -540,7 +546,7 @@ class HomeFragment : Fragment() {
settingsManager.getBoolean(
getString(R.string.random_button_key),
false
) && !isTvSettings()
) && isLayout(PHONE)
binding?.homeRandom?.visibility = View.GONE
}
@ -560,10 +566,11 @@ class HomeFragment : Fragment() {
val mutableListOfResponse = mutableListOf<SearchResponse>()
listHomepageItems.clear()
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(
d.values.toMutableList(),
homeMasterRecycler
)
(homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(d.values.map {
it.copy(
list = it.list.copy(list = it.list.list.toMutableList())
)
}.toMutableList())
homeLoading.isVisible = false
homeLoadingError.isVisible = false
@ -612,7 +619,7 @@ class HomeFragment : Fragment() {
}
is Resource.Loading -> {
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf())
(homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(listOf())
homeLoadingShimmer.startShimmer()
homeLoading.isVisible = true
homeLoadingError.isVisible = false

View File

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

View File

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

View File

@ -4,112 +4,61 @@ import android.content.res.Configuration
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import androidx.fragment.app.Fragment
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding
import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.NoStateAdapter
import com.lagradost.cloudstream3.ui.ViewHolderState
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.UIHelper.setImage
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private var items: MutableList<LoadResponse> = mutableListOf()
class HomeScrollAdapter(
fragment: Fragment
) : NoStateAdapter<LoadResponse>(fragment) {
var hasMoreItems: Boolean = false
fun getItem(position: Int): LoadResponse? {
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 {
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Any> {
val inflater = LayoutInflater.from(parent.context)
val binding = if (isTvSettings()) {
val binding = if (isLayout(TV or EMULATOR)) {
HomeScrollViewTvBinding.inflate(inflater, parent, false)
} else {
HomeScrollViewBinding.inflate(inflater, parent, false)
}
return CardViewHolder(
binding,
//forceHorizontalPosters
)
return ViewHolderState(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is CardViewHolder -> {
holder.bind(items[position])
override fun onBindContent(
holder: ViewHolderState<Any>,
item: LoadResponse,
position: Int,
) {
val binding = holder.view
val itemView = holder.itemView
val isHorizontal =
binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val posterUrl =
if (isHorizontal) item.backgroundPosterUrl ?: item.posterUrl else item.posterUrl
?: item.backgroundPosterUrl
when (binding) {
is HomeScrollViewBinding -> {
binding.homeScrollPreview.setImage(posterUrl)
binding.homeScrollPreviewTags.apply {
text = item.tags?.joinToString("") ?: ""
isGone = item.tags.isNullOrEmpty()
maxLines = 2
}
binding.homeScrollPreviewTitle.text = item.name
}
is HomeScrollViewTvBinding -> {
binding.homeScrollPreview.setImage(posterUrl)
}
}
}
class CardViewHolder
constructor(
val binding: ViewBinding,
//private val forceHorizontalPosters: Boolean? = null
) :
RecyclerView.ViewHolder(binding.root) {
fun bind(card: LoadResponse) {
val isHorizontal =
binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
val posterUrl =
if (isHorizontal) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl
?: card.backgroundPosterUrl
when (binding) {
is HomeScrollViewBinding -> {
binding.homeScrollPreview.setImage(posterUrl)
binding.homeScrollPreviewTags.apply {
text = card.tags?.joinToString("") ?: ""
isGone = card.tags.isNullOrEmpty()
maxLines = 2
}
binding.homeScrollPreviewTitle.text = card.name
}
is HomeScrollViewTvBinding -> {
binding.homeScrollPreview.setImage(posterUrl)
}
}
}
}
class HomeScrollDiffCallback(
private val oldList: List<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.SearchClickCallback
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.addProgramsToContinueWatching
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
@ -52,6 +53,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.withContext
import java.util.EnumSet
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.collections.set
class HomeViewModel : ViewModel() {
@ -124,7 +126,7 @@ class HomeViewModel : ViewModel() {
private val _resumeWatching = MutableLiveData<List<SearchResponse>>()
private val _preview = MutableLiveData<Resource<Pair<Boolean, List<LoadResponse>>>>()
private val previewResponses = mutableListOf<LoadResponse>()
private val previewResponses = CopyOnWriteArrayList<LoadResponse>()
private val previewResponsesAdded = mutableSetOf<String>()
val resumeWatching: LiveData<List<SearchResponse>> = _resumeWatching
@ -132,7 +134,7 @@ class HomeViewModel : ViewModel() {
private fun loadResumeWatching() = viewModelScope.launchSafe {
val resumeWatchingResult = getResumeWatching()
if (isTrueTvSettings() && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (isLayout(TV) && resumeWatchingResult != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ioSafe {
// this WILL crash on non tvs, so keep this inside a try catch
activity?.addProgramsToContinueWatching(resumeWatchingResult)
@ -326,7 +328,13 @@ class HomeViewModel : ViewModel() {
val filteredList =
context?.filterHomePageListByFilmQuality(list) ?: list
expandable[list.name] =
ExpandableHomepageList(filteredList, 1, home.hasNext)
ExpandableHomepageList(
filteredList.copy(
list = CopyOnWriteArrayList(
filteredList.list
)
), 1, home.hasNext
)
}
}
@ -341,8 +349,7 @@ class HomeViewModel : ViewModel() {
val currentList =
items.shuffled().filter { it.list.isNotEmpty() }
.flatMap { it.list }
.distinctBy { it.url }
.toList()
.distinctBy { it.url }.toList()
if (currentList.isNotEmpty()) {
val randomItems =

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

@ -2,9 +2,6 @@ package com.lagradost.cloudstream3.ui.result
import android.annotation.SuppressLint
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Rect
@ -34,7 +31,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.cloudstream3.APIHolder
import com.lagradost.cloudstream3.APIHolder.updateHasTrailers
import com.lagradost.cloudstream3.CommonActivity
import com.lagradost.cloudstream3.CommonActivity.showToast
import com.lagradost.cloudstream3.DubStatus
import com.lagradost.cloudstream3.LoadResponse
import com.lagradost.cloudstream3.MainActivity.Companion.afterPluginsLoadedEvent
@ -62,16 +58,15 @@ import com.lagradost.cloudstream3.ui.result.ResultFragment.updateUIEvent
import com.lagradost.cloudstream3.ui.search.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.utils.AppUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
import com.lagradost.cloudstream3.utils.AppUtils.openBrowser
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogInstant
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialogText
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
@ -688,14 +683,15 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultNextAiringTime.setText(d.nextAiringDate)
resultPoster.setImage(d.posterImage)
resultPosterBackground.setImage(d.posterBackgroundImage)
resultDescription.setTextHtml(d.plotText)
resultDescription.setOnClickListener {
activity?.let { activity ->
activity.showBottomDialogText(
d.titleText.asString(activity),
d.plotText.asString(activity).html(),
{}
)
var isExpanded = false
resultDescription.apply {
setTextHtml(d.plotText)
setOnClickListener {
isExpanded = !isExpanded
maxLines = if (isExpanded) {
Integer.MAX_VALUE
} else 10
}
}
@ -758,14 +754,8 @@ open class ResultFragmentPhone : FullScreenPlayer() {
resultReloadConnectionOpenInBrowser.isVisible = data is Resource.Failure
resultTitle.setOnLongClickListener {
val titleToCopy = resultTitle.text
val clipboardManager =
activity?.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager?
clipboardManager?.setPrimaryClip(ClipData.newPlainText("Title", titleToCopy))
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
showToast(R.string.copyTitle, Toast.LENGTH_SHORT)
}
return@setOnLongClickListener true
clipboardHelper(txt(R.string.title), resultTitle.text)
true
}
}
}
@ -901,14 +891,6 @@ open class ResultFragmentPhone : FullScreenPlayer() {
observe(viewModel.recommendations) { recommendations ->
setRecommendations(recommendations, null)
}
observe(viewModel.episodeSynopsis) { description ->
activity?.let { activity ->
activity.showBottomDialogText(
activity.getString(R.string.synopsis),
description.html()
) { viewModel.releaseEpisodeSynopsis() }
}
}
context?.let { ctx ->
val arrayAdapter = ArrayAdapter<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.SearchAdapter
import com.lagradost.cloudstream3.ui.search.SearchHelper
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isEmulatorSettings
import com.lagradost.cloudstream3.ui.settings.Globals
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
@ -308,9 +311,10 @@ class ResultFragmentTv : Fragment() {
resultEpisodesShowButton to resultEpisodesShowText
).forEach { (button , text) ->
button.setOnFocusChangeListener { _, hasFocus ->
button.setOnFocusChangeListener { view, hasFocus ->
if (!hasFocus) {
text.isSelected = false
if (view.id == R.id.result_episodes_show_button) toggleEpisodes(false)
return@setOnFocusChangeListener
}
@ -376,10 +380,6 @@ class ResultFragmentTv : Fragment() {
resultMetaSite.isFocusable = false
//resultReloadConnectionOpenInBrowser.setOnClickListener {view ->
// view.context?.openBrowser(storedData?.url ?: return@setOnClickListener, fallbackWebview = true)
//}
resultSeasonSelection.setAdapter()
resultRangeSelection.setAdapter()
resultDubSelection.setAdapter()
@ -457,11 +457,12 @@ class ResultFragmentTv : Fragment() {
observeNullable(viewModel.resumeWatching) { resume ->
binding?.apply {
// > resultResumeSeries is visible when not null
if (resume == null) {
resultResumeSeries.isVisible = false
return@observeNullable
}
resultResumeSeries.isVisible = true
resultPlayMovie.isVisible = false
resultPlaySeries.isVisible = false
// show progress no matter if series or movie
resume.progress?.let { progress ->
@ -476,10 +477,6 @@ class ResultFragmentTv : Fragment() {
resultResumeProgressHolder.isVisible = false
}
resultPlayMovie.isVisible = false
resultPlaySeries.isVisible = false
resultResumeSeries.isVisible = true
focusPlayButton()
// Stops last button right focus if it is a movie
if (resume.isMovie)
@ -490,7 +487,7 @@ class ResultFragmentTv : Fragment() {
resume.isMovie -> context?.getString(R.string.resume)
resume.result.season != null ->
"${getString(R.string.season_short)}${resume.result.season}:${getString(R.string.episode_short)}${resume.result.episode}"
else -> "${getString(R.string.episode)}${resume.result.episode}"
else -> "${getString(R.string.episode)} ${resume.result.episode}"
}
resultResumeSeriesButton.setOnClickListener {
@ -603,7 +600,7 @@ class ResultFragmentTv : Fragment() {
}
observeNullable(viewModel.subscribeStatus) { isSubscribed ->
binding?.resultSubscribe?.isVisible = isSubscribed != null && requireContext().isEmulatorSettings()
binding?.resultSubscribe?.isVisible = isSubscribed != null && isLayout(EMULATOR)
binding?.resultSubscribeButton?.apply {
if (isSubscribed == null) return@observeNullable
@ -646,15 +643,14 @@ class ResultFragmentTv : Fragment() {
}
observeNullable(viewModel.movie) { data ->
if (data == null) return@observeNullable
if (data == null ) {
return@observeNullable
}
binding?.apply {
resultPlayMovie.isVisible = (data is Resource.Success) && !comingSoon
resultPlaySeries.isVisible = false
resultEpisodesShow.isVisible = false
(data as? Resource.Success)?.value?.let { (text, ep) ->
//resultPlayMovieText.setText(text)
resultPlayMovieButton.setOnClickListener {
viewModel.handleAction(
EpisodeClickEvent(ACTION_CLICK_DEFAULT, ep)
@ -666,14 +662,17 @@ class ResultFragmentTv : Fragment() {
)
return@setOnLongClickListener true
}
//focusPlayButton()
resultPlayMovieButton.requestFocus()
resultPlayMovie.isVisible = !comingSoon && resultResumeSeries.isGone
if (comingSoon)
resultBookmarkButton.requestFocus()
else
resultPlayMovieButton.requestFocus()
// Stops last button right focus
resultSearchButton.nextFocusRightId = R.id.result_search_Button
}
}
//focusPlayButton()
}
observeNullable(viewModel.selectPopup) { popup ->
@ -754,16 +753,19 @@ class ResultFragmentTv : Fragment() {
observe(viewModel.recommendations) { recommendations ->
setRecommendations(recommendations, null)
}
observe(viewModel.episodeSynopsis) { description ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
builder.setMessage(description.html())
.setTitle(R.string.synopsis)
.setOnDismissListener {
viewModel.releaseEpisodeSynopsis()
}
.show()
if (isLayout(TV)) {
observe(viewModel.episodeSynopsis) { description ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
builder.setMessage(description.html())
.setTitle(R.string.synopsis)
.setOnDismissListener {
viewModel.releaseEpisodeSynopsis()
}
.show()
}
}
}
@ -774,16 +776,14 @@ class ResultFragmentTv : Fragment() {
binding?.apply {
resultPlayMovie.isVisible = false
resultPlaySeries.isVisible = true && !comingSoon
resultEpisodes.isVisible = true && !comingSoon
resultEpisodesShow.isVisible = true && !comingSoon
if (comingSoon)
resultBookmarkButton.requestFocus()
// resultEpisodeLoading.isVisible = episodes is Resource.Loading
if (episodes is Resource.Success) {
val first = episodes.value.firstOrNull()
if (first != null) {
resultPlaySeriesText.text = //"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}"
resultPlaySeriesText.text =
when {
first.season != null ->
"${getString(R.string.season_short)}${first.season}:${getString(R.string.episode_short)}${first.episode}"
@ -805,8 +805,9 @@ class ResultFragmentTv : Fragment() {
}
if (!hasLoadedEpisodesOnce) {
hasLoadedEpisodesOnce = true
focusPlayButton()
resultPlaySeries.requestFocus()
resultPlaySeries.isVisible = resultResumeSeries.isGone && !comingSoon
resultEpisodesShow.isVisible = true && !comingSoon
resultPlaySeriesButton.requestFocus()
}
}
@ -874,14 +875,25 @@ class ResultFragmentTv : Fragment() {
resultNextAiring.setText(d.nextAiringEpisode)
resultNextAiringTime.setText(d.nextAiringDate)
resultPoster.setImage(d.posterImage)
resultDescription.setTextHtml(d.plotText)
resultDescription.setOnClickListener { view ->
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
builder.setMessage(d.plotText.asString(ctx).html())
.setTitle(d.plotHeaderText.asString(ctx))
.show()
var isExpanded = false
resultDescription.apply {
setTextHtml(d.plotText)
setOnClickListener {
if (isLayout(EMULATOR)) {
isExpanded = !isExpanded
maxLines = if (isExpanded) {
Integer.MAX_VALUE
} else 10
} else {
view.context?.let { ctx ->
val builder: AlertDialog.Builder =
AlertDialog.Builder(ctx, R.style.AlertDialogCustom)
builder.setMessage(d.plotText.asString(ctx).html())
.setTitle(d.plotHeaderText.asString(ctx))
.show()
}
}
}
}
@ -904,9 +916,6 @@ class ResultFragmentTv : Fragment() {
)
comingSoon = d.comingSoon
resultTvComingSoon.isVisible = d.comingSoon
resultPlayMovie.isGone = d.comingSoon
resultPlaySeries.isGone = d.comingSoon
resultDataHolder.isGone = d.comingSoon
UIHelper.populateChips(resultTag, d.tags)
resultCastItems.isGone = d.actors.isNullOrEmpty()

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

View File

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

View File

@ -19,6 +19,13 @@ sealed class UiText {
data class DynamicString(val value: String) : UiText() {
override fun toString(): String = value
override fun equals(other: Any?): Boolean {
if (other !is DynamicString) return false
return this.value == other.value
}
override fun hashCode(): Int = value.hashCode()
}
class StringResource(
@ -27,6 +34,16 @@ sealed class UiText {
) : UiText() {
override fun toString(): String =
"resId = $resId\nargs = ${args.toList().map { "(${it::class} = $it)" }}"
override fun equals(other: Any?): Boolean {
if (other !is StringResource) return false
return this.resId == other.resId && this.args == other.args
}
override fun hashCode(): Int {
var result = resId
result = 31 * result + args.hashCode()
return result
}
}
fun asStringNull(context: Context?): String? {

View File

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

View File

@ -1,6 +1,5 @@
package com.lagradost.cloudstream3.ui.search
import android.app.Activity
import android.widget.Toast
import com.lagradost.cloudstream3.CommonActivity.activity
import com.lagradost.cloudstream3.CommonActivity.showToast
@ -10,7 +9,8 @@ import com.lagradost.cloudstream3.ui.download.DOWNLOAD_ACTION_PLAY_FILE
import com.lagradost.cloudstream3.ui.download.DownloadButtonSetup.handleDownloadClick
import com.lagradost.cloudstream3.ui.download.DownloadClickEvent
import com.lagradost.cloudstream3.ui.result.START_ACTION_LOAD_EP
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.VideoDownloadHelper
@ -56,7 +56,7 @@ object SearchHelper {
}
}
SEARCH_ACTION_SHOW_METADATA -> {
if(!isTvSettings()) { // we only want this on phone as UI is not done yet on tv
if(isLayout(PHONE)) { // we only want this on phone as UI is not done yet on tv
(activity as? MainActivity?)?.apply {
loadPopup(callback.card)
} ?: kotlin.run {

View File

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

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

View File

@ -1,9 +1,5 @@
package com.lagradost.cloudstream3.ui.settings
import android.app.UiModeManager
import android.content.Context
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -16,16 +12,20 @@ import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.lagradost.cloudstream3.AcraApplication.Companion.context
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.syncproviders.AccountManager.Companion.accountManagers
import com.lagradost.cloudstream3.ui.home.HomeFragment
import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbar
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.UIHelper
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
import com.lagradost.cloudstream3.utils.UIHelper.navigate
import com.lagradost.cloudstream3.utils.UIHelper.setImage
import com.lagradost.cloudstream3.utils.UIHelper.toPx
@ -33,10 +33,6 @@ import java.io.File
class SettingsFragment : Fragment() {
companion object {
var beneneCount = 0
private var isTv: Boolean = false
private var isTrueTv: Boolean = false
fun PreferenceFragmentCompat?.getPref(id: Int): Preference? {
if (this == null) return null
@ -53,12 +49,12 @@ class SettingsFragment : Fragment() {
* On TV you cannot properly scroll to the bottom of settings, this fixes that.
* */
fun PreferenceFragmentCompat.setPaddingBottom() {
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
listView?.setPadding(0, 0, 0, 100.toPx)
}
}
fun PreferenceFragmentCompat.setToolBarScrollFlags() {
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
val settingsAppbar = view?.findViewById<MaterialToolbar>(R.id.settings_toolbar)
settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> {
@ -67,7 +63,7 @@ class SettingsFragment : Fragment() {
}
}
fun Fragment?.setToolBarScrollFlags() {
if (isTvSettings()) {
if (isLayout(TV or EMULATOR)) {
val settingsAppbar = this?.view?.findViewById<MaterialToolbar>(R.id.settings_toolbar)
settingsAppbar?.updateLayoutParams<AppBarLayout.LayoutParams> {
@ -86,7 +82,7 @@ class SettingsFragment : Fragment() {
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
fixPaddingStatusbar(settingsToolbar)
UIHelper.fixPaddingStatusbar(settingsToolbar)
}
fun Fragment?.setUpToolbar(@StringRes title: Int) {
@ -101,7 +97,7 @@ class SettingsFragment : Fragment() {
activity?.onBackPressedDispatcher?.onBackPressed()
}
}
fixPaddingStatusbar(settingsToolbar)
UIHelper.fixPaddingStatusbar(settingsToolbar)
}
fun getFolderSize(dir: File): Long {
@ -117,60 +113,7 @@ class SettingsFragment : Fragment() {
return size
}
private fun Context.getLayoutInt(): Int {
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
return settingsManager.getInt(this.getString(R.string.app_layout_key), -1)
}
private fun Context.isTvSettings(): Boolean {
var value = getLayoutInt()
if (value == -1) {
value = if (isAutoTv()) 1 else 0
}
return value == 1 || value == 2
}
private fun Context.isTrueTvSettings(): Boolean {
var value = getLayoutInt()
if (value == -1) {
value = if (isAutoTv()) 1 else 0
}
return value == 1
}
fun Context.updateTv() {
isTrueTv = isTrueTvSettings()
isTv = isTvSettings()
}
fun isTrueTvSettings(): Boolean {
return isTrueTv
}
fun isTvSettings(): Boolean {
return isTv
}
fun Context.isEmulatorSettings(): Boolean {
return getLayoutInt() == 2
}
// phone exclusive
fun isTruePhone(): Boolean {
return !isTrueTvSettings() && !isTvSettings() && context?.isEmulatorSettings() != true
}
private fun Context.isAutoTv(): Boolean {
val uiModeManager = getSystemService(Context.UI_MODE_SERVICE) as UiModeManager?
// AFT = Fire TV
val model = Build.MODEL.lowercase()
return uiModeManager?.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION || Build.MODEL.contains(
"AFT"
) || model.contains("firestick") || model.contains("fire tv") || model.contains("chromecast")
}
}
override fun onDestroyView() {
binding = null
super.onDestroyView()
@ -195,8 +138,6 @@ class SettingsFragment : Fragment() {
// used to debug leaks showToast(activity,"${VideoDownloadManager.downloadStatusEvent.size} : ${VideoDownloadManager.downloadProgressEvent.size}")
val isTrueTv = isTrueTvSettings()
for (syncApi in accountManagers) {
val login = syncApi.loginInfo()
val pic = login?.profilePicture ?: continue
@ -224,7 +165,7 @@ class SettingsFragment : Fragment() {
setOnClickListener {
navigate(navigationId)
}
if (isTrueTv) {
if (isLayout(TV)) {
isFocusable = true
isFocusableInTouchMode = true
}
@ -232,9 +173,20 @@ class SettingsFragment : Fragment() {
}
// Default focus on TV
if (isTrueTv) {
if (isLayout(TV)) {
settingsGeneral.requestFocus()
}
}
val appVersion = getString(R.string.app_version)
val commitInfo = getString(R.string.commit_hash)
val buildDate = BuildConfig.BUILDDATE
binding?.buildDate?.text = buildDate
binding?.appVersionInfo?.setOnLongClickListener{
clipboardHelper(txt(R.string.extension_version), "$appVersion $commitInfo")
true
}
}
}

View File

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

View File

@ -9,11 +9,11 @@ import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.SearchQuality
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
import com.lagradost.cloudstream3.ui.settings.Globals.updateTv
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setUpToolbar
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.updateTv
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showMultiDialog

View File

@ -1,10 +1,6 @@
package com.lagradost.cloudstream3.ui.settings
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.os.TransactionTooLargeException
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
@ -20,6 +16,7 @@ import com.lagradost.cloudstream3.databinding.LogcatBinding
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.network.initClient
import com.lagradost.cloudstream3.services.BackupWorkManager
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.getPref
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setPaddingBottom
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.setToolBarScrollFlags
@ -30,6 +27,7 @@ import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showBottomDialog
import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog
import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper
import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe
import com.lagradost.cloudstream3.utils.UIHelper.hideKeyboard
import com.lagradost.cloudstream3.utils.VideoDownloadManager
@ -117,22 +115,15 @@ class SettingsUpdates : PreferenceFragmentCompat() {
binding.text1.text = text
binding.copyBtt.setOnClickListener {
// Can crash on too much text
try {
val serviceClipboard =
(activity?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager?)
?: return@setOnClickListener
val clip = ClipData.newPlainText("logcat", text)
serviceClipboard.setPrimaryClip(clip)
dialog.dismissSafe(activity)
} catch (e: TransactionTooLargeException) {
showToast(R.string.clipboard_too_large)
}
clipboardHelper(txt("Logcat"), text)
dialog.dismissSafe(activity)
}
binding.clearBtt.setOnClickListener {
Runtime.getRuntime().exec("logcat -c")
dialog.dismissSafe(activity)
}
binding.saveBtt.setOnClickListener {
var fileStream: OutputStream? = null
try {
@ -153,9 +144,11 @@ class SettingsUpdates : PreferenceFragmentCompat() {
fileStream?.closeQuietly()
}
}
binding.closeBtt.setOnClickListener {
dialog.dismissSafe(activity)
}
return@setOnPreferenceClickListener true
}

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

View File

@ -17,7 +17,8 @@ import com.lagradost.cloudstream3.plugins.PluginManager
import com.lagradost.cloudstream3.plugins.VotingApi.getVotes
import com.lagradost.cloudstream3.ui.result.setText
import com.lagradost.cloudstream3.ui.result.txt
import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTrueTvSettings
import com.lagradost.cloudstream3.ui.settings.Globals.TV
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
import com.lagradost.cloudstream3.utils.AppUtils.html
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.Coroutines.main
@ -44,7 +45,7 @@ class PluginAdapter(
private val plugins: MutableList<PluginViewData> = mutableListOf()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
val layout = if(isTrueTvSettings()) R.layout.repository_item_tv else R.layout.repository_item
val layout = if(isLayout(TV)) R.layout.repository_item_tv else R.layout.repository_item
val inflated = LayoutInflater.from(parent.context).inflate(layout, parent, false)
return PluginViewHolder(

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

View File

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

View File

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

View File

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

View File

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

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

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

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 {
val mapper: JsonMapper = JsonMapper.builder().addModule(kotlinModule())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()
@ -66,22 +88,10 @@ object DataStore {
return "${folder}/${path}"
}
fun <T> Context.setKeyRaw(path: String, value: T, isEditingAppSettings: Boolean = false) {
try {
val editor: SharedPreferences.Editor =
if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit()
when (value) {
is Boolean -> editor.putBoolean(path, value)
is Int -> editor.putInt(path, value)
is String -> editor.putString(path, value)
is Float -> editor.putFloat(path, value)
is Long -> editor.putLong(path, value)
(value as? Set<String> != null) -> editor.putStringSet(path, value as Set<String>)
}
editor.apply()
} catch (e: Exception) {
logError(e)
}
fun editor(context : Context, isEditingAppSettings: Boolean = false) : Editor {
val editor: SharedPreferences.Editor =
if (isEditingAppSettings) context.getDefaultSharedPrefs().edit() else context.getSharedPrefs().edit()
return Editor(editor)
}
fun Context.getDefaultSharedPrefs(): SharedPreferences {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -174,8 +174,8 @@
<string name="sort_close">Close</string>
<string name="sort_clear">Clear</string>
<string name="sort_save">Save</string>
<string name="copyTitle">Title copied!</string>
<string name="copyRepoUrl">Repo URL copied!</string>
<string name="repo_copy_label">Repository name and URL</string>
<string name="toast_copied">copied!</string>
<string name="subscribe_tooltip">New episode notification</string>
<string name="result_search_tooltip">Search in other extensions</string>
<string name="recommendations_tooltip">Show recommendations</string>
@ -647,6 +647,8 @@
<string name="history">History</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_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_remove_from_watched">Remove from watched</string>
<string name="confirm_exit_dialog">Are you sure you want to exit\?</string>

View File

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