feat: watch history pagination

This commit is contained in:
Bnyro 2024-08-10 17:42:05 +02:00
parent 0c39a25a4c
commit 6970bf6ee2
5 changed files with 114 additions and 81 deletions

View File

@ -46,12 +46,27 @@ object DatabaseHelper {
}
// delete the first watch history entry if the limit is reached
val watchHistory = Database.watchHistoryDao().getAll()
if (watchHistory.size > maxHistorySize.toInt()) {
Database.watchHistoryDao().delete(watchHistory.first())
val historySize = Database.watchHistoryDao().getSize()
if (historySize > maxHistorySize.toInt()) {
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) {
Database.searchHistoryDao().insert(searchHistoryItem)

View File

@ -12,6 +12,12 @@ interface WatchHistoryDao {
@Query("SELECT * FROM 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")
suspend fun findById(videoId: String): WatchHistoryItem?
@ -24,6 +30,9 @@ interface WatchHistoryDao {
@Delete
suspend fun delete(watchHistoryItem: WatchHistoryItem)
@Query("SELECT * FROM watchHistoryItem LIMIT 1 OFFSET 0")
suspend fun getOldest(): WatchHistoryItem
@Query("DELETE FROM watchHistoryItem WHERE videoId = :id")
suspend fun deleteByVideoId(id: String)

View File

@ -24,9 +24,7 @@ class WatchHistoryAdapter(
) :
RecyclerView.Adapter<WatchHistoryViewHolder>() {
private var visibleCount = minOf(10, watchHistory.size)
override fun getItemCount() = visibleCount
override fun getItemCount() = watchHistory.size
fun removeFromWatchHistory(position: Int) {
val history = watchHistory[position]
@ -34,16 +32,14 @@ class WatchHistoryAdapter(
DatabaseHolder.Database.watchHistoryDao().delete(history)
}
watchHistory.removeAt(position)
visibleCount--
notifyItemRemoved(position)
notifyItemRangeChanged(position, itemCount)
}
fun showMoreItems() {
val oldSize = visibleCount
visibleCount += minOf(10, watchHistory.size - oldSize)
if (visibleCount == oldSize) return
notifyItemRangeInserted(oldSize, visibleCount)
fun insertItems(items: List<WatchHistoryItem>) {
val oldSize = itemCount
this.watchHistory.addAll(items)
notifyItemRangeInserted(oldSize, itemCount)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WatchHistoryViewHolder {

View File

@ -40,6 +40,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlin.math.ceil
class WatchHistoryFragment : DynamicLayoutManagerFragment() {
private var _binding: FragmentWatchHistoryBinding? = null
@ -88,86 +90,86 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment() {
_binding?.watchHistoryRecView?.updatePadding(bottom = if (it) 64f.dpToPx() else 0)
}
val allHistory = runBlocking(Dispatchers.IO) {
Database.watchHistoryDao().getAll().reversed()
}
lifecycleScope.launch {
val history = withContext(Dispatchers.IO) {
DatabaseHelper.getWatchHistoryPage(1, HISTORY_PAGE_SIZE)
}
if (allHistory.isEmpty()) return
if (history.isEmpty()) return@launch
binding.filterTypeTV.text =
resources.getStringArray(R.array.filterOptions)[selectedTypeFilter]
binding.filterStatusTV.text =
resources.getStringArray(R.array.filterStatusOptions)[selectedStatusFilter]
binding.filterTypeTV.text =
resources.getStringArray(R.array.filterOptions)[selectedTypeFilter]
binding.filterStatusTV.text =
resources.getStringArray(R.array.filterStatusOptions)[selectedStatusFilter]
val watchPositionItem = arrayOf(getString(R.string.also_clear_watch_positions))
val selected = booleanArrayOf(false)
val watchPositionItem = arrayOf(getString(R.string.also_clear_watch_positions))
val selected = booleanArrayOf(false)
binding.clear.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.clear_history)
.setMultiChoiceItems(watchPositionItem, selected) { _, index, newValue ->
selected[index] = newValue
}
.setPositiveButton(R.string.okay) { _, _ ->
binding.historyContainer.isGone = true
binding.historyEmpty.isVisible = true
lifecycleScope.launch(Dispatchers.IO) {
Database.withTransaction {
Database.watchHistoryDao().deleteAll()
if (selected[0]) Database.watchPositionDao().deleteAll()
binding.clear.setOnClickListener {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.clear_history)
.setMultiChoiceItems(watchPositionItem, selected) { _, index, newValue ->
selected[index] = newValue
}
.setPositiveButton(R.string.okay) { _, _ ->
binding.historyContainer.isGone = true
binding.historyEmpty.isVisible = true
lifecycleScope.launch(Dispatchers.IO) {
Database.withTransaction {
Database.watchHistoryDao().deleteAll()
if (selected[0]) Database.watchPositionDao().deleteAll()
}
}
}
}
.setNegativeButton(R.string.cancel, null)
.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()
.setNegativeButton(R.string.cancel, null)
.show()
}
})
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>) {
val watchHistory = allHistory.filterByStatusAndWatchPosition()
private fun showWatchHistory(history: List<WatchHistoryItem>) {
val watchHistory = history.filterByStatusAndWatchPosition()
watchHistory.forEach {
it.thumbnailUrl = ProxyHelper.rewriteUrl(it.thumbnailUrl)
it.uploaderAvatar = ProxyHelper.rewriteUrl(it.uploaderAvatar)
}
val watchHistoryAdapter = WatchHistoryAdapter(
watchHistory.toMutableList()
)
val watchHistoryAdapter = WatchHistoryAdapter(watchHistory.toMutableList())
binding.playAll.setOnClickListener {
PlayingQueue.resetToDefaults()
@ -234,10 +236,17 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment() {
binding.watchHistoryRecView.addOnBottomReachedListener {
if (isLoading) return@addOnBottomReachedListener
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()
_binding = null
}
companion object {
private const val HISTORY_PAGE_SIZE = 10
}
}

View File

@ -112,10 +112,10 @@ class HomeViewModel : ViewModel() {
}
private suspend fun loadWatchingFromDB(): List<StreamItem> {
val videos = DatabaseHolder.Database.watchHistoryDao().getAll()
val videos = DatabaseHelper.getWatchHistoryPage(1, 50)
return DatabaseHelper
.filterUnwatched(videos.map { it.toStreamItem() })
.reversed()
.take(20)
}