mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
Added BaseAdapter to store internal state
This commit is contained in:
parent
a3bb853691
commit
8d5b73495d
10 changed files with 671 additions and 679 deletions
246
app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt
Normal file
246
app/src/main/java/com/lagradost/cloudstream3/ui/BaseAdapter.kt
Normal file
|
@ -0,0 +1,246 @@
|
||||||
|
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
|
||||||
|
|
||||||
|
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?>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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>?) {
|
||||||
|
mDiffer.submitList(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,31 +2,58 @@ package com.lagradost.cloudstream3.ui.home
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.SearchResponse
|
import com.lagradost.cloudstream3.SearchResponse
|
||||||
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
import com.lagradost.cloudstream3.databinding.HomeResultGridBinding
|
||||||
import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding
|
import com.lagradost.cloudstream3.databinding.HomeResultGridExpandedBinding
|
||||||
|
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||||
|
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||||
|
import com.lagradost.cloudstream3.ui.search.SEARCH_ACTION_LOAD
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
import com.lagradost.cloudstream3.ui.search.SearchResultBuilder
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isRtl
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
|
import com.lagradost.cloudstream3.utils.UIHelper.IsBottomLayout
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
import com.lagradost.cloudstream3.utils.UIHelper.toPx
|
||||||
|
|
||||||
class HomeChildItemAdapter(
|
class HomeScrollViewHolderState(view: ViewBinding) : ViewHolderState<Boolean>(view) {
|
||||||
val cardList: MutableList<SearchResponse>,
|
/*private fun recursive(view : View) : Boolean {
|
||||||
|
if (view.isFocused) {
|
||||||
|
println("VIEW: $view | id=${view.id}")
|
||||||
|
}
|
||||||
|
return (view as? ViewGroup)?.children?.any { recursive(it) } ?: false
|
||||||
|
}*/
|
||||||
|
|
||||||
|
// very shitty that we cant store the state when the view clears,
|
||||||
|
// but this is because the focus clears before the view is removed
|
||||||
|
// so we have to manually store it
|
||||||
|
var wasFocused: Boolean = false
|
||||||
|
override fun save(): Boolean = wasFocused
|
||||||
|
override fun restore(state: Boolean) {
|
||||||
|
if (state) {
|
||||||
|
wasFocused = false
|
||||||
|
// only refocus if tv
|
||||||
|
if(isLayout(TV)) {
|
||||||
|
itemView.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HomeChildItemAdapter(
|
||||||
|
fragment: Fragment,
|
||||||
|
id: Int,
|
||||||
private val nextFocusUp: Int? = null,
|
private val nextFocusUp: Int? = null,
|
||||||
private val nextFocusDown: Int? = null,
|
private val nextFocusDown: Int? = null,
|
||||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||||
) :
|
) :
|
||||||
RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
BaseAdapter<SearchResponse, Boolean>(fragment, id) {
|
||||||
var isHorizontal: Boolean = false
|
var isHorizontal: Boolean = false
|
||||||
var hasNext: Boolean = false
|
var hasNext: Boolean = false
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Boolean> {
|
||||||
val expanded = parent.context.IsBottomLayout()
|
val expanded = parent.context.IsBottomLayout()
|
||||||
/* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid
|
/* val layout = if (bottom) R.layout.home_result_grid_expanded else R.layout.home_result_grid
|
||||||
|
|
||||||
|
@ -39,164 +66,78 @@ class HomeChildItemAdapter(
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
) else HomeResultGridBinding.inflate(inflater, parent, false)
|
) else HomeResultGridBinding.inflate(inflater, parent, false)
|
||||||
|
return HomeScrollViewHolderState(binding)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindContent(
|
||||||
|
holder: ViewHolderState<Boolean>,
|
||||||
|
item: SearchResponse,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
|
when (val binding = holder.view) {
|
||||||
|
is HomeResultGridBinding -> {
|
||||||
|
binding.backgroundCard.apply {
|
||||||
|
val min = 114.toPx
|
||||||
|
val max = 180.toPx
|
||||||
|
|
||||||
return CardViewHolder(
|
layoutParams =
|
||||||
binding,
|
layoutParams.apply {
|
||||||
clickCallback,
|
width = if (!isHorizontal) {
|
||||||
itemCount,
|
min
|
||||||
|
} else {
|
||||||
|
max
|
||||||
|
}
|
||||||
|
height = if (!isHorizontal) {
|
||||||
|
max
|
||||||
|
} else {
|
||||||
|
min
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is HomeResultGridExpandedBinding -> {
|
||||||
|
binding.backgroundCard.apply {
|
||||||
|
val min = 114.toPx
|
||||||
|
val max = 180.toPx
|
||||||
|
|
||||||
|
layoutParams =
|
||||||
|
layoutParams.apply {
|
||||||
|
width = if (!isHorizontal) {
|
||||||
|
min
|
||||||
|
} else {
|
||||||
|
max
|
||||||
|
}
|
||||||
|
height = if (!isHorizontal) {
|
||||||
|
max
|
||||||
|
} else {
|
||||||
|
min
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position == 0) { // to fix tv
|
||||||
|
binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchResultBuilder.bind(
|
||||||
|
clickCallback = { click ->
|
||||||
|
// ok, so here we hijack the callback to fix the focus
|
||||||
|
when (click.action) {
|
||||||
|
SEARCH_ACTION_LOAD -> (holder as? HomeScrollViewHolderState)?.wasFocused = true
|
||||||
|
}
|
||||||
|
clickCallback(click)
|
||||||
|
},
|
||||||
|
item,
|
||||||
|
position,
|
||||||
|
holder.itemView,
|
||||||
|
null, // nextFocusBehavior,
|
||||||
nextFocusUp,
|
nextFocusUp,
|
||||||
nextFocusDown,
|
nextFocusDown
|
||||||
isHorizontal,
|
|
||||||
parent.isRtl()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
|
||||||
when (holder) {
|
|
||||||
is CardViewHolder -> {
|
|
||||||
holder.itemCount = itemCount // i know ugly af
|
|
||||||
holder.bind(cardList[position], position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return cardList.size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return (cardList[position].id ?: position).toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateList(newList: List<SearchResponse>) {
|
|
||||||
val diffResult = DiffUtil.calculateDiff(
|
|
||||||
HomeChildDiffCallback(this.cardList, newList)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
cardList.clear()
|
holder.itemView.tag = position
|
||||||
cardList.addAll(newList)
|
|
||||||
|
|
||||||
diffResult.dispatchUpdatesTo(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
class CardViewHolder
|
|
||||||
constructor(
|
|
||||||
val binding: ViewBinding,
|
|
||||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
|
||||||
var itemCount: Int,
|
|
||||||
private val nextFocusUp: Int? = null,
|
|
||||||
private val nextFocusDown: Int? = null,
|
|
||||||
private val isHorizontal: Boolean = false,
|
|
||||||
private val isRtl: Boolean
|
|
||||||
) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun bind(card: SearchResponse, position: Int) {
|
|
||||||
|
|
||||||
// TV focus fixing
|
|
||||||
/*val nextFocusBehavior = when (position) {
|
|
||||||
0 -> true
|
|
||||||
itemCount - 1 -> false
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position == 0) { // to fix tv
|
|
||||||
if (isRtl) {
|
|
||||||
itemView.nextFocusRightId = R.id.nav_rail_view
|
|
||||||
itemView.nextFocusLeftId = -1
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
itemView.nextFocusLeftId = R.id.nav_rail_view
|
|
||||||
itemView.nextFocusRightId = -1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
itemView.nextFocusRightId = -1
|
|
||||||
itemView.nextFocusLeftId = -1
|
|
||||||
}*/
|
|
||||||
|
|
||||||
|
|
||||||
when (binding) {
|
|
||||||
is HomeResultGridBinding -> {
|
|
||||||
binding.backgroundCard.apply {
|
|
||||||
val min = 114.toPx
|
|
||||||
val max = 180.toPx
|
|
||||||
|
|
||||||
layoutParams =
|
|
||||||
layoutParams.apply {
|
|
||||||
width = if (!isHorizontal) {
|
|
||||||
min
|
|
||||||
} else {
|
|
||||||
max
|
|
||||||
}
|
|
||||||
height = if (!isHorizontal) {
|
|
||||||
max
|
|
||||||
} else {
|
|
||||||
min
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
is HomeResultGridExpandedBinding -> {
|
|
||||||
binding.backgroundCard.apply {
|
|
||||||
val min = 114.toPx
|
|
||||||
val max = 180.toPx
|
|
||||||
|
|
||||||
layoutParams =
|
|
||||||
layoutParams.apply {
|
|
||||||
width = if (!isHorizontal) {
|
|
||||||
min
|
|
||||||
} else {
|
|
||||||
max
|
|
||||||
}
|
|
||||||
height = if (!isHorizontal) {
|
|
||||||
max
|
|
||||||
} else {
|
|
||||||
min
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (position == 0) { // to fix tv
|
|
||||||
binding.backgroundCard.nextFocusLeftId = R.id.nav_rail_view
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchResultBuilder.bind(
|
|
||||||
clickCallback,
|
|
||||||
card,
|
|
||||||
position,
|
|
||||||
itemView,
|
|
||||||
null, // nextFocusBehavior,
|
|
||||||
nextFocusUp,
|
|
||||||
nextFocusDown
|
|
||||||
)
|
|
||||||
itemView.tag = position
|
|
||||||
|
|
||||||
//val ani = ScaleAnimation(0.9f, 1.0f, 0.9f, 1f)
|
|
||||||
//ani.fillAfter = true
|
|
||||||
//ani.duration = 200
|
|
||||||
//itemView.startAnimation(ani)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class HomeChildDiffCallback(
|
|
||||||
private val oldList: List<SearchResponse>,
|
|
||||||
private val newList: List<SearchResponse>
|
|
||||||
) :
|
|
||||||
DiffUtil.Callback() {
|
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
|
||||||
oldList[oldItemPosition].name == newList[newItemPosition].name
|
|
||||||
|
|
||||||
override fun getOldListSize() = oldList.size
|
|
||||||
|
|
||||||
override fun getNewListSize() = newList.size
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
|
||||||
oldList[oldItemPosition] == newList[newItemPosition] && oldItemPosition < oldList.size - 1 // always update the last item
|
|
||||||
}
|
|
|
@ -451,10 +451,6 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
homeMasterAdapter?.onSaveInstanceState(
|
|
||||||
instanceState,
|
|
||||||
binding?.homeMasterRecycler
|
|
||||||
)
|
|
||||||
|
|
||||||
bottomSheetDialog?.ownHide()
|
bottomSheetDialog?.ownHide()
|
||||||
binding = null
|
binding = null
|
||||||
|
@ -517,11 +513,9 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
homeMasterAdapter = HomeParentItemAdapterPreview(
|
homeMasterAdapter = HomeParentItemAdapterPreview(
|
||||||
mutableListOf(),
|
fragment = this@HomeFragment,
|
||||||
homeViewModel,
|
homeViewModel,
|
||||||
).apply {
|
)
|
||||||
onRestoreInstanceState(instanceState)
|
|
||||||
}
|
|
||||||
homeMasterRecycler.adapter = homeMasterAdapter
|
homeMasterRecycler.adapter = homeMasterAdapter
|
||||||
//fixPaddingStatusbar(homeLoadingStatusbar)
|
//fixPaddingStatusbar(homeLoadingStatusbar)
|
||||||
|
|
||||||
|
@ -572,10 +566,11 @@ class HomeFragment : Fragment() {
|
||||||
val mutableListOfResponse = mutableListOf<SearchResponse>()
|
val mutableListOfResponse = mutableListOf<SearchResponse>()
|
||||||
listHomepageItems.clear()
|
listHomepageItems.clear()
|
||||||
|
|
||||||
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(
|
(homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(d.values.map {
|
||||||
d.values.toMutableList(),
|
it.copy(
|
||||||
homeMasterRecycler
|
list = it.list.copy(list = it.list.list.toMutableList())
|
||||||
)
|
)
|
||||||
|
}.toMutableList())
|
||||||
|
|
||||||
homeLoading.isVisible = false
|
homeLoading.isVisible = false
|
||||||
homeLoadingError.isVisible = false
|
homeLoadingError.isVisible = false
|
||||||
|
@ -624,7 +619,7 @@ class HomeFragment : Fragment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
is Resource.Loading -> {
|
is Resource.Loading -> {
|
||||||
(homeMasterRecycler.adapter as? ParentItemAdapter)?.updateList(listOf())
|
(homeMasterRecycler.adapter as? ParentItemAdapter)?.submitList(listOf())
|
||||||
homeLoadingShimmer.startShimmer()
|
homeLoadingShimmer.startShimmer()
|
||||||
homeLoading.isVisible = true
|
homeLoading.isVisible = true
|
||||||
homeLoadingError.isVisible = false
|
homeLoadingError.isVisible = false
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
package com.lagradost.cloudstream3.ui.home
|
package com.lagradost.cloudstream3.ui.home
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.TextView
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.ListUpdateCallback
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
import androidx.viewbinding.ViewBinding
|
||||||
import com.lagradost.cloudstream3.HomePageList
|
import com.lagradost.cloudstream3.HomePageList
|
||||||
import com.lagradost.cloudstream3.LoadResponse
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
import com.lagradost.cloudstream3.databinding.HomepageParentBinding
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
|
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||||
|
import com.lagradost.cloudstream3.ui.BaseDiffCallback
|
||||||
|
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||||
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
import com.lagradost.cloudstream3.ui.result.FOCUS_SELF
|
||||||
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
import com.lagradost.cloudstream3.ui.result.setLinearListLayout
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
import com.lagradost.cloudstream3.ui.search.SearchClickCallback
|
||||||
import com.lagradost.cloudstream3.ui.search.SearchFragment.Companion.filterSearchResponse
|
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
import com.lagradost.cloudstream3.ui.settings.Globals.PHONE
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
@ -33,256 +32,85 @@ class LoadClickCallback(
|
||||||
)
|
)
|
||||||
|
|
||||||
open class ParentItemAdapter(
|
open class ParentItemAdapter(
|
||||||
private var items: MutableList<HomeViewModel.ExpandableHomepageList>,
|
open val fragment: Fragment,
|
||||||
//private val viewModel: HomeViewModel,
|
id: Int,
|
||||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
private val clickCallback: (SearchClickCallback) -> Unit,
|
||||||
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
|
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
|
||||||
private val expandCallback: ((String) -> Unit)? = null,
|
private val expandCallback: ((String) -> Unit)? = null,
|
||||||
) : RecyclerView.Adapter<ViewHolder>() {
|
) : BaseAdapter<HomeViewModel.ExpandableHomepageList, Bundle>(
|
||||||
// Ok, this is fucked, but there is a reason for this as we want to resume 1. when scrolling up and down
|
fragment,
|
||||||
// and 2. when doing into a thing and coming back. 1 is always active, but 2 requires doing it in the fragment
|
id,
|
||||||
// as OnCreateView is called and this adapter is recreated losing the internal state to the GC
|
diffCallback = BaseDiffCallback(
|
||||||
//
|
itemSame = { a, b -> a.list.name == b.list.name },
|
||||||
// 1. This works by having the adapter having a internal state "scrollStates" that keeps track of the states
|
contentSame = { a, b ->
|
||||||
// when a view recycles, it looks up this internal state
|
a.list.list == b.list.list
|
||||||
// 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 0..itemCount) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
) {
|
||||||
//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(
|
||||||
class ParentViewHolder(
|
"value",
|
||||||
val binding: HomepageParentBinding,
|
recyclerView?.layoutManager?.onSaveInstanceState()
|
||||||
// val viewModel: HomeViewModel,
|
)
|
||||||
private val clickCallback: (SearchClickCallback) -> Unit,
|
(recyclerView?.adapter as? BaseAdapter<*,*>)?.save(recyclerView)
|
||||||
private val moreInfoClickCallback: (HomeViewModel.ExpandableHomepageList) -> Unit,
|
|
||||||
private val expandCallback: ((String) -> Unit)? = null,
|
|
||||||
) :
|
|
||||||
ViewHolder(binding.root) {
|
|
||||||
val title: TextView = binding.homeChildMoreInfo
|
|
||||||
private val recyclerView: RecyclerView = binding.homeChildRecyclerview
|
|
||||||
private val startFocus = R.id.nav_rail_view
|
|
||||||
private val endFocus = FOCUS_SELF
|
|
||||||
fun update(expand: HomeViewModel.ExpandableHomepageList) {
|
|
||||||
val info = expand.list
|
|
||||||
(recyclerView.adapter as? HomeChildItemAdapter?)?.apply {
|
|
||||||
updateList(info.list.toMutableList())
|
|
||||||
hasNext = expand.hasNext
|
|
||||||
} ?: run {
|
|
||||||
recyclerView.adapter = HomeChildItemAdapter(
|
|
||||||
info.list.toMutableList(),
|
|
||||||
clickCallback = clickCallback,
|
|
||||||
nextFocusUp = recyclerView.nextFocusUpId,
|
|
||||||
nextFocusDown = recyclerView.nextFocusDownId,
|
|
||||||
).apply {
|
|
||||||
isHorizontal = info.isHorizontalImages
|
|
||||||
hasNext = expand.hasNext
|
|
||||||
}
|
|
||||||
recyclerView.setLinearListLayout(
|
|
||||||
isHorizontal = true,
|
|
||||||
nextLeft = startFocus,
|
|
||||||
nextRight = endFocus,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(expand: HomeViewModel.ExpandableHomepageList) {
|
override fun restore(state: Bundle) {
|
||||||
val info = expand.list
|
(binding as? HomepageParentBinding)?.homeChildRecyclerview?.layoutManager?.onRestoreInstanceState(
|
||||||
recyclerView.adapter = HomeChildItemAdapter(
|
state.getParcelable("value")
|
||||||
info.list.toMutableList(),
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onUpdateContent(
|
||||||
|
holder: ViewHolderState<Bundle>,
|
||||||
|
item: HomeViewModel.ExpandableHomepageList,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
|
val binding = holder.view
|
||||||
|
if (binding !is HomepageParentBinding) return
|
||||||
|
(binding.homeChildRecyclerview.adapter as? HomeChildItemAdapter)?.submitList(item.list.list)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindContent(
|
||||||
|
holder: ViewHolderState<Bundle>,
|
||||||
|
item: HomeViewModel.ExpandableHomepageList,
|
||||||
|
position: Int
|
||||||
|
) {
|
||||||
|
val startFocus = R.id.nav_rail_view
|
||||||
|
val endFocus = FOCUS_SELF
|
||||||
|
val binding = holder.view
|
||||||
|
if (binding !is HomepageParentBinding) return
|
||||||
|
val info = item.list
|
||||||
|
binding.apply {
|
||||||
|
homeChildRecyclerview.adapter = HomeChildItemAdapter(
|
||||||
|
fragment = fragment,
|
||||||
|
id = id + position + 100,
|
||||||
clickCallback = clickCallback,
|
clickCallback = clickCallback,
|
||||||
nextFocusUp = recyclerView.nextFocusUpId,
|
nextFocusUp = homeChildRecyclerview.nextFocusUpId,
|
||||||
nextFocusDown = recyclerView.nextFocusDownId,
|
nextFocusDown = homeChildRecyclerview.nextFocusDownId,
|
||||||
).apply {
|
).apply {
|
||||||
isHorizontal = info.isHorizontalImages
|
isHorizontal = info.isHorizontalImages
|
||||||
hasNext = expand.hasNext
|
hasNext = item.hasNext
|
||||||
|
submitList(item.list.list)
|
||||||
}
|
}
|
||||||
recyclerView.setLinearListLayout(
|
homeChildRecyclerview.setLinearListLayout(
|
||||||
isHorizontal = true,
|
isHorizontal = true,
|
||||||
nextLeft = startFocus,
|
nextLeft = startFocus,
|
||||||
nextRight = endFocus,
|
nextRight = endFocus,
|
||||||
)
|
)
|
||||||
title.text = info.name
|
homeChildMoreInfo.text = info.name
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
homeChildRecyclerview.addOnScrollListener(object :
|
||||||
|
RecyclerView.OnScrollListener() {
|
||||||
var expandCount = 0
|
var expandCount = 0
|
||||||
val name = expand.list.name
|
val name = item.list.name
|
||||||
|
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(
|
||||||
|
recyclerView: RecyclerView,
|
||||||
|
newState: Int
|
||||||
|
) {
|
||||||
super.onScrollStateChanged(recyclerView, newState)
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
|
|
||||||
val adapter = recyclerView.adapter
|
val adapter = recyclerView.adapter
|
||||||
|
@ -307,26 +135,34 @@ open class ParentItemAdapter(
|
||||||
|
|
||||||
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
|
//(recyclerView.adapter as HomeChildItemAdapter).notifyDataSetChanged()
|
||||||
if (isLayout(PHONE)) {
|
if (isLayout(PHONE)) {
|
||||||
title.setOnClickListener {
|
homeChildMoreInfo.setOnClickListener {
|
||||||
moreInfoClickCallback.invoke(expand)
|
moreInfoClickCallback.invoke(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class SearchDiffCallback(
|
override fun onCreateContent(parent: ViewGroup): ParentItemHolder {
|
||||||
private val oldList: List<HomeViewModel.ExpandableHomepageList>,
|
val layoutResId = when {
|
||||||
private val newList: List<HomeViewModel.ExpandableHomepageList>
|
isLayout(TV) -> R.layout.homepage_parent_tv
|
||||||
) :
|
isLayout(EMULATOR) -> R.layout.homepage_parent_emulator
|
||||||
DiffUtil.Callback() {
|
else -> R.layout.homepage_parent
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
}
|
||||||
oldList[oldItemPosition].list.name == newList[newItemPosition].list.name
|
|
||||||
|
|
||||||
override fun getOldListSize() = oldList.size
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
val binding = try {
|
||||||
|
HomepageParentBinding.bind(inflater.inflate(layoutResId, parent, false))
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
logError(t)
|
||||||
|
// just in case someone forgot we don't want to crash
|
||||||
|
HomepageParentBinding.inflate(inflater)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getNewListSize() = newList.size
|
return ParentItemHolder(binding)
|
||||||
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
fun updateList(newList: List<HomePageList>) {
|
||||||
oldList[oldItemPosition] == newList[newItemPosition]
|
submitList(newList.map { HomeViewModel.ExpandableHomepageList(it, 1, false) }
|
||||||
}
|
.toMutableList())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
package com.lagradost.cloudstream3.ui.home
|
package com.lagradost.cloudstream3.ui.home
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -7,6 +9,7 @@ import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
|
@ -26,6 +29,7 @@ import com.lagradost.cloudstream3.databinding.FragmentHomeHeadTvBinding
|
||||||
import com.lagradost.cloudstream3.mvvm.Resource
|
import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.debugException
|
import com.lagradost.cloudstream3.mvvm.debugException
|
||||||
import com.lagradost.cloudstream3.mvvm.observe
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
|
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||||
import com.lagradost.cloudstream3.ui.WatchType
|
import com.lagradost.cloudstream3.ui.WatchType
|
||||||
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear
|
import com.lagradost.cloudstream3.ui.account.AccountHelper.showAccountSelectLinear
|
||||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage
|
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.selectHomepage
|
||||||
|
@ -47,114 +51,87 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixPaddingStatusbarView
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.populateChips
|
import com.lagradost.cloudstream3.utils.UIHelper.populateChips
|
||||||
|
|
||||||
class HomeParentItemAdapterPreview(
|
class HomeParentItemAdapterPreview(
|
||||||
items: MutableList<HomeViewModel.ExpandableHomepageList>,
|
override val fragment: Fragment,
|
||||||
private val viewModel: HomeViewModel,
|
private val viewModel: HomeViewModel,
|
||||||
) : ParentItemAdapter(items,
|
) : ParentItemAdapter(fragment, id = "HomeParentItemAdapterPreview".hashCode(),
|
||||||
clickCallback = {
|
clickCallback = {
|
||||||
viewModel.click(it)
|
viewModel.click(it)
|
||||||
}, moreInfoClickCallback = {
|
}, moreInfoClickCallback = {
|
||||||
viewModel.popup(it)
|
viewModel.popup(it)
|
||||||
}, expandCallback = {
|
}, expandCallback = {
|
||||||
viewModel.expand(it)
|
viewModel.expand(it)
|
||||||
}) {
|
}) {
|
||||||
val headItems = 1
|
override val headers = 1
|
||||||
|
override fun onCreateHeader(parent: ViewGroup): ViewHolderState<Bundle> {
|
||||||
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
|
val binding = if (isLayout(TV or EMULATOR)) FragmentHomeHeadTvBinding.inflate(
|
||||||
|
inflater,
|
||||||
|
parent,
|
||||||
|
false
|
||||||
|
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
|
||||||
|
|
||||||
companion object {
|
if (binding is FragmentHomeHeadTvBinding && isLayout(EMULATOR)) {
|
||||||
private const val VIEW_TYPE_HEADER = 2
|
binding.homeBookmarkParentItemMoreInfo.isVisible = true
|
||||||
private const val VIEW_TYPE_ITEM = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) = when (position) {
|
val marginInDp = 50
|
||||||
0 -> VIEW_TYPE_HEADER
|
val density = binding.horizontalScrollChips.context.resources.displayMetrics.density
|
||||||
else -> VIEW_TYPE_ITEM
|
val marginInPixels = (marginInDp * density).toInt()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams
|
||||||
when (holder) {
|
params.marginEnd = marginInPixels
|
||||||
is HeaderViewHolder -> {}
|
binding.horizontalScrollChips.layoutParams = params
|
||||||
else -> super.onBindViewHolder(holder, position - headItems)
|
binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds(
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
ContextCompat.getDrawable(
|
||||||
|
parent.context,
|
||||||
|
R.drawable.ic_baseline_arrow_forward_24
|
||||||
|
),
|
||||||
|
null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return HeaderViewHolder(binding, viewModel, fragment = fragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
override fun onBindHeader(holder: ViewHolderState<Bundle>) {
|
||||||
return when (viewType) {
|
(holder as? HeaderViewHolder)?.bind()
|
||||||
VIEW_TYPE_HEADER -> {
|
}
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
|
||||||
val binding = if (isLayout(TV or EMULATOR)) FragmentHomeHeadTvBinding.inflate(
|
|
||||||
inflater,
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
) else FragmentHomeHeadBinding.inflate(inflater, parent, false)
|
|
||||||
|
|
||||||
if (binding is FragmentHomeHeadTvBinding && isLayout(EMULATOR)) {
|
private class HeaderViewHolder(
|
||||||
binding.homeBookmarkParentItemMoreInfo.isVisible = true
|
val binding: ViewBinding, val viewModel: HomeViewModel, fragment: Fragment,
|
||||||
|
) :
|
||||||
|
ViewHolderState<Bundle>(binding) {
|
||||||
|
|
||||||
val marginInDp = 50
|
override fun save(): Bundle =
|
||||||
val density = binding.horizontalScrollChips.context.resources.displayMetrics.density
|
Bundle().apply {
|
||||||
val marginInPixels = (marginInDp * density).toInt()
|
putParcelable(
|
||||||
|
"resumeRecyclerView",
|
||||||
val params = binding.horizontalScrollChips.layoutParams as ViewGroup.MarginLayoutParams
|
resumeRecyclerView.layoutManager?.onSaveInstanceState()
|
||||||
params.marginEnd = marginInPixels
|
|
||||||
binding.horizontalScrollChips.layoutParams = params
|
|
||||||
binding.homeWatchParentItemTitle.setCompoundDrawablesWithIntrinsicBounds(
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
ContextCompat.getDrawable(
|
|
||||||
parent.context,
|
|
||||||
R.drawable.ic_baseline_arrow_forward_24
|
|
||||||
),
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HeaderViewHolder(
|
|
||||||
binding,
|
|
||||||
viewModel,
|
|
||||||
)
|
)
|
||||||
|
putParcelable(
|
||||||
|
"bookmarkRecyclerView",
|
||||||
|
bookmarkRecyclerView.layoutManager?.onSaveInstanceState()
|
||||||
|
)
|
||||||
|
//putInt("previewViewpager", previewViewpager.currentItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
VIEW_TYPE_ITEM -> super.onCreateViewHolder(parent, viewType)
|
override fun restore(state: Bundle) {
|
||||||
else -> error("Unhandled viewType=$viewType")
|
state.getParcelable<Parcelable>("resumeRecyclerView")?.let { recycle ->
|
||||||
}
|
resumeRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return super.getItemCount() + headItems
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
if (position == 0) return 0//previewData.hashCode().toLong()
|
|
||||||
return super.getItemId(position - headItems)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
|
|
||||||
when (holder) {
|
|
||||||
is HeaderViewHolder -> {
|
|
||||||
holder.onViewDetachedFromWindow()
|
|
||||||
}
|
}
|
||||||
|
state.getParcelable<Parcelable>("bookmarkRecyclerView")?.let { recycle ->
|
||||||
else -> super.onViewDetachedFromWindow(holder)
|
bookmarkRecyclerView.layoutManager?.onRestoreInstanceState(recycle)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
|
|
||||||
when (holder) {
|
|
||||||
is HeaderViewHolder -> {
|
|
||||||
holder.onViewAttachedToWindow()
|
|
||||||
}
|
}
|
||||||
|
//state.getInt("previewViewpager").let { recycle ->
|
||||||
else -> super.onViewAttachedToWindow(holder)
|
// previewViewpager.setCurrentItem(recycle,true)
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
class HeaderViewHolder
|
val previewAdapter = HomeScrollAdapter(fragment = fragment)
|
||||||
constructor(
|
private val resumeAdapter = HomeChildItemAdapter(
|
||||||
val binding: ViewBinding,
|
fragment,
|
||||||
val viewModel: HomeViewModel,
|
id = "resumeAdapter".hashCode(),
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
|
||||||
private var previewAdapter: HomeScrollAdapter = HomeScrollAdapter()
|
|
||||||
private var resumeAdapter: HomeChildItemAdapter = HomeChildItemAdapter(
|
|
||||||
ArrayList(),
|
|
||||||
nextFocusUp = itemView.nextFocusUpId,
|
nextFocusUp = itemView.nextFocusUpId,
|
||||||
nextFocusDown = itemView.nextFocusDownId
|
nextFocusDown = itemView.nextFocusDownId
|
||||||
) { callback ->
|
) { callback ->
|
||||||
|
@ -209,8 +186,9 @@ class HomeParentItemAdapterPreview(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var bookmarkAdapter: HomeChildItemAdapter = HomeChildItemAdapter(
|
private val bookmarkAdapter = HomeChildItemAdapter(
|
||||||
ArrayList(),
|
fragment,
|
||||||
|
id = "bookmarkAdapter".hashCode(),
|
||||||
nextFocusUp = itemView.nextFocusUpId,
|
nextFocusUp = itemView.nextFocusUpId,
|
||||||
nextFocusDown = itemView.nextFocusDownId
|
nextFocusDown = itemView.nextFocusDownId
|
||||||
) { callback ->
|
) { callback ->
|
||||||
|
@ -219,7 +197,10 @@ class HomeParentItemAdapterPreview(
|
||||||
return@HomeChildItemAdapter
|
return@HomeChildItemAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
(callback.view.context?.getActivity() as? MainActivity)?.loadPopup(callback.card, load = false)
|
(callback.view.context?.getActivity() as? MainActivity)?.loadPopup(
|
||||||
|
callback.card,
|
||||||
|
load = false
|
||||||
|
)
|
||||||
/*
|
/*
|
||||||
callback.view.context?.getActivity()?.showOptionSelectStringRes(
|
callback.view.context?.getActivity()?.showOptionSelectStringRes(
|
||||||
callback.view,
|
callback.view,
|
||||||
|
@ -269,7 +250,6 @@ class HomeParentItemAdapterPreview(
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private val previewViewpager: ViewPager2 =
|
private val previewViewpager: ViewPager2 =
|
||||||
itemView.findViewById(R.id.home_preview_viewpager)
|
itemView.findViewById(R.id.home_preview_viewpager)
|
||||||
|
|
||||||
|
@ -277,38 +257,24 @@ class HomeParentItemAdapterPreview(
|
||||||
itemView.findViewById(R.id.home_preview_viewpager_text)
|
itemView.findViewById(R.id.home_preview_viewpager_text)
|
||||||
|
|
||||||
// private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview)
|
// private val previewHeader: FrameLayout = itemView.findViewById(R.id.home_preview)
|
||||||
private var resumeHolder: View = itemView.findViewById(R.id.home_watch_holder)
|
private val resumeHolder: View = itemView.findViewById(R.id.home_watch_holder)
|
||||||
private var resumeRecyclerView: RecyclerView =
|
private val resumeRecyclerView: RecyclerView =
|
||||||
itemView.findViewById(R.id.home_watch_child_recyclerview)
|
itemView.findViewById(R.id.home_watch_child_recyclerview)
|
||||||
private var bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder)
|
private val bookmarkHolder: View = itemView.findViewById(R.id.home_bookmarked_holder)
|
||||||
private var bookmarkRecyclerView: RecyclerView =
|
private val bookmarkRecyclerView: RecyclerView =
|
||||||
itemView.findViewById(R.id.home_bookmarked_child_recyclerview)
|
itemView.findViewById(R.id.home_bookmarked_child_recyclerview)
|
||||||
|
|
||||||
private var homeAccount: View? =
|
private val homeAccount: View? = itemView.findViewById(R.id.home_preview_switch_account)
|
||||||
itemView.findViewById(R.id.home_preview_switch_account)
|
private val alternativeHomeAccount: View? =
|
||||||
private var alternativeHomeAccount: View? =
|
|
||||||
itemView.findViewById(R.id.alternative_switch_account)
|
itemView.findViewById(R.id.alternative_switch_account)
|
||||||
|
|
||||||
private var topPadding: View? = itemView.findViewById(R.id.home_padding)
|
private val topPadding: View? = itemView.findViewById(R.id.home_padding)
|
||||||
|
|
||||||
private var alternativeAccountPadding: View? = itemView.findViewById(R.id.alternative_account_padding)
|
private val alternativeAccountPadding: View? =
|
||||||
|
itemView.findViewById(R.id.alternative_account_padding)
|
||||||
|
|
||||||
private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding)
|
private val homeNonePadding: View = itemView.findViewById(R.id.home_none_padding)
|
||||||
|
|
||||||
private val previewCallback: ViewPager2.OnPageChangeCallback =
|
|
||||||
object : ViewPager2.OnPageChangeCallback() {
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
previewAdapter.apply {
|
|
||||||
if (position >= itemCount - 1 && hasMoreItems) {
|
|
||||||
hasMoreItems = false // don't make two requests
|
|
||||||
viewModel.loadMoreHomeScrollResponses()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val item = previewAdapter.getItem(position) ?: return
|
|
||||||
onSelect(item, position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSelect(item: LoadResponse, position: Int) {
|
fun onSelect(item: LoadResponse, position: Int) {
|
||||||
(binding as? FragmentHomeHeadTvBinding)?.apply {
|
(binding as? FragmentHomeHeadTvBinding)?.apply {
|
||||||
homePreviewDescription.isGone =
|
homePreviewDescription.isGone =
|
||||||
|
@ -381,14 +347,14 @@ class HomeParentItemAdapterPreview(
|
||||||
|
|
||||||
homePreviewBookmark.setOnClickListener { fab ->
|
homePreviewBookmark.setOnClickListener { fab ->
|
||||||
fab.context.getActivity()?.showBottomDialog(
|
fab.context.getActivity()?.showBottomDialog(
|
||||||
WatchType.values()
|
WatchType.entries
|
||||||
.map { fab.context.getString(it.stringRes) }
|
.map { fab.context.getString(it.stringRes) }
|
||||||
.toList(),
|
.toList(),
|
||||||
DataStoreHelper.getResultWatchState(id).ordinal,
|
DataStoreHelper.getResultWatchState(id).ordinal,
|
||||||
fab.context.getString(R.string.action_add_to_bookmarks),
|
fab.context.getString(R.string.action_add_to_bookmarks),
|
||||||
showApply = false,
|
showApply = false,
|
||||||
{}) {
|
{}) {
|
||||||
val newValue = WatchType.values()[it]
|
val newValue = WatchType.entries[it]
|
||||||
|
|
||||||
ResultViewModel2().updateWatchStatus(
|
ResultViewModel2().updateWatchStatus(
|
||||||
newValue,
|
newValue,
|
||||||
|
@ -413,38 +379,22 @@ class HomeParentItemAdapterPreview(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onViewDetachedFromWindow() {
|
private val previewCallback: ViewPager2.OnPageChangeCallback =
|
||||||
previewViewpager.unregisterOnPageChangeCallback(previewCallback)
|
object : ViewPager2.OnPageChangeCallback() {
|
||||||
}
|
override fun onPageSelected(position: Int) {
|
||||||
|
previewAdapter.apply {
|
||||||
fun onViewAttachedToWindow() {
|
if (position >= itemCount - 1 && hasMoreItems) {
|
||||||
previewViewpager.registerOnPageChangeCallback(previewCallback)
|
hasMoreItems = false // don't make two requests
|
||||||
|
viewModel.loadMoreHomeScrollResponses()
|
||||||
binding.root.findViewTreeLifecycleOwner()?.apply {
|
|
||||||
observe(viewModel.preview) {
|
|
||||||
updatePreview(it)
|
|
||||||
}
|
|
||||||
if (binding is FragmentHomeHeadTvBinding) {
|
|
||||||
observe(viewModel.apiName) { name ->
|
|
||||||
binding.homePreviewChangeApi.text = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
observe(viewModel.resumeWatching) {
|
|
||||||
updateResume(it)
|
|
||||||
}
|
|
||||||
observe(viewModel.bookmarks) {
|
|
||||||
updateBookmarks(it)
|
|
||||||
}
|
|
||||||
observe(viewModel.availableWatchStatusTypes) { (checked, visible) ->
|
|
||||||
for ((chip, watch) in toggleList) {
|
|
||||||
chip.apply {
|
|
||||||
isVisible = visible.contains(watch)
|
|
||||||
isChecked = checked.contains(watch)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toggleListHolder?.isGone = visible.isEmpty()
|
val item = previewAdapter.getItem(position) ?: return
|
||||||
|
onSelect(item, position)
|
||||||
}
|
}
|
||||||
} ?: debugException { "Expected findViewTreeLifecycleOwner" }
|
}
|
||||||
|
|
||||||
|
override fun onViewDetachedFromWindow() {
|
||||||
|
previewViewpager.unregisterOnPageChangeCallback(previewCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val toggleList = listOf<Pair<Chip, WatchType>>(
|
private val toggleList = listOf<Pair<Chip, WatchType>>(
|
||||||
|
@ -457,6 +407,8 @@ class HomeParentItemAdapterPreview(
|
||||||
|
|
||||||
private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder)
|
private val toggleListHolder: ChipGroup? = itemView.findViewById(R.id.home_type_holder)
|
||||||
|
|
||||||
|
fun bind() = Unit
|
||||||
|
|
||||||
init {
|
init {
|
||||||
previewViewpager.setPageTransformer(HomeScrollTransformer())
|
previewViewpager.setPageTransformer(HomeScrollTransformer())
|
||||||
|
|
||||||
|
@ -563,7 +515,8 @@ class HomeParentItemAdapterPreview(
|
||||||
|
|
||||||
when (preview) {
|
when (preview) {
|
||||||
is Resource.Success -> {
|
is Resource.Success -> {
|
||||||
if (!previewAdapter.setItems(
|
previewAdapter.submitList(preview.value.second)
|
||||||
|
/*if (!.setItems(
|
||||||
preview.value.second,
|
preview.value.second,
|
||||||
preview.value.first
|
preview.value.first
|
||||||
)
|
)
|
||||||
|
@ -575,15 +528,16 @@ class HomeParentItemAdapterPreview(
|
||||||
previewViewpager.fakeDragBy(1f)
|
previewViewpager.fakeDragBy(1f)
|
||||||
previewViewpager.endFakeDrag()
|
previewViewpager.endFakeDrag()
|
||||||
previewCallback.onPageSelected(0)
|
previewCallback.onPageSelected(0)
|
||||||
previewViewpager.isVisible = true
|
|
||||||
previewViewpagerText.isVisible = true
|
|
||||||
alternativeAccountPadding?.isVisible = false
|
|
||||||
//previewHeader.isVisible = true
|
//previewHeader.isVisible = true
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
previewViewpager.isVisible = true
|
||||||
|
previewViewpagerText.isVisible = true
|
||||||
|
alternativeAccountPadding?.isVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
previewAdapter.setItems(listOf(), false)
|
previewAdapter.submitList(listOf())
|
||||||
previewViewpager.setCurrentItem(0, false)
|
previewViewpager.setCurrentItem(0, false)
|
||||||
previewViewpager.isVisible = false
|
previewViewpager.isVisible = false
|
||||||
previewViewpagerText.isVisible = false
|
previewViewpagerText.isVisible = false
|
||||||
|
@ -595,7 +549,7 @@ class HomeParentItemAdapterPreview(
|
||||||
|
|
||||||
private fun updateResume(resumeWatching: List<SearchResponse>) {
|
private fun updateResume(resumeWatching: List<SearchResponse>) {
|
||||||
resumeHolder.isVisible = resumeWatching.isNotEmpty()
|
resumeHolder.isVisible = resumeWatching.isNotEmpty()
|
||||||
resumeAdapter.updateList(resumeWatching)
|
resumeAdapter.submitList(resumeWatching)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
binding is FragmentHomeHeadBinding ||
|
binding is FragmentHomeHeadBinding ||
|
||||||
|
@ -625,7 +579,7 @@ class HomeParentItemAdapterPreview(
|
||||||
private fun updateBookmarks(data: Pair<Boolean, List<SearchResponse>>) {
|
private fun updateBookmarks(data: Pair<Boolean, List<SearchResponse>>) {
|
||||||
val (visible, list) = data
|
val (visible, list) = data
|
||||||
bookmarkHolder.isVisible = visible
|
bookmarkHolder.isVisible = visible
|
||||||
bookmarkAdapter.updateList(list)
|
bookmarkAdapter.submitList(list)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
binding is FragmentHomeHeadBinding ||
|
binding is FragmentHomeHeadBinding ||
|
||||||
|
@ -655,5 +609,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" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,43 +4,23 @@ import android.content.res.Configuration
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import com.lagradost.cloudstream3.LoadResponse
|
import com.lagradost.cloudstream3.LoadResponse
|
||||||
import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding
|
import com.lagradost.cloudstream3.databinding.HomeScrollViewBinding
|
||||||
import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding
|
import com.lagradost.cloudstream3.databinding.HomeScrollViewTvBinding
|
||||||
|
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||||
|
import com.lagradost.cloudstream3.ui.ViewHolderState
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
import com.lagradost.cloudstream3.utils.UIHelper.setImage
|
||||||
|
|
||||||
class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
class HomeScrollAdapter(
|
||||||
private var items: MutableList<LoadResponse> = mutableListOf()
|
fragment: Fragment
|
||||||
|
) : BaseAdapter<LoadResponse, Any>(fragment, "HomeScrollAdapter".hashCode()) {
|
||||||
var hasMoreItems: Boolean = false
|
var hasMoreItems: Boolean = false
|
||||||
|
|
||||||
fun getItem(position: Int): LoadResponse? {
|
override fun onCreateContent(parent: ViewGroup): ViewHolderState<Any> {
|
||||||
return items.getOrNull(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setItems(newItems: List<LoadResponse>, hasNext: Boolean): Boolean {
|
|
||||||
val isSame = newItems.firstOrNull()?.url == items.firstOrNull()?.url
|
|
||||||
hasMoreItems = hasNext
|
|
||||||
|
|
||||||
val diffResult = DiffUtil.calculateDiff(
|
|
||||||
HomeScrollDiffCallback(this.items, newItems)
|
|
||||||
)
|
|
||||||
|
|
||||||
items.clear()
|
|
||||||
items.addAll(newItems)
|
|
||||||
|
|
||||||
|
|
||||||
diffResult.dispatchUpdatesTo(this)
|
|
||||||
|
|
||||||
return isSame
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
val inflater = LayoutInflater.from(parent.context)
|
val inflater = LayoutInflater.from(parent.context)
|
||||||
val binding = if (isLayout(TV or EMULATOR)) {
|
val binding = if (isLayout(TV or EMULATOR)) {
|
||||||
HomeScrollViewTvBinding.inflate(inflater, parent, false)
|
HomeScrollViewTvBinding.inflate(inflater, parent, false)
|
||||||
|
@ -48,70 +28,37 @@ class HomeScrollAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||||
HomeScrollViewBinding.inflate(inflater, parent, false)
|
HomeScrollViewBinding.inflate(inflater, parent, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return CardViewHolder(
|
return ViewHolderState(binding)
|
||||||
binding,
|
|
||||||
//forceHorizontalPosters
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindContent(
|
||||||
when (holder) {
|
holder: ViewHolderState<Any>,
|
||||||
is CardViewHolder -> {
|
item: LoadResponse,
|
||||||
holder.bind(items[position])
|
position: Int,
|
||||||
|
) {
|
||||||
|
val binding = holder.view
|
||||||
|
val itemView = holder.itemView
|
||||||
|
val isHorizontal =
|
||||||
|
binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
|
||||||
|
val posterUrl =
|
||||||
|
if (isHorizontal) item.backgroundPosterUrl ?: item.posterUrl else item.posterUrl
|
||||||
|
?: item.backgroundPosterUrl
|
||||||
|
|
||||||
|
when (binding) {
|
||||||
|
is HomeScrollViewBinding -> {
|
||||||
|
binding.homeScrollPreview.setImage(posterUrl)
|
||||||
|
binding.homeScrollPreviewTags.apply {
|
||||||
|
text = item.tags?.joinToString(" • ") ?: ""
|
||||||
|
isGone = item.tags.isNullOrEmpty()
|
||||||
|
maxLines = 2
|
||||||
|
}
|
||||||
|
binding.homeScrollPreviewTitle.text = item.name
|
||||||
|
}
|
||||||
|
|
||||||
|
is HomeScrollViewTvBinding -> {
|
||||||
|
binding.homeScrollPreview.setImage(posterUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CardViewHolder
|
|
||||||
constructor(
|
|
||||||
val binding: ViewBinding,
|
|
||||||
//private val forceHorizontalPosters: Boolean? = null
|
|
||||||
) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun bind(card: LoadResponse) {
|
|
||||||
val isHorizontal =
|
|
||||||
binding is HomeScrollViewTvBinding || itemView.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
||||||
|
|
||||||
val posterUrl =
|
|
||||||
if (isHorizontal) card.backgroundPosterUrl ?: card.posterUrl else card.posterUrl
|
|
||||||
?: card.backgroundPosterUrl
|
|
||||||
|
|
||||||
when (binding) {
|
|
||||||
is HomeScrollViewBinding -> {
|
|
||||||
binding.homeScrollPreview.setImage(posterUrl)
|
|
||||||
binding.homeScrollPreviewTags.apply {
|
|
||||||
text = card.tags?.joinToString(" • ") ?: ""
|
|
||||||
isGone = card.tags.isNullOrEmpty()
|
|
||||||
maxLines = 2
|
|
||||||
}
|
|
||||||
binding.homeScrollPreviewTitle.text = card.name
|
|
||||||
}
|
|
||||||
|
|
||||||
is HomeScrollViewTvBinding -> {
|
|
||||||
binding.homeScrollPreview.setImage(posterUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class HomeScrollDiffCallback(
|
|
||||||
private val oldList: List<LoadResponse>,
|
|
||||||
private val newList: List<LoadResponse>
|
|
||||||
) :
|
|
||||||
DiffUtil.Callback() {
|
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
|
||||||
oldList[oldItemPosition].url == newList[newItemPosition].url
|
|
||||||
|
|
||||||
override fun getOldListSize() = oldList.size
|
|
||||||
|
|
||||||
override fun getNewListSize() = newList.size
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
|
|
||||||
oldList[oldItemPosition] == newList[newItemPosition]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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.setText
|
||||||
import com.lagradost.cloudstream3.ui.result.txt
|
import com.lagradost.cloudstream3.ui.result.txt
|
||||||
import com.lagradost.cloudstream3.ui.settings.Globals
|
import com.lagradost.cloudstream3.ui.settings.Globals
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.EMULATOR
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.TV
|
||||||
|
import com.lagradost.cloudstream3.ui.settings.Globals.isLayout
|
||||||
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
import com.lagradost.cloudstream3.ui.settings.SettingsFragment
|
||||||
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
import com.lagradost.cloudstream3.utils.AppUtils.isUsingMobileData
|
||||||
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
import com.lagradost.cloudstream3.utils.DataStoreHelper
|
||||||
|
@ -78,7 +81,6 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
private var isVerticalOrientation: Boolean = false
|
private var isVerticalOrientation: Boolean = false
|
||||||
protected open var lockRotation = true
|
protected open var lockRotation = true
|
||||||
protected open var isFullScreenPlayer = true
|
protected open var isFullScreenPlayer = true
|
||||||
protected open var isTv = false
|
|
||||||
protected var playerBinding: PlayerCustomLayoutBinding? = null
|
protected var playerBinding: PlayerCustomLayoutBinding? = null
|
||||||
|
|
||||||
private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false)
|
private var durationMode : Boolean by UserPreferenceDelegate("duration_mode", false)
|
||||||
|
@ -1205,7 +1207,7 @@ open class FullScreenPlayer : AbstractPlayerFragment() {
|
||||||
|
|
||||||
// netflix capture back and hide ~monke
|
// netflix capture back and hide ~monke
|
||||||
KeyEvent.KEYCODE_BACK -> {
|
KeyEvent.KEYCODE_BACK -> {
|
||||||
if (isShowing && isTv) {
|
if (isShowing && isLayout(TV or EMULATOR)) {
|
||||||
onClickChange()
|
onClickChange()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,7 +174,7 @@ class QuickSearchFragment : Fragment() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding?.quickSearchMasterRecycler?.adapter =
|
binding?.quickSearchMasterRecycler?.adapter =
|
||||||
ParentItemAdapter(mutableListOf(), { callback ->
|
ParentItemAdapter(fragment = this, id = "quickSearchMasterRecycler".hashCode(), { callback ->
|
||||||
SearchHelper.handleSearchClickCallback(callback)
|
SearchHelper.handleSearchClickCallback(callback)
|
||||||
//when (callback.action) {
|
//when (callback.action) {
|
||||||
//SEARCH_ACTION_LOAD -> {
|
//SEARCH_ACTION_LOAD -> {
|
||||||
|
|
|
@ -46,6 +46,7 @@ import com.lagradost.cloudstream3.mvvm.Resource
|
||||||
import com.lagradost.cloudstream3.mvvm.logError
|
import com.lagradost.cloudstream3.mvvm.logError
|
||||||
import com.lagradost.cloudstream3.mvvm.observe
|
import com.lagradost.cloudstream3.mvvm.observe
|
||||||
import com.lagradost.cloudstream3.ui.APIRepository
|
import com.lagradost.cloudstream3.ui.APIRepository
|
||||||
|
import com.lagradost.cloudstream3.ui.BaseAdapter
|
||||||
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
import com.lagradost.cloudstream3.ui.home.HomeFragment
|
||||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
|
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.bindChips
|
||||||
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan
|
import com.lagradost.cloudstream3.ui.home.HomeFragment.Companion.currentSpan
|
||||||
|
@ -161,7 +162,8 @@ class SearchFragment : Fragment() {
|
||||||
**/
|
**/
|
||||||
fun search(query: String?) {
|
fun search(query: String?) {
|
||||||
if (query == null) return
|
if (query == null) return
|
||||||
|
// don't resume state from prev search
|
||||||
|
(binding?.searchMasterRecycler?.adapter as? BaseAdapter<*,*>)?.clear()
|
||||||
context?.let { ctx ->
|
context?.let { ctx ->
|
||||||
val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }
|
val default = enumValues<TvType>().sorted().filter { it != TvType.NSFW }
|
||||||
.map { it.ordinal.toString() }.toSet()
|
.map { it.ordinal.toString() }.toSet()
|
||||||
|
@ -506,8 +508,8 @@ class SearchFragment : Fragment() {
|
||||||
}*/
|
}*/
|
||||||
//main_search.onActionViewExpanded()*/
|
//main_search.onActionViewExpanded()*/
|
||||||
|
|
||||||
val masterAdapter: RecyclerView.Adapter<RecyclerView.ViewHolder> =
|
val masterAdapter =
|
||||||
ParentItemAdapter(mutableListOf(), { callback ->
|
ParentItemAdapter(fragment = this, id = "masterAdapter".hashCode(), { callback ->
|
||||||
SearchHelper.handleSearchClickCallback(callback)
|
SearchHelper.handleSearchClickCallback(callback)
|
||||||
}, { item ->
|
}, { item ->
|
||||||
bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = {
|
bottomSheetDialog = activity?.loadHomepageList(item, dismissCallback = {
|
||||||
|
|
Loading…
Reference in a new issue