mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-27 15:30:31 +05:30
refactor: move watch history logic to view model
This commit is contained in:
parent
bac80715e3
commit
3108361aff
@ -90,12 +90,10 @@ object DatabaseHelper {
|
||||
}
|
||||
|
||||
suspend fun filterByWatchStatus(
|
||||
streams: List<WatchHistoryItem>,
|
||||
watchHistoryItem: WatchHistoryItem,
|
||||
unfinished: Boolean = true
|
||||
): List<WatchHistoryItem> {
|
||||
return streams.filter {
|
||||
unfinished xor isVideoWatched(it.videoId, it.duration ?: 0)
|
||||
}
|
||||
): Boolean {
|
||||
return unfinished xor isVideoWatched(watchHistoryItem.videoId, watchHistoryItem.duration ?: 0)
|
||||
}
|
||||
|
||||
fun filterByStatusAndWatchPosition(
|
||||
|
@ -22,30 +22,11 @@ import com.github.libretube.util.TextUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class WatchHistoryAdapter :
|
||||
ListAdapter<WatchHistoryItem, WatchHistoryViewHolder>(DiffUtilItemCallback()) {
|
||||
|
||||
fun removeFromWatchHistory(position: Int) {
|
||||
val history = getItem(position)
|
||||
runBlocking(Dispatchers.IO) {
|
||||
DatabaseHolder.Database.watchHistoryDao().delete(history)
|
||||
}
|
||||
val updatedList = currentList.toMutableList().also {
|
||||
it.removeAt(position)
|
||||
}
|
||||
submitList(updatedList)
|
||||
}
|
||||
|
||||
fun insertItems(items: List<WatchHistoryItem>) {
|
||||
val updatedList = currentList.toMutableList().also {
|
||||
it.addAll(items)
|
||||
}
|
||||
submitList(updatedList)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WatchHistoryViewHolder {
|
||||
val layoutInflater = LayoutInflater.from(parent.context)
|
||||
val binding = VideoRowBinding.inflate(layoutInflater, parent, false)
|
||||
|
@ -2,23 +2,19 @@ package com.github.libretube.ui.fragments
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.Parcelable
|
||||
import android.view.View
|
||||
import androidx.core.os.postDelayed
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.room.withTransaction
|
||||
import com.github.libretube.R
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.databinding.FragmentWatchHistoryBinding
|
||||
import com.github.libretube.db.DatabaseHelper
|
||||
import com.github.libretube.db.DatabaseHolder.Database
|
||||
import com.github.libretube.db.obj.WatchHistoryItem
|
||||
import com.github.libretube.extensions.ceilHalf
|
||||
@ -26,49 +22,28 @@ import com.github.libretube.extensions.dpToPx
|
||||
import com.github.libretube.extensions.setOnDismissListener
|
||||
import com.github.libretube.helpers.NavBarHelper
|
||||
import com.github.libretube.helpers.NavigationHelper
|
||||
import com.github.libretube.helpers.PreferenceHelper
|
||||
import com.github.libretube.ui.adapters.WatchHistoryAdapter
|
||||
import com.github.libretube.ui.base.DynamicLayoutManagerFragment
|
||||
import com.github.libretube.ui.extensions.addOnBottomReachedListener
|
||||
import com.github.libretube.ui.extensions.setupFragmentAnimation
|
||||
import com.github.libretube.ui.models.CommonPlayerViewModel
|
||||
import com.github.libretube.ui.models.WatchHistoryModel
|
||||
import com.github.libretube.ui.sheets.BaseBottomSheet
|
||||
import com.github.libretube.util.PlayingQueue
|
||||
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(R.layout.fragment_watch_history) {
|
||||
private var _binding: FragmentWatchHistoryBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val commonPlayerViewModel: CommonPlayerViewModel by activityViewModels()
|
||||
private var isLoading = false
|
||||
private var recyclerViewState: Parcelable? = null
|
||||
|
||||
private val viewModel: WatchHistoryModel by viewModels()
|
||||
private val watchHistoryAdapter = WatchHistoryAdapter()
|
||||
|
||||
private var selectedStatusFilter = PreferenceHelper.getInt(
|
||||
PreferenceKeys.SELECTED_HISTORY_STATUS_FILTER,
|
||||
0
|
||||
)
|
||||
set(value) {
|
||||
PreferenceHelper.putInt(PreferenceKeys.SELECTED_HISTORY_STATUS_FILTER, value)
|
||||
field = value
|
||||
}
|
||||
private var selectedTypeFilter = PreferenceHelper.getInt(
|
||||
PreferenceKeys.SELECTED_HISTORY_TYPE_FILTER,
|
||||
0
|
||||
)
|
||||
set(value) {
|
||||
PreferenceHelper.putInt(PreferenceKeys.SELECTED_HISTORY_TYPE_FILTER, value)
|
||||
field = value
|
||||
}
|
||||
|
||||
override fun setLayoutManagers(gridItems: Int) {
|
||||
_binding?.watchHistoryRecView?.layoutManager =
|
||||
GridLayoutManager(context, gridItems.ceilHalf())
|
||||
@ -83,7 +58,8 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
|
||||
}
|
||||
|
||||
binding.watchHistoryRecView.setOnDismissListener { position ->
|
||||
watchHistoryAdapter.removeFromWatchHistory(position)
|
||||
val item = viewModel.filteredWatchHistory.value?.getOrNull(position) ?: return@setOnDismissListener
|
||||
viewModel.removeFromHistory(item)
|
||||
}
|
||||
|
||||
// observe changes to indicate if the history is empty
|
||||
@ -107,66 +83,81 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
|
||||
}
|
||||
})
|
||||
|
||||
lifecycleScope.launch {
|
||||
val history = withContext(Dispatchers.IO) {
|
||||
DatabaseHelper.getWatchHistoryPage(1, HISTORY_PAGE_SIZE)
|
||||
}
|
||||
binding.filterTypeTV.text =
|
||||
resources.getStringArray(R.array.filterOptions)[viewModel.selectedTypeFilter]
|
||||
binding.filterStatusTV.text =
|
||||
resources.getStringArray(R.array.filterStatusOptions)[viewModel.selectedStatusFilter]
|
||||
|
||||
if (history.isEmpty()) return@launch
|
||||
val watchPositionItem = arrayOf(getString(R.string.also_clear_watch_positions))
|
||||
val selected = booleanArrayOf(false)
|
||||
|
||||
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)
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
binding.filterTypeTV.setOnClickListener {
|
||||
val filterOptions = resources.getStringArray(R.array.filterOptions)
|
||||
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)
|
||||
}
|
||||
BaseBottomSheet().apply {
|
||||
setSimpleItems(filterOptions.toList()) { index ->
|
||||
binding.filterTypeTV.text = filterOptions[index]
|
||||
viewModel.selectedTypeFilter = index
|
||||
}
|
||||
}.show(childFragmentManager)
|
||||
}
|
||||
|
||||
binding.filterStatusTV.setOnClickListener {
|
||||
val filterOptions = resources.getStringArray(R.array.filterStatusOptions)
|
||||
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)
|
||||
}
|
||||
BaseBottomSheet().apply {
|
||||
setSimpleItems(filterOptions.toList()) { index ->
|
||||
binding.filterStatusTV.text = filterOptions[index]
|
||||
viewModel.selectedStatusFilter = index
|
||||
}
|
||||
}.show(childFragmentManager)
|
||||
}
|
||||
|
||||
showWatchHistory(history)
|
||||
binding.playAll.setOnClickListener {
|
||||
val history = viewModel.filteredWatchHistory.value.orEmpty()
|
||||
if (history.isEmpty()) return@setOnClickListener
|
||||
|
||||
PlayingQueue.add(
|
||||
*history.reversed().map(WatchHistoryItem::toStreamItem).toTypedArray()
|
||||
)
|
||||
NavigationHelper.navigateVideo(
|
||||
requireContext(),
|
||||
history.last().videoId,
|
||||
keepQueue = true
|
||||
)
|
||||
}
|
||||
|
||||
viewModel.filteredWatchHistory.observe(viewLifecycleOwner) { history ->
|
||||
binding.historyEmpty.isGone = history.isNotEmpty()
|
||||
binding.historyContainer.isVisible = history.isNotEmpty()
|
||||
|
||||
watchHistoryAdapter.submitList(history)
|
||||
}
|
||||
|
||||
viewModel.fetchNextPage()
|
||||
|
||||
binding.watchHistoryRecView.addOnBottomReachedListener {
|
||||
viewModel.fetchNextPage()
|
||||
}
|
||||
|
||||
if (NavBarHelper.getStartFragmentId(requireContext()) != R.id.watchHistoryFragment) {
|
||||
@ -174,69 +165,6 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
|
||||
}
|
||||
}
|
||||
|
||||
private fun showWatchHistory(history: List<WatchHistoryItem>) {
|
||||
val watchHistory = history.filterByStatusAndWatchPosition()
|
||||
|
||||
binding.playAll.setOnClickListener {
|
||||
PlayingQueue.add(
|
||||
*watchHistory.reversed().map(WatchHistoryItem::toStreamItem).toTypedArray()
|
||||
)
|
||||
NavigationHelper.navigateVideo(
|
||||
requireContext(),
|
||||
watchHistory.last().videoId,
|
||||
keepQueue = true
|
||||
)
|
||||
}
|
||||
watchHistoryAdapter.submitList(history)
|
||||
binding.historyEmpty.isGone = true
|
||||
binding.historyContainer.isVisible = true
|
||||
|
||||
// add a listener for scroll end, delay needed to prevent loading new ones the first time
|
||||
handler.postDelayed(200) {
|
||||
if (_binding == null) return@postDelayed
|
||||
|
||||
binding.watchHistoryRecView.addOnBottomReachedListener {
|
||||
if (isLoading) return@addOnBottomReachedListener
|
||||
isLoading = true
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<WatchHistoryItem>.filterByStatusAndWatchPosition(): List<WatchHistoryItem> {
|
||||
val watchHistoryItem = this.filter {
|
||||
val isLive = (it.duration ?: -1L) < 0L
|
||||
when (selectedTypeFilter) {
|
||||
0 -> true
|
||||
1 -> !it.isShort && !isLive
|
||||
2 -> it.isShort // where is the StreamItem converted to watchHistoryItem?
|
||||
3 -> isLive
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedStatusFilter == 0) {
|
||||
return watchHistoryItem
|
||||
}
|
||||
|
||||
return runBlocking {
|
||||
when (selectedStatusFilter) {
|
||||
1 -> DatabaseHelper.filterByWatchStatus(watchHistoryItem)
|
||||
2 -> DatabaseHelper.filterByWatchStatus(watchHistoryItem, false)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
// manually restore the recyclerview state due to https://github.com/material-components/material-components-android/issues/3473
|
||||
@ -247,8 +175,4 @@ class WatchHistoryFragment : DynamicLayoutManagerFragment(R.layout.fragment_watc
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val HISTORY_PAGE_SIZE = 10
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,105 @@
|
||||
package com.github.libretube.ui.models
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.github.libretube.constants.PreferenceKeys
|
||||
import com.github.libretube.db.DatabaseHelper
|
||||
import com.github.libretube.db.DatabaseHolder
|
||||
import com.github.libretube.db.obj.WatchHistoryItem
|
||||
import com.github.libretube.helpers.PreferenceHelper
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class WatchHistoryModel : ViewModel() {
|
||||
private val watchHistory = MutableLiveData<List<WatchHistoryItem>>()
|
||||
|
||||
private var currentPage = 1
|
||||
private var isLoading = false
|
||||
|
||||
private val selectedStatus = MutableStateFlow(
|
||||
PreferenceHelper.getInt(PreferenceKeys.SELECTED_HISTORY_STATUS_FILTER, 0)
|
||||
)
|
||||
private val selectedType = MutableStateFlow(
|
||||
PreferenceHelper.getInt(PreferenceKeys.SELECTED_HISTORY_TYPE_FILTER, 0)
|
||||
)
|
||||
|
||||
val filteredWatchHistory =
|
||||
combine(watchHistory.asFlow(), selectedStatus, selectedType) { history, _, _ -> history }
|
||||
.flowOn(Dispatchers.IO).map { history -> history.filter { it.shouldIncludeByFilters() } }
|
||||
.asLiveData()
|
||||
|
||||
var selectedStatusFilter
|
||||
get() = selectedStatus.value
|
||||
set(value) {
|
||||
PreferenceHelper.putInt(PreferenceKeys.SELECTED_HISTORY_STATUS_FILTER, value)
|
||||
selectedStatus.value = value
|
||||
}
|
||||
|
||||
var selectedTypeFilter
|
||||
get() = selectedType.value
|
||||
set(value) {
|
||||
PreferenceHelper.putInt(PreferenceKeys.SELECTED_HISTORY_TYPE_FILTER, value)
|
||||
selectedType.value = value
|
||||
}
|
||||
|
||||
private suspend fun WatchHistoryItem.shouldIncludeByFilters(): Boolean {
|
||||
val isLive = (duration ?: -1L) < 0L
|
||||
val matchesFilter = when (selectedTypeFilter) {
|
||||
0 -> true
|
||||
1 -> !isShort && !isLive
|
||||
2 -> isShort // where is the StreamItem converted to watchHistoryItem?
|
||||
3 -> isLive
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
if (!matchesFilter) return false
|
||||
|
||||
// no watch position filter
|
||||
if (selectedStatusFilter == 0) return true
|
||||
|
||||
return when (selectedStatusFilter) {
|
||||
1 -> DatabaseHelper.filterByWatchStatus(this)
|
||||
2 -> DatabaseHelper.filterByWatchStatus(this, false)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
}
|
||||
|
||||
fun fetchNextPage() = viewModelScope.launch(Dispatchers.IO) {
|
||||
if (isLoading) return@launch
|
||||
isLoading = true
|
||||
|
||||
val newHistory = withContext(Dispatchers.IO) {
|
||||
DatabaseHelper.getWatchHistoryPage(currentPage, HISTORY_PAGE_SIZE)
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
currentPage++
|
||||
|
||||
watchHistory.postValue(
|
||||
watchHistory.value.orEmpty().toMutableList().apply {
|
||||
addAll(newHistory)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun removeFromHistory(watchHistoryItem: WatchHistoryItem) =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
DatabaseHolder.Database.watchHistoryDao().delete(watchHistoryItem)
|
||||
|
||||
watchHistory.postValue(
|
||||
watchHistory.value.orEmpty().filter { it != watchHistoryItem }
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val HISTORY_PAGE_SIZE = 10
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package com.github.libretube.ui.models.sources
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.github.libretube.db.DatabaseHelper
|
||||
import com.github.libretube.db.obj.WatchHistoryItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class WatchHistoryPagingSource(
|
||||
private val shouldIncludeItemPredicate: suspend (WatchHistoryItem) -> Boolean
|
||||
): PagingSource<Int, WatchHistoryItem>() {
|
||||
override fun getRefreshKey(state: PagingState<Int, WatchHistoryItem>) = null
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, WatchHistoryItem> {
|
||||
val newHistory = withContext(Dispatchers.IO) {
|
||||
DatabaseHelper.getWatchHistoryPage( params.key ?: 0, params.loadSize)
|
||||
}.filter { shouldIncludeItemPredicate(it) }
|
||||
|
||||
return LoadResult.Page(newHistory, params.key ?: 0, params.key?.plus(1) ?: 0)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user