package audio.funkwhale.ffa.fragments import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import audio.funkwhale.ffa.repositories.HttpUpstream import audio.funkwhale.ffa.repositories.Repository import audio.funkwhale.ffa.utils.Event import audio.funkwhale.ffa.utils.EventBus import audio.funkwhale.ffa.utils.FFACache import audio.funkwhale.ffa.utils.untilNetwork import com.google.gson.Gson import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext abstract class FFAAdapter : RecyclerView.Adapter() { var data: MutableList = mutableListOf() private var unfilteredData: MutableList = mutableListOf() fun getUnfilteredData(): MutableList { return unfilteredData } fun setUnfilteredData(data: MutableList) { unfilteredData = data applyFilter() } open fun applyFilter() { data.clear() data.addAll(unfilteredData) } init { super.setHasStableIds(true) } abstract override fun getItemId(position: Int): Long } abstract class FFAFragment> : Fragment() { companion object { const val OFFSCREEN_PAGES = 20 } abstract val recycler: RecyclerView open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context) open val alwaysRefresh = true lateinit var repository: Repository lateinit var adapter: A lateinit var swiper: SwipeRefreshLayout private var moreLoading = false private var listener: Job? = null fun repository() = repository as T override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) recycler.layoutManager = layoutManager (recycler.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false recycler.adapter = adapter (repository.upstream as? HttpUpstream<*, *>)?.let { upstream -> if (upstream.behavior == HttpUpstream.Behavior.Progressive) { recycler.setOnScrollChangeListener { _, _, _, _, _ -> val offset = recycler.computeVerticalScrollOffset() if (!moreLoading && offset > 0 && needsMoreOffscreenPages()) { moreLoading = true fetch(Repository.Origin.Network.origin, adapter.data.size) } } } } if (listener == null) { listener = lifecycleScope.launch(IO) { EventBus.get().collect { event -> if (event is Event.ListingsChanged) { withContext(Main) { swiper.isRefreshing = true fetch(Repository.Origin.Network.origin) } } } } } fetch(Repository.Origin.Cache.origin) if (alwaysRefresh && adapter.data.isEmpty()) { fetch(Repository.Origin.Network.origin) } } override fun onResume() { super.onResume() swiper.setOnRefreshListener { fetch(Repository.Origin.Network.origin) } } fun update() { swiper.isRefreshing = true fetch(Repository.Origin.Network.origin) } open fun onDataFetched(data: List) {} private fun fetch(upstreams: Int = Repository.Origin.Network.origin, size: Int = 0) { var first = size == 0 if (!moreLoading && upstreams == Repository.Origin.Network.origin) { lifecycleScope.launch(Main) { swiper.isRefreshing = true } } moreLoading = true repository.fetch(upstreams, size) .untilNetwork(lifecycleScope, IO) { data, isCache, _, hasMore -> if (isCache && data.isEmpty()) { moreLoading = false return@untilNetwork fetch(Repository.Origin.Network.origin) } lifecycleScope.launch(Main) { if (isCache) { moreLoading = false adapter.setUnfilteredData(data.toMutableList()) adapter.notifyDataSetChanged() return@launch } if (first) { adapter.getUnfilteredData().clear() } onDataFetched(data) adapter.getUnfilteredData().addAll(data) adapter.applyFilter() withContext(IO) { try { repository.cacheId?.let { cacheId -> FFACache.set( context, cacheId, Gson().toJson(repository.cache(adapter.getUnfilteredData())).toString() ) } } catch (e: ConcurrentModificationException) { } } if (hasMore) { (repository.upstream as? HttpUpstream<*, *>)?.let { upstream -> if (!isCache && upstream.behavior == HttpUpstream.Behavior.Progressive) { if (first || needsMoreOffscreenPages()) { fetch(Repository.Origin.Network.origin, adapter.getUnfilteredData().size) } else { moreLoading = false } } else { moreLoading = false } } } (repository.upstream as? HttpUpstream<*, *>)?.let { upstream -> when (upstream.behavior) { HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper.isRefreshing = false HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper.isRefreshing = false HttpUpstream.Behavior.Single -> if (!hasMore) swiper.isRefreshing = false } } when (first) { true -> { adapter.notifyDataSetChanged() first = false } false -> adapter.notifyItemRangeInserted(adapter.data.size, data.size) } } } } private fun needsMoreOffscreenPages(): Boolean { view?.let { val offset = recycler.computeVerticalScrollOffset() val left = recycler.computeVerticalScrollRange() - recycler.height - offset return left < (recycler.height * OFFSCREEN_PAGES) } return false } }