mirror of
https://github.com/libre-tube/LibreTube.git
synced 2025-04-29 08:20:32 +05:30
feat: Improve new user experience + adjust home load (#5491)
* Added necessary translations * Added support for redirecting directly to IntentSettings * Create HomeViewModel * Used HomeViewModel * Update app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt Co-authored-by: Bnyro <82752168+Bnyro@users.noreply.github.com> * Update app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt Co-authored-by: Bnyro <82752168+Bnyro@users.noreply.github.com> * Update app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt Co-authored-by: Bnyro <82752168+Bnyro@users.noreply.github.com> * Swap elvis operator for if statement for improved readability. * Move runSafely to separate file * Change when statement by if statement Co-authored-by: Bnyro <82752168+Bnyro@users.noreply.github.com> * Format if statement * * Remove LiveData properties; * Change buttons style for consistency; * Move updateIfChanged to a separate file; --------- Co-authored-by: Bnyro <82752168+Bnyro@users.noreply.github.com>
This commit is contained in:
parent
3cb99a712d
commit
e31943f5ab
@ -0,0 +1,7 @@
|
|||||||
|
package com.github.libretube.extensions
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
|
||||||
|
fun <T> MutableLiveData<T>.updateIfChanged(newValue: T) {
|
||||||
|
if (value != newValue) value = newValue
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.github.libretube.extensions
|
||||||
|
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
suspend fun <T> runSafely(
|
||||||
|
onSuccess: (List<T>) -> Unit = { },
|
||||||
|
ioBlock: suspend () -> List<T>,
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val result = runCatching { ioBlock.invoke() }
|
||||||
|
.getOrNull()
|
||||||
|
?.takeIf { it.isNotEmpty() } ?: return@withContext
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
if (result.isNotEmpty()) {
|
||||||
|
onSuccess.invoke(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,11 +2,13 @@ package com.github.libretube.ui.activities
|
|||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.activity.addCallback
|
import androidx.activity.addCallback
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.commit
|
import androidx.fragment.app.commit
|
||||||
import androidx.fragment.app.replace
|
import androidx.fragment.app.replace
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.databinding.ActivitySettingsBinding
|
import com.github.libretube.databinding.ActivitySettingsBinding
|
||||||
import com.github.libretube.ui.base.BaseActivity
|
import com.github.libretube.ui.base.BaseActivity
|
||||||
|
import com.github.libretube.ui.preferences.InstanceSettings
|
||||||
import com.github.libretube.ui.preferences.MainSettings
|
import com.github.libretube.ui.preferences.MainSettings
|
||||||
|
|
||||||
class SettingsActivity : BaseActivity() {
|
class SettingsActivity : BaseActivity() {
|
||||||
@ -24,9 +26,7 @@ class SettingsActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
supportFragmentManager.commit {
|
redirectTo<MainSettings>()
|
||||||
replace<MainSettings>(R.id.settings)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// new way of dealing with back presses instead of onBackPressed()
|
// new way of dealing with back presses instead of onBackPressed()
|
||||||
@ -34,15 +34,32 @@ class SettingsActivity : BaseActivity() {
|
|||||||
if (supportFragmentManager.findFragmentById(R.id.settings) is MainSettings) {
|
if (supportFragmentManager.findFragmentById(R.id.settings) is MainSettings) {
|
||||||
finishAndRemoveTask()
|
finishAndRemoveTask()
|
||||||
} else {
|
} else {
|
||||||
supportFragmentManager.commit {
|
redirectTo<MainSettings>()
|
||||||
replace<MainSettings>(R.id.settings)
|
|
||||||
}
|
|
||||||
changeTopBarText(getString(R.string.settings))
|
changeTopBarText(getString(R.string.settings))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleRedirect()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRedirect() {
|
||||||
|
val redirectKey = intent.extras?.getString(REDIRECT_KEY)
|
||||||
|
|
||||||
|
if (redirectKey == REDIRECT_TO_INTENT_SETTINGS) redirectTo<InstanceSettings>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun changeTopBarText(text: String) {
|
fun changeTopBarText(text: String) {
|
||||||
if (this::binding.isInitialized) binding.toolbar.title = text
|
if (this::binding.isInitialized) binding.toolbar.title = text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private inline fun <reified T : Fragment> redirectTo() {
|
||||||
|
supportFragmentManager.commit {
|
||||||
|
replace<T>(R.id.settings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REDIRECT_KEY = "redirect"
|
||||||
|
const val REDIRECT_TO_INTENT_SETTINGS = "intent_settings"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.github.libretube.ui.fragments
|
package com.github.libretube.ui.fragments
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -8,42 +9,34 @@ import androidx.core.view.isGone
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.github.libretube.R
|
import com.github.libretube.R
|
||||||
import com.github.libretube.api.PlaylistsHelper
|
import com.github.libretube.api.PlaylistsHelper
|
||||||
import com.github.libretube.api.RetrofitInstance
|
import com.github.libretube.api.obj.Playlists
|
||||||
import com.github.libretube.api.SubscriptionHelper
|
import com.github.libretube.api.obj.StreamItem
|
||||||
import com.github.libretube.constants.PreferenceKeys
|
import com.github.libretube.constants.PreferenceKeys.HOME_TAB_CONTENT
|
||||||
import com.github.libretube.databinding.FragmentHomeBinding
|
import com.github.libretube.databinding.FragmentHomeBinding
|
||||||
import com.github.libretube.db.DatabaseHelper
|
import com.github.libretube.db.obj.PlaylistBookmark
|
||||||
import com.github.libretube.db.DatabaseHolder
|
|
||||||
import com.github.libretube.enums.ContentFilter
|
|
||||||
import com.github.libretube.helpers.LocaleHelper
|
|
||||||
import com.github.libretube.helpers.PlayerHelper
|
|
||||||
import com.github.libretube.helpers.PreferenceHelper
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
|
import com.github.libretube.ui.activities.SettingsActivity
|
||||||
import com.github.libretube.ui.adapters.PlaylistBookmarkAdapter
|
import com.github.libretube.ui.adapters.PlaylistBookmarkAdapter
|
||||||
import com.github.libretube.ui.adapters.PlaylistsAdapter
|
import com.github.libretube.ui.adapters.PlaylistsAdapter
|
||||||
import com.github.libretube.ui.adapters.VideosAdapter
|
import com.github.libretube.ui.adapters.VideosAdapter
|
||||||
|
import com.github.libretube.ui.adapters.VideosAdapter.Companion.LayoutMode
|
||||||
|
import com.github.libretube.ui.models.HomeViewModel
|
||||||
import com.github.libretube.ui.models.SubscriptionsViewModel
|
import com.github.libretube.ui.models.SubscriptionsViewModel
|
||||||
import com.github.libretube.util.deArrow
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.awaitAll
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class HomeFragment : Fragment() {
|
class HomeFragment : Fragment() {
|
||||||
private var _binding: FragmentHomeBinding? = null
|
private var _binding: FragmentHomeBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
private val subscriptionsViewModel: SubscriptionsViewModel by activityViewModels()
|
private val subscriptionsViewModel: SubscriptionsViewModel by activityViewModels()
|
||||||
|
private val homeViewModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
@ -82,7 +75,50 @@ class HomeFragment : Fragment() {
|
|||||||
fetchHomeFeed()
|
fetchHomeFeed()
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchHomeFeed()
|
binding.refreshButton.setOnClickListener {
|
||||||
|
fetchHomeFeed()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.changeInstance.setOnClickListener {
|
||||||
|
redirectToIntentSettings()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
observeChanges()
|
||||||
|
|
||||||
|
// Avoid re-fetching when re-entering the screen if it was loaded successfully
|
||||||
|
if (homeViewModel.loadedSuccessfully.value == false) {
|
||||||
|
fetchHomeFeed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeChanges() {
|
||||||
|
with (homeViewModel) {
|
||||||
|
trending.observe(viewLifecycleOwner, ::showTrending)
|
||||||
|
feed.observe(viewLifecycleOwner, ::showFeed)
|
||||||
|
bookmarks.observe(viewLifecycleOwner, ::showBookmarks)
|
||||||
|
playlists.observe(viewLifecycleOwner, ::showPlaylists)
|
||||||
|
continueWatching.observe(viewLifecycleOwner, ::showContinueWatching)
|
||||||
|
isLoading.observe(viewLifecycleOwner, ::updateLoading)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
super.onPause()
|
||||||
|
stopObservingChanges()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopObservingChanges() {
|
||||||
|
with (homeViewModel) {
|
||||||
|
trending.removeObserver(::showTrending)
|
||||||
|
feed.removeObserver(::showFeed)
|
||||||
|
bookmarks.removeObserver(::showBookmarks)
|
||||||
|
playlists.removeObserver(::showPlaylists)
|
||||||
|
continueWatching.removeObserver(::showContinueWatching)
|
||||||
|
isLoading.removeObserver(::updateLoading)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
@ -91,118 +127,58 @@ class HomeFragment : Fragment() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchHomeFeed() {
|
private fun fetchHomeFeed() {
|
||||||
lifecycleScope.launch {
|
binding.nothingHere.isGone = true
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
val defaultItems = resources.getStringArray(R.array.homeTabItemsValues)
|
||||||
binding.nothingHere.isGone = true
|
val visibleItems = PreferenceHelper.getStringSet(HOME_TAB_CONTENT, defaultItems.toSet())
|
||||||
val defaultItems = resources.getStringArray(R.array.homeTabItemsValues)
|
|
||||||
val visibleItems = PreferenceHelper
|
|
||||||
.getStringSet(PreferenceKeys.HOME_TAB_CONTENT, defaultItems.toSet())
|
|
||||||
awaitAll(
|
|
||||||
async { if (visibleItems.contains(TRENDING)) loadTrending() },
|
|
||||||
async { if (visibleItems.contains(WATCHING)) loadVideosToContinueWatching() },
|
|
||||||
async { if (visibleItems.contains(BOOKMARKS)) loadBookmarks() },
|
|
||||||
async { if (visibleItems.contains(FEATURED)) loadFeed() },
|
|
||||||
async { if (visibleItems.contains(PLAYLISTS)) loadPlaylists() }
|
|
||||||
)
|
|
||||||
|
|
||||||
val binding = _binding ?: return@repeatOnLifecycle
|
homeViewModel.loadHomeFeed(
|
||||||
// No category is shown because they are either empty or disabled
|
context = requireContext(),
|
||||||
if (binding.progress.isVisible) {
|
savedFeed = subscriptionsViewModel.videoFeed.value,
|
||||||
binding.progress.isGone = true
|
visibleItems = visibleItems,
|
||||||
binding.nothingHere.isVisible = true
|
onUnusualLoadTime = ::showChangeInstanceSnackBar
|
||||||
}
|
)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadTrending() {
|
private fun showTrending(streamItems: List<StreamItem>?) {
|
||||||
val region = LocaleHelper.getTrendingRegion(requireContext())
|
if (streamItems == null) return
|
||||||
val trending = runCatching {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
RetrofitInstance.api.getTrending(region).deArrow().take(10)
|
|
||||||
}
|
|
||||||
}.getOrNull()?.takeIf { it.isNotEmpty() } ?: return
|
|
||||||
val binding = _binding ?: return
|
|
||||||
|
|
||||||
makeVisible(binding.trendingRV, binding.trendingTV)
|
makeVisible(binding.trendingRV, binding.trendingTV)
|
||||||
binding.trendingRV.layoutManager = GridLayoutManager(context, 2)
|
binding.trendingRV.layoutManager = GridLayoutManager(context, 2)
|
||||||
binding.trendingRV.adapter = VideosAdapter(
|
binding.trendingRV.adapter = VideosAdapter(
|
||||||
trending.toMutableList(),
|
streamItems.toMutableList(),
|
||||||
forceMode = VideosAdapter.Companion.LayoutMode.TRENDING_ROW
|
forceMode = LayoutMode.TRENDING_ROW
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadFeed() {
|
private fun showFeed(streamItems: List<StreamItem>?) {
|
||||||
val savedFeed = subscriptionsViewModel.videoFeed.value
|
if (streamItems == null) return
|
||||||
val feed = if (
|
|
||||||
PreferenceHelper.getBoolean(PreferenceKeys.SAVE_FEED, false) &&
|
|
||||||
!savedFeed.isNullOrEmpty()
|
|
||||||
) {
|
|
||||||
savedFeed
|
|
||||||
} else {
|
|
||||||
runCatching {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
SubscriptionHelper.getFeed()
|
|
||||||
}
|
|
||||||
}.getOrNull()?.takeIf { it.isNotEmpty() } ?: return
|
|
||||||
}
|
|
||||||
|
|
||||||
val allowShorts = ContentFilter.SHORTS.isEnabled()
|
|
||||||
val allowVideos = ContentFilter.VIDEOS.isEnabled()
|
|
||||||
val allowAll = (!allowShorts && !allowVideos)
|
|
||||||
|
|
||||||
var filteredFeed = feed.filter {
|
|
||||||
(allowShorts && it.isShort) || (allowVideos && !it.isShort) || allowAll
|
|
||||||
}
|
|
||||||
if (PreferenceHelper.getBoolean(PreferenceKeys.HIDE_WATCHED_FROM_FEED, false)) {
|
|
||||||
filteredFeed = runBlocking { DatabaseHelper.filterUnwatched(filteredFeed) }
|
|
||||||
}
|
|
||||||
val binding = _binding ?: return
|
|
||||||
|
|
||||||
makeVisible(binding.featuredRV, binding.featuredTV)
|
makeVisible(binding.featuredRV, binding.featuredTV)
|
||||||
binding.featuredRV.layoutManager = LinearLayoutManager(
|
val feedVideos = streamItems.take(20).toMutableList()
|
||||||
context,
|
with (binding.featuredRV) {
|
||||||
LinearLayoutManager.HORIZONTAL,
|
layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
|
||||||
false
|
adapter = VideosAdapter(feedVideos, forceMode = LayoutMode.RELATED_COLUMN)
|
||||||
)
|
}
|
||||||
binding.featuredRV.adapter = VideosAdapter(
|
|
||||||
filteredFeed.take(20).toMutableList(),
|
|
||||||
forceMode = VideosAdapter.Companion.LayoutMode.RELATED_COLUMN
|
|
||||||
)
|
|
||||||
binding.featuredRV.setHasFixedSize(true)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadBookmarks() {
|
private fun showBookmarks(bookmarks: List<PlaylistBookmark>?) {
|
||||||
val bookmarkedPlaylists = withContext(Dispatchers.IO) {
|
if (bookmarks == null) return
|
||||||
DatabaseHolder.Database.playlistBookmarkDao().getAll()
|
|
||||||
}.takeIf { it.isNotEmpty() } ?: return
|
|
||||||
val binding = _binding ?: return
|
|
||||||
|
|
||||||
makeVisible(binding.bookmarksTV, binding.bookmarksRV)
|
makeVisible(binding.bookmarksTV, binding.bookmarksRV)
|
||||||
binding.bookmarksRV.layoutManager = LinearLayoutManager(
|
with (binding.bookmarksRV) {
|
||||||
context,
|
layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
|
||||||
LinearLayoutManager.HORIZONTAL,
|
adapter = PlaylistBookmarkAdapter(bookmarks.toMutableList())
|
||||||
false
|
}
|
||||||
)
|
|
||||||
binding.bookmarksRV.adapter = PlaylistBookmarkAdapter(
|
|
||||||
bookmarkedPlaylists,
|
|
||||||
PlaylistBookmarkAdapter.Companion.BookmarkMode.HOME
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadPlaylists() {
|
private fun showPlaylists(playlists: List<Playlists>?) {
|
||||||
val playlists = runCatching {
|
if (playlists == null) return
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
PlaylistsHelper.getPlaylists().take(20)
|
|
||||||
}
|
|
||||||
}.getOrNull()?.takeIf { it.isNotEmpty() } ?: return
|
|
||||||
val binding = _binding ?: return
|
|
||||||
|
|
||||||
makeVisible(binding.playlistsRV, binding.playlistsTV)
|
makeVisible(binding.playlistsRV, binding.playlistsTV)
|
||||||
binding.playlistsRV.layoutManager = LinearLayoutManager(context)
|
binding.playlistsRV.layoutManager = LinearLayoutManager(context)
|
||||||
binding.playlistsRV.adapter = PlaylistsAdapter(
|
binding.playlistsRV.adapter = PlaylistsAdapter(
|
||||||
playlists.toMutableList(),
|
playlists.toMutableList(),
|
||||||
PlaylistsHelper.getPrivatePlaylistType()
|
playlistType = PlaylistsHelper.getPrivatePlaylistType()
|
||||||
)
|
)
|
||||||
binding.playlistsRV.adapter?.registerAdapterDataObserver(object :
|
binding.playlistsRV.adapter?.registerAdapterDataObserver(object :
|
||||||
RecyclerView.AdapterDataObserver() {
|
RecyclerView.AdapterDataObserver() {
|
||||||
@ -216,46 +192,72 @@ class HomeFragment : Fragment() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun loadVideosToContinueWatching() {
|
private fun showContinueWatching(unwatchedVideos: List<StreamItem>?) {
|
||||||
if (!PlayerHelper.watchHistoryEnabled) return
|
if (unwatchedVideos == null) return
|
||||||
|
|
||||||
val videos = withContext(Dispatchers.IO) {
|
|
||||||
DatabaseHolder.Database.watchHistoryDao().getAll()
|
|
||||||
}
|
|
||||||
val unwatchedVideos = DatabaseHelper.filterUnwatched(videos.map { it.toStreamItem() })
|
|
||||||
.reversed()
|
|
||||||
.take(20)
|
|
||||||
if (unwatchedVideos.isEmpty()) return
|
|
||||||
val binding = _binding ?: return
|
|
||||||
|
|
||||||
makeVisible(binding.watchingRV, binding.watchingTV)
|
makeVisible(binding.watchingRV, binding.watchingTV)
|
||||||
binding.watchingRV.layoutManager = LinearLayoutManager(
|
binding.watchingRV.layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
|
||||||
context,
|
|
||||||
LinearLayoutManager.HORIZONTAL,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
binding.watchingRV.adapter = VideosAdapter(
|
binding.watchingRV.adapter = VideosAdapter(
|
||||||
unwatchedVideos.toMutableList(),
|
unwatchedVideos.toMutableList(),
|
||||||
forceMode = VideosAdapter.Companion.LayoutMode.RELATED_COLUMN
|
forceMode = LayoutMode.RELATED_COLUMN
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateLoading(isLoading: Boolean) {
|
||||||
|
if (isLoading) {
|
||||||
|
showLoading()
|
||||||
|
} else {
|
||||||
|
hideLoading()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showLoading() {
|
||||||
|
binding.progress.isVisible = !binding.refresh.isRefreshing
|
||||||
|
binding.nothingHere.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideLoading() {
|
||||||
|
binding.progress.isVisible = false
|
||||||
|
binding.refresh.isRefreshing = false
|
||||||
|
|
||||||
|
val hasContent = homeViewModel.loadedSuccessfully.value == true
|
||||||
|
if (hasContent) {
|
||||||
|
showContent()
|
||||||
|
} else {
|
||||||
|
showNothingHere()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showNothingHere() {
|
||||||
|
binding.nothingHere.isVisible = true
|
||||||
|
binding.scroll.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showContent() {
|
||||||
|
binding.nothingHere.isVisible = false
|
||||||
|
binding.scroll.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showChangeInstanceSnackBar() {
|
||||||
|
val root = _binding?.root ?: return
|
||||||
|
Snackbar
|
||||||
|
.make(root, R.string.suggest_change_instance, Snackbar.LENGTH_LONG)
|
||||||
|
.apply {
|
||||||
|
setAction(R.string.change) {
|
||||||
|
redirectToIntentSettings()
|
||||||
|
}
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun redirectToIntentSettings() {
|
||||||
|
val settingsIntent = Intent(context, SettingsActivity::class.java).apply {
|
||||||
|
putExtra(SettingsActivity.REDIRECT_KEY, SettingsActivity.REDIRECT_TO_INTENT_SETTINGS)
|
||||||
|
}
|
||||||
|
startActivity(settingsIntent)
|
||||||
|
}
|
||||||
|
|
||||||
private fun makeVisible(vararg views: View) {
|
private fun makeVisible(vararg views: View) {
|
||||||
views.forEach {
|
views.forEach { it.isVisible = true }
|
||||||
it.isVisible = true
|
|
||||||
}
|
|
||||||
val binding = _binding ?: return
|
|
||||||
binding.progress.isGone = true
|
|
||||||
binding.scroll.isVisible = true
|
|
||||||
binding.refresh.isRefreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// The values of the preference entries for the home tab content
|
|
||||||
private const val FEATURED = "featured"
|
|
||||||
private const val WATCHING = "watching"
|
|
||||||
private const val TRENDING = "trending"
|
|
||||||
private const val BOOKMARKS = "bookmarks"
|
|
||||||
private const val PLAYLISTS = "playlists"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,160 @@
|
|||||||
|
package com.github.libretube.ui.models
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.github.libretube.api.PlaylistsHelper
|
||||||
|
import com.github.libretube.api.RetrofitInstance
|
||||||
|
import com.github.libretube.api.SubscriptionHelper
|
||||||
|
import com.github.libretube.api.obj.Playlists
|
||||||
|
import com.github.libretube.api.obj.StreamItem
|
||||||
|
import com.github.libretube.constants.PreferenceKeys.HIDE_WATCHED_FROM_FEED
|
||||||
|
import com.github.libretube.constants.PreferenceKeys.SAVE_FEED
|
||||||
|
import com.github.libretube.db.DatabaseHelper
|
||||||
|
import com.github.libretube.db.DatabaseHolder
|
||||||
|
import com.github.libretube.db.obj.PlaylistBookmark
|
||||||
|
import com.github.libretube.enums.ContentFilter
|
||||||
|
import com.github.libretube.extensions.runSafely
|
||||||
|
import com.github.libretube.extensions.updateIfChanged
|
||||||
|
import com.github.libretube.helpers.LocaleHelper
|
||||||
|
import com.github.libretube.helpers.PlayerHelper
|
||||||
|
import com.github.libretube.helpers.PreferenceHelper
|
||||||
|
import com.github.libretube.util.deArrow
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.awaitAll
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class HomeViewModel: ViewModel() {
|
||||||
|
|
||||||
|
private val useSavedFeed get() = PreferenceHelper.getBoolean(SAVE_FEED, false)
|
||||||
|
private val hideWatched get() = PreferenceHelper.getBoolean(HIDE_WATCHED_FROM_FEED, false)
|
||||||
|
|
||||||
|
val trending: MutableLiveData<List<StreamItem>> = MutableLiveData(null)
|
||||||
|
|
||||||
|
val feed: MutableLiveData<List<StreamItem>> = MutableLiveData(null)
|
||||||
|
|
||||||
|
val bookmarks: MutableLiveData<List<PlaylistBookmark>> = MutableLiveData(null)
|
||||||
|
|
||||||
|
val playlists: MutableLiveData<List<Playlists>> = MutableLiveData(null)
|
||||||
|
|
||||||
|
val continueWatching: MutableLiveData<List<StreamItem>> = MutableLiveData(null)
|
||||||
|
|
||||||
|
val isLoading: MutableLiveData<Boolean> = MutableLiveData(true)
|
||||||
|
|
||||||
|
val loadedSuccessfully: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||||
|
|
||||||
|
private var loadHomeJob: Job? = null
|
||||||
|
|
||||||
|
fun loadHomeFeed(
|
||||||
|
context: Context,
|
||||||
|
savedFeed: List<StreamItem>? = null,
|
||||||
|
visibleItems: Set<String>,
|
||||||
|
onUnusualLoadTime: () -> Unit
|
||||||
|
) {
|
||||||
|
isLoading.value = true
|
||||||
|
|
||||||
|
loadHomeJob?.cancel()
|
||||||
|
loadHomeJob = viewModelScope.launch {
|
||||||
|
val result = async {
|
||||||
|
awaitAll(
|
||||||
|
async { if (visibleItems.contains(TRENDING)) loadTrending(context) },
|
||||||
|
async { if (visibleItems.contains(FEATURED)) loadFeed(savedFeed) },
|
||||||
|
async { if (visibleItems.contains(BOOKMARKS)) loadBookmarks() },
|
||||||
|
async { if (visibleItems.contains(PLAYLISTS)) loadPlaylists() },
|
||||||
|
async { if (visibleItems.contains(WATCHING)) loadVideosToContinueWatching() }
|
||||||
|
)
|
||||||
|
loadedSuccessfully.value = trending.value.isNullOrEmpty() == false
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
delay(UNUSUAL_LOAD_TIME_MS)
|
||||||
|
if (result.isActive) {
|
||||||
|
onUnusualLoadTime.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private suspend fun loadTrending(context: Context) {
|
||||||
|
val region = LocaleHelper.getTrendingRegion(context)
|
||||||
|
|
||||||
|
runSafely(
|
||||||
|
onSuccess = { videos -> trending.updateIfChanged(videos) },
|
||||||
|
ioBlock = { RetrofitInstance.api.getTrending(region).deArrow().take(10) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadFeed(savedFeed: List<StreamItem>? = null) {
|
||||||
|
runSafely(
|
||||||
|
onSuccess = { videos -> feed.updateIfChanged(videos) },
|
||||||
|
ioBlock = { tryLoadFeed(savedFeed) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadBookmarks() {
|
||||||
|
runSafely(
|
||||||
|
onSuccess = { newBookmarks -> bookmarks.updateIfChanged(newBookmarks) },
|
||||||
|
ioBlock = { DatabaseHolder.Database.playlistBookmarkDao().getAll() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadPlaylists() {
|
||||||
|
runSafely(
|
||||||
|
onSuccess = { newPlaylists -> playlists.updateIfChanged(newPlaylists) },
|
||||||
|
ioBlock = { PlaylistsHelper.getPlaylists().take(20) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadVideosToContinueWatching() {
|
||||||
|
if (!PlayerHelper.watchHistoryEnabled) return
|
||||||
|
runSafely(
|
||||||
|
onSuccess = { videos -> continueWatching.updateIfChanged(videos) },
|
||||||
|
ioBlock = ::loadWatchingFromDB
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadWatchingFromDB(): List<StreamItem> {
|
||||||
|
val videos = DatabaseHolder.Database.watchHistoryDao().getAll()
|
||||||
|
return DatabaseHelper
|
||||||
|
.filterUnwatched(videos.map { it.toStreamItem() })
|
||||||
|
.reversed()
|
||||||
|
.take(20)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun tryLoadFeed(savedFeed: List<StreamItem>?): List<StreamItem> {
|
||||||
|
val feed = if (useSavedFeed && !savedFeed.isNullOrEmpty()) {
|
||||||
|
savedFeed
|
||||||
|
} else {
|
||||||
|
SubscriptionHelper.getFeed()
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (hideWatched) feed.filterWatched() else feed
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun List<StreamItem>.filterWatched(): List<StreamItem> {
|
||||||
|
val allowShorts = ContentFilter.SHORTS.isEnabled()
|
||||||
|
val allowVideos = ContentFilter.VIDEOS.isEnabled()
|
||||||
|
val allowAll = (!allowShorts && !allowVideos)
|
||||||
|
|
||||||
|
val filteredFeed = this.filter {
|
||||||
|
allowAll || (allowShorts && it.isShort) || (allowVideos && !it.isShort)
|
||||||
|
}
|
||||||
|
return runBlocking { DatabaseHelper.filterUnwatched(filteredFeed) }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val UNUSUAL_LOAD_TIME_MS = 10000L
|
||||||
|
private const val FEATURED = "featured"
|
||||||
|
private const val WATCHING = "watching"
|
||||||
|
private const val TRENDING = "trending"
|
||||||
|
private const val BOOKMARKS = "bookmarks"
|
||||||
|
private const val PLAYLISTS = "playlists"
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progress"
|
android:id="@+id/progress"
|
||||||
@ -126,6 +127,26 @@
|
|||||||
android:text="@string/emptyList"
|
android:text="@string/emptyList"
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/refresh_button"
|
||||||
|
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="30dp"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:text="@string/retry"/>
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/change_instance"
|
||||||
|
style="@style/Widget.Material3.Button.ElevatedButton"
|
||||||
|
android:layout_width="150dp"
|
||||||
|
android:layout_height="50dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="5dp"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:text="@string/change_instance"/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
@ -301,6 +301,8 @@
|
|||||||
<string name="play_next">Play next</string>
|
<string name="play_next">Play next</string>
|
||||||
<string name="navigation_bar">Navigation bar</string>
|
<string name="navigation_bar">Navigation bar</string>
|
||||||
<string name="change_region">Trending seems to be unavailable for the current region. Please select another in the settings.</string>
|
<string name="change_region">Trending seems to be unavailable for the current region. Please select another in the settings.</string>
|
||||||
|
<string name="change_instance">Change instance</string>
|
||||||
|
<string name="suggest_change_instance">Loading is taking more than usual. Consider changing instance</string>
|
||||||
<string name="playback_pitch">Pitch</string>
|
<string name="playback_pitch">Pitch</string>
|
||||||
<string name="filename">Filename</string>
|
<string name="filename">Filename</string>
|
||||||
<string name="invalid_filename">Invalid filename!</string>
|
<string name="invalid_filename">Invalid filename!</string>
|
||||||
@ -504,6 +506,7 @@
|
|||||||
<string name="invalid_input">Invalid input</string>
|
<string name="invalid_input">Invalid input</string>
|
||||||
<string name="add_to_group">Add to group</string>
|
<string name="add_to_group">Add to group</string>
|
||||||
<string name="uptime">%.2f%% uptime</string>
|
<string name="uptime">%.2f%% uptime</string>
|
||||||
|
<string name="change">Change</string>
|
||||||
|
|
||||||
<!-- Notification channel strings -->
|
<!-- Notification channel strings -->
|
||||||
<string name="download_channel_name">Download Service</string>
|
<string name="download_channel_name">Download Service</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user