From 98d92d3fe294f44ceafeff48b030c9dd0a8cf55c Mon Sep 17 00:00:00 2001 From: Koen <koen@pop-os.localdomain> Date: Wed, 6 Dec 2023 16:32:17 +0100 Subject: [PATCH] Updated HistoryView to use pager. --- .../channel/tab/ChannelContentsFragment.kt | 4 - .../fragment/mainactivity/main/FeedView.kt | 4 - .../mainactivity/main/HistoryFragment.kt | 310 ++++++++++++++---- .../views/adapters/HistoryListAdapter.kt | 119 ------- .../views/adapters/HistoryListViewHolder.kt | 30 +- 5 files changed, 267 insertions(+), 200 deletions(-) delete mode 100644 app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListAdapter.kt diff --git a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt index c1b0127e..0bf75149 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/channel/tab/ChannelContentsFragment.kt @@ -101,10 +101,6 @@ class ChannelContentsFragment : Fragment(), IChannelTabFragment { return@TaskHandler it.getResults(); }).success { setLoading(false); - if (it.isEmpty()) { - return@success; - } - val posBefore = _results.size; val toAdd = it.filter { it is IPlatformVideo }.map { it as IPlatformVideo } _results.addAll(toAdd); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt index 595f880f..f5294ff7 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/FeedView.kt @@ -132,10 +132,6 @@ abstract class FeedView<TFragment, TResult, TConverted, TPager, TViewHolder> : L }).success { setLoading(false); - if (it.isEmpty()) { - return@success; - } - val posBefore = recyclerData.results.size; val filteredResults = filterResults(it); recyclerData.results.addAll(filteredResults); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt index 8e65eb1e..4cd2d885 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/HistoryFragment.kt @@ -1,5 +1,6 @@ package com.futo.platformplayer.fragment.mainactivity.main +import android.annotation.SuppressLint import android.content.Context import android.os.Bundle import android.view.LayoutInflater @@ -8,88 +9,279 @@ import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.ImageButton +import android.widget.LinearLayout import androidx.core.widget.addTextChangedListener +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.futo.platformplayer.* +import com.futo.platformplayer.api.media.models.contents.IPlatformContent +import com.futo.platformplayer.api.media.structures.IAsyncPager +import com.futo.platformplayer.api.media.structures.IPager +import com.futo.platformplayer.constructs.TaskHandler +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.models.HistoryVideo import com.futo.platformplayer.states.StateHistory import com.futo.platformplayer.states.StatePlayer -import com.futo.platformplayer.states.StatePlaylists import com.futo.platformplayer.views.others.TagsView -import com.futo.platformplayer.views.adapters.HistoryListAdapter +import com.futo.platformplayer.views.adapters.HistoryListViewHolder +import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader class HistoryFragment : MainFragment() { override val isMainView : Boolean = true; override val isTab: Boolean = true; override val hasBottomBar: Boolean get() = true; - private var _adapter: HistoryListAdapter? = null; + private var _view: HistoryView? = null; override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - val view = inflater.inflate(R.layout.fragment_history, container, false); - - val inputMethodManager = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager; - - val recyclerHistory = view.findViewById<RecyclerView>(R.id.recycler_history); - val clearSearch = view.findViewById<ImageButton>(R.id.button_clear_search); - val editSearch = view.findViewById<EditText>(R.id.edit_search); - var tagsView = view.findViewById<TagsView>(R.id.tags_text); - tagsView.setPairs(listOf( - Pair(getString(R.string.last_hour), 60L), - Pair(getString(R.string.last_24_hours), 24L * 60L), - Pair(getString(R.string.last_week), 7L * 24L * 60L), - Pair(getString(R.string.last_30_days), 30L * 24L * 60L), - Pair(getString(R.string.last_year), 365L * 30L * 24L * 60L), - Pair(getString(R.string.all_time), -1L))); - - val adapter = HistoryListAdapter(); - adapter.onClick.subscribe { v -> - val diff = v.video.duration - v.position; - val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video }; - StatePlayer.instance.clearQueue(); - navigate<VideoDetailFragment>(vid).maximizeVideoDetail(); - editSearch.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(editSearch.windowToken, 0); - }; - _adapter = adapter; - - recyclerHistory.adapter = adapter; - recyclerHistory.isSaveEnabled = false; - recyclerHistory.layoutManager = LinearLayoutManager(context); - - tagsView.onClick.subscribe { timeMinutesToErase -> - UIDialogs.showConfirmationDialog(requireContext(), getString(R.string.are_you_sure_delete_historical), { - StateHistory.instance.removeHistoryRange(timeMinutesToErase.second as Long); - UIDialogs.toast(view.context, timeMinutesToErase.first + " " + getString(R.string.removed)); - adapter.updateFilteredVideos(); - adapter.notifyDataSetChanged(); - }); - }; - - clearSearch.setOnClickListener { - editSearch.text.clear(); - clearSearch.visibility = View.GONE; - adapter.setQuery(""); - editSearch.clearFocus(); - inputMethodManager.hideSoftInputFromWindow(editSearch.windowToken, 0); - }; - - editSearch.addTextChangedListener { _ -> - val text = editSearch.text; - clearSearch.visibility = if (text.isEmpty()) { View.GONE } else { View.VISIBLE }; - adapter.setQuery(text.toString()); - }; - + val view = HistoryView(this, inflater); + _view = view; return view; } override fun onDestroyMainView() { super.onDestroyMainView(); - _adapter?.cleanup(); - _adapter = null; + _view = null; + } + + override fun onShownWithView(parameter: Any?, isBack: Boolean) { + super.onShownWithView(parameter, isBack) + _view?.setPager(StateHistory.instance.getHistoryPager()); + } + + @SuppressLint("ViewConstructor") + class HistoryView : LinearLayout { + private val _fragment: HistoryFragment; + private val _adapter: InsertedViewAdapterWithLoader<HistoryListViewHolder>; + private val _recyclerHistory: RecyclerView; + private val _clearSearch: ImageButton; + private val _editSearch: EditText; + private val _tagsView: TagsView; + private val _llmHistory: LinearLayoutManager; + private val _pagerLock = Object(); + private var _nextPageHandler: TaskHandler<IPager<HistoryVideo>, List<HistoryVideo>>; + private var _pager: IPager<HistoryVideo>? = null; + private val _results = arrayListOf<HistoryVideo>(); + private var _loading = false; + + private var _automaticNextPageCounter = 0; + + constructor(fragment: HistoryFragment, inflater: LayoutInflater) : super(inflater.context) { + _fragment = fragment; + inflater.inflate(R.layout.fragment_history, this); + + _recyclerHistory = findViewById(R.id.recycler_history); + _clearSearch = findViewById(R.id.button_clear_search); + _editSearch = findViewById(R.id.edit_search); + _tagsView = findViewById(R.id.tags_text); + _tagsView.setPairs(listOf( + Pair(context.getString(R.string.last_hour), 60L), + Pair(context.getString(R.string.last_24_hours), 24L * 60L), + Pair(context.getString(R.string.last_week), 7L * 24L * 60L), + Pair(context.getString(R.string.last_30_days), 30L * 24L * 60L), + Pair(context.getString(R.string.last_year), 365L * 30L * 24L * 60L), + Pair(context.getString(R.string.all_time), -1L) + )); + + _adapter = InsertedViewAdapterWithLoader(context, arrayListOf(), arrayListOf(), + { _results.size }, + { view, _ -> + val holder = HistoryListViewHolder(view); + holder.onRemove.subscribe(::onHistoryVideoRemove); + holder.onClick.subscribe(::onHistoryVideoClick); + return@InsertedViewAdapterWithLoader holder; + }, + { viewHolder, position -> + var watchTime: String? = null; + if (position == 0) { + watchTime = _results[position].date.toHumanNowDiffStringMinDay(); + } else { + val previousWatchTime = _results[position - 1].date.toHumanNowDiffStringMinDay(); + val currentWatchTime = _results[position].date.toHumanNowDiffStringMinDay(); + if (previousWatchTime != currentWatchTime) { + watchTime = currentWatchTime; + } + } + + viewHolder.bind(_results[position], watchTime); + } + ); + + _recyclerHistory.adapter = _adapter; + _recyclerHistory.isSaveEnabled = false; + _llmHistory = LinearLayoutManager(context); + _recyclerHistory.layoutManager = _llmHistory; + + _tagsView.onClick.subscribe { timeMinutesToErase -> + UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_delete_historical), { + StateHistory.instance.removeHistoryRange(timeMinutesToErase.second as Long); + UIDialogs.toast(context, timeMinutesToErase.first + " " + context.getString(R.string.removed)); + updatePager(); + }); + }; + + _clearSearch.setOnClickListener { + val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager; + _editSearch.text.clear(); + _clearSearch.visibility = View.GONE; + setPager(StateHistory.instance.getHistoryPager()); + _editSearch.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0); + }; + + _editSearch.addTextChangedListener { _ -> + val text = _editSearch.text; + _clearSearch.visibility = if (text.isEmpty()) { View.GONE } else { View.VISIBLE }; + updatePager(); + }; + + _recyclerHistory.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy); + + val visibleItemCount = _recyclerHistory.childCount; + val firstVisibleItem = _llmHistory.findFirstVisibleItemPosition(); + + Logger.i(TAG, "onScrolled _loading = $_loading, firstVisibleItem = $firstVisibleItem, visibleItemCount = $visibleItemCount, _results.size = ${_results.size}") + + val visibleThreshold = 15; + if (!_loading && firstVisibleItem + visibleItemCount + visibleThreshold >= _results.size && firstVisibleItem > 0) { + loadNextPage(); + } + } + }); + + _nextPageHandler = TaskHandler<IPager<HistoryVideo>, List<HistoryVideo>>({fragment.lifecycleScope}, { + if (it is IAsyncPager<*>) + it.nextPageAsync(); + else + it.nextPage(); + + return@TaskHandler it.getResults(); + }).success { + setLoading(false); + + val posBefore = _results.size; + _results.addAll(it); + _adapter.notifyItemRangeInserted(_adapter.childToParentPosition(posBefore), it.size); + ensureEnoughContentVisible(it) + }.exception<Throwable> { + Logger.w(TAG, "Failed to load next page.", it); + UIDialogs.showGeneralRetryErrorDialog(context, context.getString(R.string.failed_to_load_next_page), it, { + loadNextPage(); + }); + }; + } + + private fun updatePager() { + val query = _editSearch.text.toString(); + if (_editSearch.text.isNotEmpty()) { + setPager(StateHistory.instance.getHistorySearchPager(query)); + } else { + setPager(StateHistory.instance.getHistoryPager()); + } + } + + fun setPager(pager: IPager<HistoryVideo>) { + synchronized(_pagerLock) { + loadPagerInternal(pager); + } + } + + private fun onHistoryVideoRemove(v: HistoryVideo) { + val index = _results.indexOf(v); + if (index == -1) { + return; + } + + StateHistory.instance.removeHistory(v.video.url); + _results.removeAt(index); + _adapter.notifyItemRemoved(index); + } + + private fun onHistoryVideoClick(v: HistoryVideo) { + val index = _results.indexOf(v); + if (index == -1) { + return; + } + + _results.removeAt(index); + _results.add(0, v); + + _adapter.notifyItemMoved(index, 0); + _adapter.notifyItemRangeChanged(0, 2); + + val inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager; + val diff = v.video.duration - v.position; + val vid: Any = if (diff > 5) { v.video.withTimestamp(v.position) } else { v.video }; + StatePlayer.instance.clearQueue(); + _fragment.navigate<VideoDetailFragment>(vid).maximizeVideoDetail(); + _editSearch.clearFocus(); + inputMethodManager.hideSoftInputFromWindow(_editSearch.windowToken, 0); + } + + private fun loadNextPage() { + synchronized(_pagerLock) { + val pager: IPager<HistoryVideo> = _pager ?: return; + val hasMorePages = pager.hasMorePages(); + Logger.i(TAG, "loadNextPage() hasMorePages=$hasMorePages"); + + if (pager.hasMorePages()) { + setLoading(true); + _nextPageHandler.run(pager); + } + } + } + + private fun setLoading(loading: Boolean) { + Logger.v(TAG, "setLoading loading=${loading}"); + _loading = loading; + _adapter.setLoading(loading); + } + + private fun loadPagerInternal(pager: IPager<HistoryVideo>) { + Logger.i(TAG, "Setting new internal pager on feed"); + + _results.clear(); + val toAdd = pager.getResults(); + _results.addAll(toAdd); + _adapter.notifyDataSetChanged(); + ensureEnoughContentVisible(toAdd) + _pager = pager; + } + + private fun ensureEnoughContentVisible(results: List<HistoryVideo>) { + val canScroll = if (_results.isEmpty()) false else { + val layoutManager = _llmHistory + val firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition() + + if (firstVisibleItemPosition != RecyclerView.NO_POSITION) { + val firstVisibleView = layoutManager.findViewByPosition(firstVisibleItemPosition) + val itemHeight = firstVisibleView?.height ?: 0 + val occupiedSpace = _results.size * itemHeight + val recyclerViewHeight = _recyclerHistory.height + Logger.i(TAG, "ensureEnoughContentVisible loadNextPage occupiedSpace=$occupiedSpace recyclerViewHeight=$recyclerViewHeight") + occupiedSpace >= recyclerViewHeight + } else { + false + } + + } + + Logger.i(TAG, "ensureEnoughContentVisible loadNextPage canScroll=$canScroll _automaticNextPageCounter=$_automaticNextPageCounter") + if (!canScroll || results.isEmpty()) { + _automaticNextPageCounter++ + if(_automaticNextPageCounter <= 4) + loadNextPage() + } else { + _automaticNextPageCounter = 0; + } + } } companion object { fun newInstance() = HistoryFragment().apply {} + private const val TAG = "HistoryFragment" } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListAdapter.kt deleted file mode 100644 index 33335ade..00000000 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListAdapter.kt +++ /dev/null @@ -1,119 +0,0 @@ -package com.futo.platformplayer.views.adapters - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.futo.platformplayer.* -import com.futo.platformplayer.api.media.structures.IPager -import com.futo.platformplayer.constructs.Event1 -import com.futo.platformplayer.models.HistoryVideo -import com.futo.platformplayer.states.StateApp -import com.futo.platformplayer.states.StateHistory -import com.futo.platformplayer.states.StatePlaylists -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch - -class HistoryListAdapter : RecyclerView.Adapter<HistoryListViewHolder> { - private lateinit var _filteredVideos: MutableList<HistoryVideo>; - - val onClick = Event1<HistoryVideo>(); - private var _query: String = ""; - - constructor() : super() { - updateFilteredVideos(); - - StateHistory.instance.onHistoricVideoChanged.subscribe(this) { video, position -> - StateApp.instance.scope.launch(Dispatchers.Main) { - val index = _filteredVideos.indexOfFirst { v -> v.video.url == video.url }; - if (index == -1) { - return@launch; - } - - _filteredVideos[index].position = position; - if (index < _filteredVideos.size - 2) { - notifyItemRangeChanged(index, 2); - } else { - notifyItemChanged(index); - } - } - }; - } - - fun setQuery(query: String) { - _query = query; - updateFilteredVideos(); - } - - fun updateFilteredVideos() { - val videos = StateHistory.instance.getHistory(); - //filtered val pager = StateHistory.instance.getHistorySearchPager("querrryyyyy"); TODO: Implement pager - - if (_query.isBlank()) { - _filteredVideos = videos.toMutableList(); - } else { - _filteredVideos = videos.filter { v -> v.video.name.lowercase().contains(_query); }.toMutableList(); - } - - notifyDataSetChanged(); - } - - fun cleanup() { - StateHistory.instance.onHistoricVideoChanged.remove(this); - } - - override fun getItemCount() = _filteredVideos.size; - - override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): HistoryListViewHolder { - val view = LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false); - val holder = HistoryListViewHolder(view); - - holder.onRemove.subscribe { v -> - val videos = _filteredVideos; - val index = videos.indexOf(v); - if (index == -1) { - return@subscribe; - } - - StateHistory.instance.removeHistory(v.video.url); - _filteredVideos.removeAt(index); - notifyItemRemoved(index); - }; - holder.onClick.subscribe { v -> - val videos = _filteredVideos; - val index = videos.indexOf(v); - if (index == -1) { - return@subscribe; - } - - _filteredVideos.removeAt(index); - _filteredVideos.add(0, v); - - notifyItemMoved(index, 0); - notifyItemRangeChanged(0, 2); - onClick.emit(v); - }; - - return holder; - } - - override fun onBindViewHolder(viewHolder: HistoryListViewHolder, position: Int) { - val videos = _filteredVideos; - var watchTime: String? = null; - if (position == 0) { - watchTime = videos[position].date.toHumanNowDiffStringMinDay(); - } else { - val previousWatchTime = videos[position - 1].date.toHumanNowDiffStringMinDay(); - val currentWatchTime = videos[position].date.toHumanNowDiffStringMinDay(); - if (previousWatchTime != currentWatchTime) { - watchTime = currentWatchTime; - } - } - - viewHolder.bind(videos[position], watchTime); - } - - companion object { - val TAG = "HistoryListAdapter"; - } -} diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListViewHolder.kt index 7194c1e0..2f018c9d 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/HistoryListViewHolder.kt @@ -1,6 +1,8 @@ package com.futo.platformplayer.views.adapters +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.widget.ImageButton import android.widget.ImageView import android.widget.LinearLayout @@ -35,26 +37,26 @@ class HistoryListViewHolder : ViewHolder { val onClick = Event1<HistoryVideo>(); val onRemove = Event1<HistoryVideo>(); - constructor(view: View) : super(view) { - _root = view.findViewById(R.id.root); - _imageThumbnail = view.findViewById(R.id.image_video_thumbnail); - _imageThumbnail?.clipToOutline = true; - _textName = view.findViewById(R.id.text_video_name); - _textAuthor = view.findViewById(R.id.text_author); - _textMetadata = view.findViewById(R.id.text_video_metadata); - _textVideoDuration = view.findViewById(R.id.thumbnail_duration); - _containerDuration = view.findViewById(R.id.thumbnail_duration_container); - _containerLive = view.findViewById(R.id.thumbnail_live_container); - _imageRemove = view.findViewById(R.id.image_trash); - _textHeader = view.findViewById(R.id.text_header); - _timeBar = view.findViewById(R.id.time_bar); + constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_history, viewGroup, false)) { + _root = itemView.findViewById(R.id.root); + _imageThumbnail = itemView.findViewById(R.id.image_video_thumbnail); + _imageThumbnail.clipToOutline = true; + _textName = itemView.findViewById(R.id.text_video_name); + _textAuthor = itemView.findViewById(R.id.text_author); + _textMetadata = itemView.findViewById(R.id.text_video_metadata); + _textVideoDuration = itemView.findViewById(R.id.thumbnail_duration); + _containerDuration = itemView.findViewById(R.id.thumbnail_duration_container); + _containerLive = itemView.findViewById(R.id.thumbnail_live_container); + _imageRemove = itemView.findViewById(R.id.image_trash); + _textHeader = itemView.findViewById(R.id.text_header); + _timeBar = itemView.findViewById(R.id.time_bar); _root.setOnClickListener { val v = video ?: return@setOnClickListener; onClick.emit(v); }; - _imageRemove?.setOnClickListener { + _imageRemove.setOnClickListener { val v = video ?: return@setOnClickListener; onRemove.emit(v); }; -- GitLab