mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-28 16:00:31 +05:30
feat: watch history pagination
This commit is contained in:
parent
0c39a25a4c
commit
6970bf6ee2
@ -46,12 +46,27 @@ object DatabaseHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// delete the first watch history entry if the limit is reached
|
// delete the first watch history entry if the limit is reached
|
||||||
val watchHistory = Database.watchHistoryDao().getAll()
|
val historySize = Database.watchHistoryDao().getSize()
|
||||||
if (watchHistory.size > maxHistorySize.toInt()) {
|
if (historySize > maxHistorySize.toInt()) {
|
||||||
Database.watchHistoryDao().delete(watchHistory.first())
|
Database.watchHistoryDao().delete(Database.watchHistoryDao().getOldest())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun getWatchHistoryPage(page: Int, pageSize: Int): List<WatchHistoryItem> {
|
||||||
|
val watchHistoryDao = Database.watchHistoryDao()
|
||||||
|
val historySize = watchHistoryDao.getSize()
|
||||||
|
|
||||||
|
if (historySize < pageSize * (page-1)) return emptyList()
|
||||||
|
|
||||||
|
val offset = historySize - (pageSize * page)
|
||||||
|
val limit = if (offset < 0) {
|
||||||
|
offset + pageSize
|
||||||
|
} else {
|
||||||
|
pageSize
|
||||||
|
}
|
||||||
|
return watchHistoryDao.getN(limit, maxOf(offset, 0)).reversed()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun addToSearchHistory(searchHistoryItem: SearchHistoryItem) {
|
suspend fun addToSearchHistory(searchHistoryItem: SearchHistoryItem) {
|
||||||
Database.searchHistoryDao().insert(searchHistoryItem)
|
Database.searchHistoryDao().insert(searchHistoryItem)
|
||||||
|
|
||||||
|
@ -12,6 +12,12 @@ interface WatchHistoryDao {
|
|||||||
@Query("SELECT * FROM watchHistoryItem")
|
@Query("SELECT * FROM watchHistoryItem")
|
||||||
suspend fun getAll(): List<WatchHistoryItem>
|
suspend fun getAll(): List<WatchHistoryItem>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM watchHistoryItem LIMIT :limit OFFSET :offset")
|
||||||
|
suspend fun getN(limit: Int, offset: Int): List<WatchHistoryItem>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(videoId) FROM watchHistoryItem")
|
||||||
|
suspend fun getSize(): Int
|
||||||
|
|
||||||
@Query("SELECT * FROM watchHistoryItem WHERE videoId LIKE :videoId LIMIT 1")
|
@Query("SELECT * FROM watchHistoryItem WHERE videoId LIKE :videoId LIMIT 1")
|
||||||
suspend fun findById(videoId: String): WatchHistoryItem?
|
suspend fun findById(videoId: String): WatchHistoryItem?
|
||||||
|
|
||||||
@ -24,6 +30,9 @@ interface WatchHistoryDao {
|
|||||||
@Delete
|
@Delete
|
||||||
suspend fun delete(watchHistoryItem: WatchHistoryItem)
|
suspend fun delete(watchHistoryItem: WatchHistoryItem)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM watchHistoryItem LIMIT 1 OFFSET 0")
|
||||||
|
suspend fun getOldest(): WatchHistoryItem
|
||||||
|
|
||||||
@Query("DELETE FROM watchHistoryItem WHERE videoId = :id")
|
@Query("DELETE FROM watchHistoryItem WHERE videoId = :id")
|
||||||
suspend fun deleteByVideoId(id: String)
|
suspend fun deleteByVideoId(id: String)
|
||||||
|
|
||||||
|
@ -24,9 +24,7 @@ class WatchHistoryAdapter(
|
|||||||
) :
|
) :
|
||||||
RecyclerView.Adapter<WatchHistoryViewHolder>() {
|
RecyclerView.Adapter<WatchHistoryViewHolder>() {
|
||||||
|
|
||||||
private var visibleCount = minOf(10, watchHistory.size)
|
override fun getItemCount() = watchHistory.size
|
||||||
|
|
||||||
override fun getItemCount() = visibleCount
|
|
||||||
|
|
||||||
fun removeFromWatchHistory(position: Int) {
|
fun removeFromWatchHistory(position: Int) {
|
||||||
val history = watchHistory[position]
|
val history = watchHistory[position]
|
||||||
@ -34,16 +32,14 @@ class WatchHistoryAdapter(
|
|||||||
DatabaseHolder.Database.watchHistoryDao().delete(history)
|
DatabaseHolder.Database.watchHistoryDao().delete(history)
|
||||||
}
|
}
|
||||||
watchHistory.removeAt(position)
|
watchHistory.removeAt(position)
|
||||||
visibleCount--
|
|
||||||
notifyItemRemoved(position)
|
notifyItemRemoved(position)
|
||||||
notifyItemRangeChanged(position, itemCount)
|
notifyItemRangeChanged(position, itemCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showMoreItems() {
|
fun insertItems(items: List<WatchHistoryItem>) {
|
||||||
val oldSize = visibleCount
|
val oldSize = itemCount
|
||||||
visibleCount += minOf(10, watchHistory.size - oldSize)
|
this.watchHistory.addAll(items)
|
||||||
if (visibleCount == oldSize) return
|
notifyItemRangeInserted(oldSize, itemCount)
|
||||||
notifyItemRangeInserted(oldSize, visibleCount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WatchHistoryViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WatchHistoryViewHolder {
|
||||||
|
@ -40,6 +40,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
class WatchHistoryFragment : DynamicLayoutManagerFragment() {
|
class WatchHistoryFragment : DynamicLayoutManagerFragment() {
|
||||||
private var _binding: FragmentWatchHistoryBinding? = null
|
private var _binding: FragmentWatchHistoryBinding? = null
|
||||||
@ -88,86 +90,86 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment() {
|
|||||||
_binding?.watchHistoryRecView?.updatePadding(bottom = if (it) 64f.dpToPx() else 0)
|
_binding?.watchHistoryRecView?.updatePadding(bottom = if (it) 64f.dpToPx() else 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val allHistory = runBlocking(Dispatchers.IO) {
|
lifecycleScope.launch {
|
||||||
Database.watchHistoryDao().getAll().reversed()
|
val history = withContext(Dispatchers.IO) {
|
||||||
}
|
DatabaseHelper.getWatchHistoryPage(1, HISTORY_PAGE_SIZE)
|
||||||
|
}
|
||||||
|
|
||||||
if (allHistory.isEmpty()) return
|
if (history.isEmpty()) return@launch
|
||||||
|
|
||||||
binding.filterTypeTV.text =
|
binding.filterTypeTV.text =
|
||||||
resources.getStringArray(R.array.filterOptions)[selectedTypeFilter]
|
resources.getStringArray(R.array.filterOptions)[selectedTypeFilter]
|
||||||
binding.filterStatusTV.text =
|
binding.filterStatusTV.text =
|
||||||
resources.getStringArray(R.array.filterStatusOptions)[selectedStatusFilter]
|
resources.getStringArray(R.array.filterStatusOptions)[selectedStatusFilter]
|
||||||
|
|
||||||
val watchPositionItem = arrayOf(getString(R.string.also_clear_watch_positions))
|
val watchPositionItem = arrayOf(getString(R.string.also_clear_watch_positions))
|
||||||
val selected = booleanArrayOf(false)
|
val selected = booleanArrayOf(false)
|
||||||
|
|
||||||
binding.clear.setOnClickListener {
|
binding.clear.setOnClickListener {
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
.setTitle(R.string.clear_history)
|
.setTitle(R.string.clear_history)
|
||||||
.setMultiChoiceItems(watchPositionItem, selected) { _, index, newValue ->
|
.setMultiChoiceItems(watchPositionItem, selected) { _, index, newValue ->
|
||||||
selected[index] = newValue
|
selected[index] = newValue
|
||||||
}
|
}
|
||||||
.setPositiveButton(R.string.okay) { _, _ ->
|
.setPositiveButton(R.string.okay) { _, _ ->
|
||||||
binding.historyContainer.isGone = true
|
binding.historyContainer.isGone = true
|
||||||
binding.historyEmpty.isVisible = true
|
binding.historyEmpty.isVisible = true
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
Database.withTransaction {
|
Database.withTransaction {
|
||||||
Database.watchHistoryDao().deleteAll()
|
Database.watchHistoryDao().deleteAll()
|
||||||
if (selected[0]) Database.watchPositionDao().deleteAll()
|
if (selected[0]) Database.watchPositionDao().deleteAll()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
.setNegativeButton(R.string.cancel, null)
|
||||||
.setNegativeButton(R.string.cancel, null)
|
.show()
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.filterTypeTV.setOnClickListener {
|
|
||||||
val filterOptions = resources.getStringArray(R.array.filterOptions)
|
|
||||||
|
|
||||||
BaseBottomSheet().apply {
|
|
||||||
setSimpleItems(filterOptions.toList()) { index ->
|
|
||||||
binding.filterTypeTV.text = filterOptions[index]
|
|
||||||
selectedTypeFilter = index
|
|
||||||
showWatchHistory(allHistory)
|
|
||||||
}
|
|
||||||
}.show(childFragmentManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.filterStatusTV.setOnClickListener {
|
|
||||||
val filterOptions = resources.getStringArray(R.array.filterStatusOptions)
|
|
||||||
|
|
||||||
BaseBottomSheet().apply {
|
|
||||||
setSimpleItems(filterOptions.toList()) { index ->
|
|
||||||
binding.filterStatusTV.text = filterOptions[index]
|
|
||||||
selectedStatusFilter = index
|
|
||||||
showWatchHistory(allHistory)
|
|
||||||
}
|
|
||||||
}.show(childFragmentManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
// manually restore the recyclerview state due to https://github.com/material-components/material-components-android/issues/3473
|
|
||||||
binding.watchHistoryRecView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
|
||||||
super.onScrollStateChanged(recyclerView, newState)
|
|
||||||
recyclerViewState = binding.watchHistoryRecView.layoutManager?.onSaveInstanceState()
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
showWatchHistory(allHistory)
|
binding.filterTypeTV.setOnClickListener {
|
||||||
|
val filterOptions = resources.getStringArray(R.array.filterOptions)
|
||||||
|
|
||||||
|
BaseBottomSheet().apply {
|
||||||
|
setSimpleItems(filterOptions.toList()) { index ->
|
||||||
|
binding.filterTypeTV.text = filterOptions[index]
|
||||||
|
selectedTypeFilter = index
|
||||||
|
showWatchHistory(history)
|
||||||
|
}
|
||||||
|
}.show(childFragmentManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.filterStatusTV.setOnClickListener {
|
||||||
|
val filterOptions = resources.getStringArray(R.array.filterStatusOptions)
|
||||||
|
|
||||||
|
BaseBottomSheet().apply {
|
||||||
|
setSimpleItems(filterOptions.toList()) { index ->
|
||||||
|
binding.filterStatusTV.text = filterOptions[index]
|
||||||
|
selectedStatusFilter = index
|
||||||
|
showWatchHistory(history)
|
||||||
|
}
|
||||||
|
}.show(childFragmentManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
// manually restore the recyclerview state due to https://github.com/material-components/material-components-android/issues/3473
|
||||||
|
binding.watchHistoryRecView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
|
recyclerViewState = binding.watchHistoryRecView.layoutManager?.onSaveInstanceState()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
showWatchHistory(history)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showWatchHistory(allHistory: List<WatchHistoryItem>) {
|
private fun showWatchHistory(history: List<WatchHistoryItem>) {
|
||||||
val watchHistory = allHistory.filterByStatusAndWatchPosition()
|
val watchHistory = history.filterByStatusAndWatchPosition()
|
||||||
|
|
||||||
watchHistory.forEach {
|
watchHistory.forEach {
|
||||||
it.thumbnailUrl = ProxyHelper.rewriteUrl(it.thumbnailUrl)
|
it.thumbnailUrl = ProxyHelper.rewriteUrl(it.thumbnailUrl)
|
||||||
it.uploaderAvatar = ProxyHelper.rewriteUrl(it.uploaderAvatar)
|
it.uploaderAvatar = ProxyHelper.rewriteUrl(it.uploaderAvatar)
|
||||||
}
|
}
|
||||||
|
|
||||||
val watchHistoryAdapter = WatchHistoryAdapter(
|
val watchHistoryAdapter = WatchHistoryAdapter(watchHistory.toMutableList())
|
||||||
watchHistory.toMutableList()
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.playAll.setOnClickListener {
|
binding.playAll.setOnClickListener {
|
||||||
PlayingQueue.resetToDefaults()
|
PlayingQueue.resetToDefaults()
|
||||||
@ -234,10 +236,17 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment() {
|
|||||||
|
|
||||||
binding.watchHistoryRecView.addOnBottomReachedListener {
|
binding.watchHistoryRecView.addOnBottomReachedListener {
|
||||||
if (isLoading) return@addOnBottomReachedListener
|
if (isLoading) return@addOnBottomReachedListener
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
watchHistoryAdapter.showMoreItems()
|
|
||||||
isLoading = false
|
lifecycleScope.launch {
|
||||||
|
val newHistory = withContext(Dispatchers.IO) {
|
||||||
|
val currentPage = ceil(watchHistoryAdapter.itemCount.toFloat() / HISTORY_PAGE_SIZE).toInt()
|
||||||
|
DatabaseHelper.getWatchHistoryPage( currentPage + 1, HISTORY_PAGE_SIZE)
|
||||||
|
}.filterByStatusAndWatchPosition()
|
||||||
|
|
||||||
|
watchHistoryAdapter.insertItems(newHistory)
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -277,4 +286,8 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment() {
|
|||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
_binding = null
|
_binding = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val HISTORY_PAGE_SIZE = 10
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -112,10 +112,10 @@ class HomeViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadWatchingFromDB(): List<StreamItem> {
|
private suspend fun loadWatchingFromDB(): List<StreamItem> {
|
||||||
val videos = DatabaseHolder.Database.watchHistoryDao().getAll()
|
val videos = DatabaseHelper.getWatchHistoryPage(1, 50)
|
||||||
|
|
||||||
return DatabaseHelper
|
return DatabaseHelper
|
||||||
.filterUnwatched(videos.map { it.toStreamItem() })
|
.filterUnwatched(videos.map { it.toStreamItem() })
|
||||||
.reversed()
|
|
||||||
.take(20)
|
.take(20)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user