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:
RafaRamos 2024-01-19 15:03:17 +00:00 committed by GitHub
parent 3cb99a712d
commit e31943f5ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 379 additions and 148 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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