Updated HistoryView to use pager.

This commit is contained in:
Koen 2023-12-06 16:32:17 +01:00
parent c5d35b27f0
commit 98d92d3fe2
5 changed files with 267 additions and 200 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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"
}
}

View file

@ -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";
}
}

View file

@ -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);
};