Merge branch 'master' of https://github.com/recloudstream/cloudstream into batteryoptimization

This commit is contained in:
IndusAryan 2024-03-23 11:58:03 +05:30
commit 2c6c5cdc03
20 changed files with 866 additions and 793 deletions

View file

@ -1790,8 +1790,6 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener,
}
} catch (e: Exception) {
logError(e)
} finally {
setKey(HAS_DONE_SETUP_KEY, true)
}
// Used to check current focus for TV

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

@ -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,84 +66,15 @@ class HomeChildItemAdapter(
parent,
false
) else HomeResultGridBinding.inflate(inflater, parent, false)
return CardViewHolder(
binding,
clickCallback,
itemCount,
nextFocusUp,
nextFocusDown,
isHorizontal,
parent.isRtl()
)
return HomeScrollViewHolderState(binding)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is CardViewHolder -> {
holder.itemCount = itemCount // i know ugly af
holder.bind(cardList[position], position)
}
}
}
override fun getItemCount(): Int {
return cardList.size
}
override fun getItemId(position: Int): Long {
return (cardList[position].id ?: position).toLong()
}
fun updateList(newList: List<SearchResponse>) {
val diffResult = DiffUtil.calculateDiff(
HomeChildDiffCallback(this.cardList, newList)
)
cardList.clear()
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) {
override fun onBindContent(
holder: ViewHolderState<Boolean>,
item: SearchResponse,
position: Int
) {
when (val binding = holder.view) {
is HomeResultGridBinding -> {
binding.backgroundCard.apply {
val min = 114.toPx
@ -136,8 +94,6 @@ class HomeChildItemAdapter(
}
}
}
}
is HomeResultGridExpandedBinding -> {
@ -167,36 +123,21 @@ class HomeChildItemAdapter(
}
SearchResultBuilder.bind(
clickCallback,
card,
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,
itemView,
holder.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

@ -451,10 +451,6 @@ class HomeFragment : Fragment() {
}
override fun onDestroyView() {
homeMasterAdapter?.onSaveInstanceState(
instanceState,
binding?.homeMasterRecycler
)
bottomSheetDialog?.ownHide()
binding = null
@ -517,11 +513,9 @@ class HomeFragment : Fragment() {
}
}
homeMasterAdapter = HomeParentItemAdapterPreview(
mutableListOf(),
fragment = this@HomeFragment,
homeViewModel,
).apply {
onRestoreInstanceState(instanceState)
}
)
homeMasterRecycler.adapter = homeMasterAdapter
//fixPaddingStatusbar(homeLoadingStatusbar)
@ -572,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
@ -624,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,24 +1,23 @@
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
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.recyclerview.widget.RecyclerView.ViewHolder
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.Globals.EMULATOR
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
import com.lagradost.cloudstream3.ui.settings.Globals.TV
@ -33,256 +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<ViewHolder>() {
// Ok, this is fucked, but there is a reason for this as we want to resume 1. when scrolling up and down
// and 2. when doing into a thing and coming back. 1 is always active, but 2 requires doing it in the fragment
// as OnCreateView is called and this adapter is recreated losing the internal state to the GC
//
// 1. This works by having the adapter having a internal state "scrollStates" that keeps track of the states
// when a view recycles, it looks up this internal state
// 2. To solve the the coming back shit we have to save "scrollStates" to a Bundle inside the
// fragment via onSaveInstanceState, because this cant be easy for some reason as the adapter does
// not have a state but the layout-manager for no reason, then it is resumed via onRestoreInstanceState
//
// Even when looking at a real example they do this :skull:
// https://github.com/vivchar/RendererRecyclerViewAdapter/blob/185251ee9d94fb6eb3e063b00d646b745186c365/example/src/main/java/com/github/vivchar/example/pages/github/GithubFragment.kt#L32
private val scrollStates = mutableMapOf<Int, Parcelable?>()
companion object {
private const val SCROLL_KEY: String = "ParentItemAdapter::scrollStates.keys"
private const val SCROLL_VALUE: String = "ParentItemAdapter::scrollStates.values"
}
open fun onRestoreInstanceState(savedInstanceState: Bundle?) {
try {
val keys = savedInstanceState?.getIntArray(SCROLL_KEY) ?: intArrayOf()
val values = savedInstanceState?.getParcelableArray(SCROLL_VALUE) ?: arrayOf()
for ((k, v) in keys.zip(values)) {
this.scrollStates[k] = v
}
} catch (t: Throwable) {
logError(t)
}
}
open fun onSaveInstanceState(outState: Bundle, recyclerView: RecyclerView? = null) {
if (recyclerView != null) {
for (position in items.indices) {
val holder = recyclerView.findViewHolderForAdapterPosition(position) ?: continue
saveHolder(holder)
}
}
outState.putIntArray(SCROLL_KEY, scrollStates.keys.toIntArray())
outState.putParcelableArray(SCROLL_VALUE, scrollStates.values.toTypedArray())
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
when (holder) {
is ParentViewHolder -> {
holder.bind(items[position])
scrollStates[holder.absoluteAdapterPosition]?.let {
holder.binding.homeChildRecyclerview.layoutManager?.onRestoreInstanceState(it)
}
}
}
}
private fun saveHolder(holder : ViewHolder) {
when (holder) {
is ParentViewHolder -> {
scrollStates[holder.absoluteAdapterPosition] =
holder.binding.homeChildRecyclerview.layoutManager?.onSaveInstanceState()
}
}
}
override fun onViewRecycled(holder: ViewHolder) {
saveHolder(holder)
super.onViewRecycled(holder)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val layoutResId = when {
isLayout(TV) -> R.layout.homepage_parent_tv
isLayout(EMULATOR) -> R.layout.homepage_parent_emulator
else -> R.layout.homepage_parent
}
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)
}
return ParentViewHolder(
binding,
clickCallback,
moreInfoClickCallback,
expandCallback
)
}
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)
) {
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)
}
class ParentViewHolder(
val binding: HomepageParentBinding,
// val viewModel: HomeViewModel,
private val clickCallback: (SearchClickCallback) -> Unit,
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
private val expandCallback: ((String) -> Unit)? = null,
) :
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,
override fun restore(state: Bundle) {
(binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState(
state.getParcelable("value")
)
}
}
fun bind(expand: HomeViewModel.ExpandableHomepageList) {
val info = expand.list
recyclerView.adapter = HomeChildItemAdapter(
info.list.toMutableList(),
override fun onUpdateContent(
holder: ViewHolderState<Bundle>,
item: HomeViewModel.ExpandableHomepageList,
position: Int
) {
val binding = holder.view
if (binding !is HomepageParentBinding) return
(binding.homeChildRecyclerview.adapter as? HomeChildItemAdapter)?.submitList(item.list.list)
}
override fun onBindContent(
holder: ViewHolderState<Bundle>,
item: HomeViewModel.ExpandableHomepageList,
position: Int
) {
val startFocus = R.id.nav_rail_view
val endFocus = FOCUS_SELF
val binding = holder.view
if (binding !is HomepageParentBinding) return
val info = item.list
binding.apply {
homeChildRecyclerview.adapter = HomeChildItemAdapter(
fragment = fragment,
id = id + position + 100,
clickCallback = clickCallback,
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
@ -307,26 +135,34 @@ open class ParentItemAdapter(
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
if (isLayout(PHONE)) {
title.setOnClickListener {
moreInfoClickCallback.invoke(expand)
}
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 getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldList[oldItemPosition] == newList[newItemPosition]
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
}
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)
}
return ParentItemHolder(binding)
}
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
@ -47,9 +51,9 @@ 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,
) : ParentItemAdapter(fragment, id = "HomeParentItemAdapterPreview".hashCode(),
clickCallback = {
viewModel.click(it)
}, moreInfoClickCallback = {
@ -57,28 +61,8 @@ class HomeParentItemAdapterPreview(
}, expandCallback = {
viewModel.expand(it)
}) {
val headItems = 1
companion object {
private const val VIEW_TYPE_HEADER = 2
private const val VIEW_TYPE_ITEM = 1
}
override fun getItemViewType(position: Int) = when (position) {
0 -> VIEW_TYPE_HEADER
else -> VIEW_TYPE_ITEM
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> {}
else -> super.onBindViewHolder(holder, position - headItems)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
VIEW_TYPE_HEADER -> {
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,
@ -107,54 +91,47 @@ class HomeParentItemAdapterPreview(
)
}
HeaderViewHolder(
binding,
viewModel,
return HeaderViewHolder(binding, viewModel, fragment = fragment)
}
override fun onBindHeader(holder: ViewHolderState<Bundle>) {
(holder as? HeaderViewHolder)?.bind()
}
private class HeaderViewHolder(
val binding: ViewBinding, val viewModel: HomeViewModel, fragment: Fragment,
) :
ViewHolderState<Bundle>(binding) {
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 restore(state: Bundle) {
state.getParcelable<Parcelable>("resumeRecyclerView")?.let { recycle ->
resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
}
state.getParcelable<Parcelable>("bookmarkRecyclerView")?.let { recycle ->
bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
}
//state.getInt("previewViewpager").let { recycle ->
// previewViewpager.setCurrentItem(recycle,true)
//}
}
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()
}
else -> super.onViewDetachedFromWindow(holder)
}
}
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
when (holder) {
is HeaderViewHolder -> {
holder.onViewAttachedToWindow()
}
else -> super.onViewAttachedToWindow(holder)
}
}
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 ->
@ -209,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 ->
@ -219,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,
@ -269,7 +250,6 @@ class HomeParentItemAdapterPreview(
*/
}
private val previewViewpager: ViewPager2 =
itemView.findViewById(R.id.home_preview_viewpager)
@ -277,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 =
@ -381,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,
@ -413,40 +379,24 @@ class HomeParentItemAdapterPreview(
}
}
fun onViewDetachedFromWindow() {
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.getItemOrNull(position) ?: return
onSelect(item, position)
}
}
override 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)
}
}
toggleListHolder?.isGone = visible.isEmpty()
}
} ?: debugException { "Expected findViewTreeLifecycleOwner" }
}
private val toggleList = listOf<Pair<Chip, WatchType>>(
Pair(itemView.findViewById(R.id.home_type_watching_btt), WatchType.WATCHING),
Pair(itemView.findViewById(R.id.home_type_completed_btt), WatchType.COMPLETED),
@ -457,6 +407,8 @@ class HomeParentItemAdapterPreview(
private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder)
fun bind() = Unit
init {
previewViewpager.setPageTransformer(HomeScrollTransformer())
@ -563,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
)
@ -575,15 +529,16 @@ class HomeParentItemAdapterPreview(
previewViewpager.fakeDragBy(1f)
previewViewpager.endFakeDrag()
previewCallback.onPageSelected(0)
//previewHeader.isVisible = true
}*/
previewViewpager.isVisible = true
previewViewpagerText.isVisible = true
alternativeAccountPadding?.isVisible = false
//previewHeader.isVisible = true
}
}
else -> {
previewAdapter.setItems(listOf(), false)
previewAdapter.submitList(listOf())
previewViewpager.setCurrentItem(0, false)
previewViewpager.isVisible = false
previewViewpagerText.isVisible = false
@ -595,7 +550,7 @@ class HomeParentItemAdapterPreview(
private fun updateResume(resumeWatching: List<SearchResponse>) {
resumeHolder.isVisible = resumeWatching.isNotEmpty()
resumeAdapter.updateList(resumeWatching)
resumeAdapter.submitList(resumeWatching)
if (
binding is FragmentHomeHeadBinding ||
@ -625,7 +580,7 @@ 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 ||
@ -655,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,43 +4,23 @@ 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.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 (isLayout(TV or EMULATOR)) {
HomeScrollViewTvBinding.inflate(inflater, parent, false)
@ -48,44 +28,32 @@ class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
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])
}
}
}
class CardViewHolder
constructor(
val binding: ViewBinding,
//private val forceHorizontalPosters: Boolean? = null
) :
RecyclerView.ViewHolder(binding.root) {
fun bind(card: LoadResponse) {
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) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl
?: card.backgroundPosterUrl
if (isHorizontal) item.backgroundPosterUrl ?: item.posterUrl else item.posterUrl
?: item.backgroundPosterUrl
when (binding) {
is HomeScrollViewBinding -> {
binding.homeScrollPreview.setImage(posterUrl)
binding.homeScrollPreviewTags.apply {
text = card.tags?.joinToString("") ?: ""
isGone = card.tags.isNullOrEmpty()
text = item.tags?.joinToString("") ?: ""
isGone = item.tags.isNullOrEmpty()
maxLines = 2
}
binding.homeScrollPreviewTitle.text = card.name
binding.homeScrollPreviewTitle.text = item.name
}
is HomeScrollViewTvBinding -> {
@ -94,24 +62,3 @@ class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
}
}
}
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

@ -53,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() {
@ -125,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
@ -327,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
)
}
}
@ -342,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,12 +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.Globals
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.SettingsFragment
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
import com.lagradost.cloudstream3.utils.AppUtils.loadSearchResult
import com.lagradost.cloudstream3.utils.AppUtils.reduceDragSensitivity
@ -62,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"
@ -165,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)
@ -312,9 +312,8 @@ class LibraryFragment : Fragment() {
binding?.viewpager?.setPageTransformer(LibraryScrollTransformer())
binding?.viewpager?.adapter =
binding?.viewpager?.adapter ?: ViewpagerAdapter(
mutableListOf(),
binding?.viewpager?.adapter = ViewpagerAdapter(
fragment = this,
{ isScrollingDown: Boolean ->
if (isScrollingDown) {
binding?.sortFab?.shrink()
@ -337,7 +336,10 @@ class LibraryFragment : Fragment() {
when (searchClickCallback.action) {
SEARCH_ACTION_SHOW_METADATA -> {
(activity as? MainActivity)?.loadPopup(searchClickCallback.card, load = false)
(activity as? MainActivity)?.loadPopup(
searchClickCallback.card,
load = false
)
/*activity?.showPluginSelectionDialog(
syncId,
syncName,
@ -395,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"
@ -403,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)
@ -464,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
})
@ -569,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,72 +1,95 @@
package com.lagradost.cloudstream3.ui.library
import android.os.Build
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.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) {
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(
page.items.toMutableList(),
item.items.toMutableList(),
this,
clickCallback
)
}
} else {
(adapter as? PageAdapter)?.updateList(page.items)
scrollToPosition(0)
(adapter as? PageAdapter)?.updateList(item.items)
// scrollToPosition(0)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
@ -98,8 +121,3 @@ class ViewpagerAdapter(
}
}
}
override fun getItemCount(): Int {
return pages.size
}
}

View file

@ -47,6 +47,9 @@ 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
@ -78,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)
@ -1205,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
}

View file

@ -174,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 -> {

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
@ -161,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()
@ -506,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

@ -14,6 +14,7 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.lagradost.cloudstream3.BuildConfig
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.MainSettingsBinding
import com.lagradost.cloudstream3.mvvm.logError
@ -179,6 +180,9 @@ class SettingsFragment : Fragment() {
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")

View file

@ -9,6 +9,7 @@ import android.widget.ArrayAdapter
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.databinding.FragmentSetupLayoutBinding
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
@ -86,6 +87,7 @@ class SetupFragmentLayout : Fragment() {
nextBtt.setOnClickListener {
setKey(HAS_DONE_SETUP_KEY, true)
findNavController().navigate(R.id.navigation_home)
}

View file

@ -120,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"
@ -136,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" />