From bfd72c8714cb36b4a2c0282c8f6ec76d2f575f8e Mon Sep 17 00:00:00 2001 From: dhanuarf <10323845+dhanuarf@users.noreply.github.com> Date: Sun, 26 Oct 2025 09:11:31 +0700 Subject: [PATCH 1/5] refactor(trending): group streams based on region --- .../com/github/libretube/ui/fragments/HomeFragment.kt | 3 ++- .../com/github/libretube/ui/fragments/TrendsFragment.kt | 7 ++++--- .../com/github/libretube/ui/models/TrendsViewModel.kt | 8 +++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt index b2dc77f7bf..60af682f8f 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt @@ -208,7 +208,8 @@ class HomeFragment : Fragment(R.layout.fragment_home) { // cache the loaded trends in the [TrendsViewModel] so that the trends don't need to be // reloaded there - trendsViewModel.setStreamsForCategory(category, streamItems) + val region = PreferenceHelper.getTrendingRegion(requireContext()) + trendsViewModel.setStreamsForCategory(category, TrendsViewModel.TrendingStreams(region, streamItems)) makeVisible(binding.trendingRV, binding.trendingTV) trendingAdapter.submitList(streamItems.take(10)) diff --git a/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt index 9025ef7a49..e455bbaa0f 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt @@ -90,9 +90,9 @@ class TrendsContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_tre binding.homeRefresh.isRefreshing = false binding.progressBar.isGone = true - adapter.submitList(videos) + adapter.submitList(videos.streams) - if (videos.isEmpty()) { + if (videos.streams.isEmpty()) { Snackbar.make(binding.root, R.string.change_region, Snackbar.LENGTH_LONG) .setAction(R.string.settings) { val settingsIntent = Intent(context, SettingsActivity::class.java) @@ -119,7 +119,8 @@ class TrendsContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_tre // every time the user navigates to the fragment for the selected category, // fetch the trends for the selected category if they're not yet cached repeatOnLifecycle(Lifecycle.State.RESUMED) { - if (viewModel.trendingVideos.value.orEmpty()[category].isNullOrEmpty()) { + val trendingVideos = viewModel.trendingVideos.value.orEmpty()[category] + if (trendingVideos == null) { viewModel.fetchTrending(requireContext(), category) } } diff --git a/app/src/main/java/com/github/libretube/ui/models/TrendsViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/TrendsViewModel.kt index 6237c33843..4790cc8855 100644 --- a/app/src/main/java/com/github/libretube/ui/models/TrendsViewModel.kt +++ b/app/src/main/java/com/github/libretube/ui/models/TrendsViewModel.kt @@ -18,7 +18,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class TrendsViewModel : ViewModel() { - val trendingVideos = MutableLiveData>>() + data class TrendingStreams(val region: String, val streams: List) + + val trendingVideos = MutableLiveData>() var recyclerViewState: Parcelable? = null private var currentJob: Job? = null @@ -34,7 +36,7 @@ class TrendsViewModel : ViewModel() { val response = withContext(Dispatchers.IO) { MediaServiceRepository.instance.getTrending(region, category) } - setStreamsForCategory(category, response) + setStreamsForCategory(category, TrendingStreams(region, response)) } catch (e: Exception) { Log.e(TAG(), e.stackTraceToString()) context.toastFromMainDispatcher(e.localizedMessage.orEmpty()) @@ -42,7 +44,7 @@ class TrendsViewModel : ViewModel() { } } - fun setStreamsForCategory(category: TrendingCategory, streams: List) { + fun setStreamsForCategory(category: TrendingCategory, streams: TrendingStreams) { val newState = trendingVideos.value.orEmpty() .toMutableMap() .apply { From bb57986fd2b758ade3ff4f507b95b18e227b92fd Mon Sep 17 00:00:00 2001 From: dhanuarf <10323845+dhanuarf@users.noreply.github.com> Date: Sun, 26 Oct 2025 12:43:10 +0700 Subject: [PATCH 2/5] feat(TrendsFragment): add 'change region' button --- .../libretube/ui/fragments/TrendsFragment.kt | 93 +++++++++++++++---- app/src/main/res/layout/fragment_trends.xml | 26 +++++- .../res/layout/fragment_trends_content.xml | 18 ++-- 3 files changed, 109 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt index e455bbaa0f..6af941da87 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt @@ -1,6 +1,6 @@ package com.github.libretube.ui.fragments -import android.content.Intent +import android.content.Context import android.content.res.Configuration import android.os.Bundle import android.view.View @@ -18,13 +18,16 @@ import com.github.libretube.R import com.github.libretube.api.MediaServiceRepository import com.github.libretube.api.TrendingCategory import com.github.libretube.constants.IntentData +import com.github.libretube.constants.PreferenceKeys import com.github.libretube.databinding.FragmentTrendsBinding import com.github.libretube.databinding.FragmentTrendsContentBinding import com.github.libretube.extensions.serializable -import com.github.libretube.ui.activities.SettingsActivity +import com.github.libretube.helpers.LocaleHelper +import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.ui.adapters.VideoCardsAdapter import com.github.libretube.ui.base.DynamicLayoutManagerFragment import com.github.libretube.ui.models.TrendsViewModel +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayoutMediator import kotlinx.coroutines.launch @@ -37,30 +40,71 @@ class TrendsFragment : Fragment(R.layout.fragment_trends) { _binding = FragmentTrendsBinding.bind(view) val categories = MediaServiceRepository.instance.getTrendingCategories() - binding.pager.adapter = TrendsAdapter(this, categories) + + val adapter = TrendsAdapter(this, categories) + binding.pager.adapter = adapter if (categories.size <= 1) binding.tabLayout.isGone = true TabLayoutMediator(binding.tabLayout, binding.pager) { tab, position -> val category = categories[position] tab.text = getString(category.titleRes) }.attach() + + binding.trendingRegion.setOnClickListener { + showChangeRegionDialog(requireContext()) { + adapter.getFragmentAt(binding.pager.currentItem)?.also { + it.refreshTrending() + } + } + } + } + + companion object{ + fun showChangeRegionDialog(context: Context, onPositiveButtonClick: () -> Unit){ + val currentRegionPref = PreferenceHelper.getTrendingRegion(context) + + val countries = LocaleHelper.getAvailableCountries() + var selected = countries.indexOfFirst { it.code == currentRegionPref } + MaterialAlertDialogBuilder(context) + .setTitle(R.string.region) + .setSingleChoiceItems( + countries.map { it.name }.toTypedArray(), + selected + ) { _, checked -> + selected = checked + } + .setNegativeButton(R.string.cancel, null) + .setPositiveButton(R.string.okay) { _, _ -> + PreferenceHelper.putString(PreferenceKeys.REGION, countries[selected].code) + onPositiveButtonClick() + } + .show() + } } } class TrendsAdapter(fragment: Fragment, private val categories: List) : FragmentStateAdapter(fragment) { + private val fragments: MutableList = + MutableList(categories.size) { null } + override fun createFragment(position: Int): Fragment { - return TrendsContentFragment().apply { + val trendContentFragment = TrendsContentFragment().apply { arguments = bundleOf( IntentData.category to categories[position] ) } + fragments[position] = trendContentFragment + return trendContentFragment } override fun getItemCount(): Int { return categories.size } + fun getFragmentAt(position: Int): TrendsContentFragment?{ + return fragments[position] + } } class TrendsContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_trends_content) { @@ -68,6 +112,9 @@ class TrendsContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_tre private val binding get() = _binding!! private val viewModel: TrendsViewModel by activityViewModels() + private var _category: TrendingCategory? = null + private val category get() = _category!! + override fun setLayoutManagers(gridItems: Int) { _binding?.recview?.layoutManager = GridLayoutManager(context, gridItems) } @@ -76,7 +123,7 @@ class TrendsContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_tre _binding = FragmentTrendsContentBinding.bind(view) super.onViewCreated(view, savedInstanceState) - val category = requireArguments() + _category = requireArguments() .serializable(IntentData.category)!! val adapter = VideoCardsAdapter() @@ -87,16 +134,17 @@ class TrendsContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_tre val videos = categoryMap[category] if (videos == null) return@observe - binding.homeRefresh.isRefreshing = false - binding.progressBar.isGone = true + showLoadingIndicator(false) adapter.submitList(videos.streams) - if (videos.streams.isEmpty()) { - Snackbar.make(binding.root, R.string.change_region, Snackbar.LENGTH_LONG) - .setAction(R.string.settings) { - val settingsIntent = Intent(context, SettingsActivity::class.java) - startActivity(settingsIntent) + val trendingRegion = PreferenceHelper.getTrendingRegion(requireContext()) + if (videos.streams.isEmpty() && (videos.region == trendingRegion)) { + Snackbar.make(requireParentFragment().requireView(), R.string.change_region, Snackbar.LENGTH_LONG) + .setAction(R.string.change) { + TrendsFragment.showChangeRegionDialog(requireContext()) { + refreshTrending() + } } .show() } @@ -104,7 +152,7 @@ class TrendsContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_tre binding.homeRefresh.isEnabled = true binding.homeRefresh.setOnRefreshListener { - viewModel.fetchTrending(requireContext(), category) + refreshTrending() } binding.recview.addOnScrollListener(object : RecyclerView.OnScrollListener() { @@ -117,11 +165,13 @@ class TrendsContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_tre viewModel.fetchTrending(requireContext(), category) lifecycleScope.launch { // every time the user navigates to the fragment for the selected category, - // fetch the trends for the selected category if they're not yet cached + // fetch the trends for the selected category if they're not yet cached or if the value + // for trending region has been changed repeatOnLifecycle(Lifecycle.State.RESUMED) { + val trendingRegion = PreferenceHelper.getTrendingRegion(requireContext()) val trendingVideos = viewModel.trendingVideos.value.orEmpty()[category] - if (trendingVideos == null) { - viewModel.fetchTrending(requireContext(), category) + if (trendingVideos == null || (trendingVideos.region != trendingRegion)) { + refreshTrending() } } } @@ -137,4 +187,15 @@ class TrendsContentFragment : DynamicLayoutManagerFragment(R.layout.fragment_tre super.onDestroyView() _binding = null } + + private fun showLoadingIndicator(show: Boolean){ + binding.recview.alpha = if(show) 0.3f else 1.0f + binding.progressBar.isGone = !show + if (!show) binding.homeRefresh.isRefreshing = false + } + + fun refreshTrending(){ + showLoadingIndicator(true) + viewModel.fetchTrending(requireContext(), category) + } } diff --git a/app/src/main/res/layout/fragment_trends.xml b/app/src/main/res/layout/fragment_trends.xml index 0d6a69995c..ff4d6f24fa 100644 --- a/app/src/main/res/layout/fragment_trends.xml +++ b/app/src/main/res/layout/fragment_trends.xml @@ -5,11 +5,31 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"> - + android:orientation="horizontal" + android:gravity="center_vertical"> + + + + + + - - + + \ No newline at end of file From eeeb8ce72981654fd0cba0046ace47b2c7fcd673 Mon Sep 17 00:00:00 2001 From: dhanuarf <10323845+dhanuarf@users.noreply.github.com> Date: Sun, 26 Oct 2025 13:28:22 +0700 Subject: [PATCH 3/5] feat(HomeFragment): update feed if trend region has changed --- .../libretube/ui/fragments/HomeFragment.kt | 22 ++++++++++++++----- .../libretube/ui/models/HomeViewModel.kt | 10 +++++++-- app/src/main/res/layout/fragment_home.xml | 14 ++++++------ 3 files changed, 31 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt index 60af682f8f..9153e33fee 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt @@ -178,8 +178,13 @@ class HomeFragment : Fragment(R.layout.fragment_home) { override fun onResume() { super.onResume() - // Avoid re-fetching when re-entering the screen if it was loaded successfully - if (homeViewModel.loadedSuccessfully.value == false) { + // Avoid re-fetching when re-entering the screen if it was loaded successfully, except when + // the value of trending region has changed + val isTrendingRegionChanged = homeViewModel.trending.value?.let { + it.second.region != PreferenceHelper.getTrendingRegion(requireContext()) + } == true + + if (homeViewModel.loadedSuccessfully.value == false || isTrendingRegionChanged) { fetchHomeFeed() } } @@ -202,17 +207,20 @@ class HomeFragment : Fragment(R.layout.fragment_home) { ) } - private fun showTrending(trends: Pair>?) { + private fun showTrending(trends: Pair?) { if (trends == null) return - val (category, streamItems) = trends + val (category, trendingStreams) = trends // cache the loaded trends in the [TrendsViewModel] so that the trends don't need to be // reloaded there val region = PreferenceHelper.getTrendingRegion(requireContext()) - trendsViewModel.setStreamsForCategory(category, TrendsViewModel.TrendingStreams(region, streamItems)) + trendsViewModel.setStreamsForCategory( + category, + TrendsViewModel.TrendingStreams(region, trendingStreams.streams) + ) makeVisible(binding.trendingRV, binding.trendingTV) - trendingAdapter.submitList(streamItems.take(10)) + trendingAdapter.submitList(trendingStreams.streams.take(10)) } private fun showFeed(streamItems: List?) { @@ -271,6 +279,7 @@ class HomeFragment : Fragment(R.layout.fragment_home) { private fun showLoading() { binding.progress.isVisible = !binding.refresh.isRefreshing binding.nothingHere.isVisible = false + binding.scroll.alpha = 0.3f } private fun hideLoading() { @@ -283,6 +292,7 @@ class HomeFragment : Fragment(R.layout.fragment_home) { } else { showNothingHere() } + binding.scroll.alpha = 1.0f } private fun showNothingHere() { diff --git a/app/src/main/java/com/github/libretube/ui/models/HomeViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/HomeViewModel.kt index ba3ea7a32d..4882a00751 100644 --- a/app/src/main/java/com/github/libretube/ui/models/HomeViewModel.kt +++ b/app/src/main/java/com/github/libretube/ui/models/HomeViewModel.kt @@ -31,7 +31,8 @@ import kotlinx.coroutines.withContext class HomeViewModel : ViewModel() { private val hideWatched get() = PreferenceHelper.getBoolean(HIDE_WATCHED_FROM_FEED, false) - val trending: MutableLiveData>> = MutableLiveData(null) + val trending: MutableLiveData> = + MutableLiveData(null) val feed: MutableLiveData> = MutableLiveData(null) val bookmarks: MutableLiveData> = MutableLiveData(null) val playlists: MutableLiveData> = MutableLiveData(null) @@ -82,7 +83,12 @@ class HomeViewModel : ViewModel() { runSafely( onSuccess = { videos -> - trending.updateIfChanged(Pair(category, videos)) + trending.updateIfChanged( + Pair( + category, + TrendsViewModel.TrendingStreams(region, videos) + ) + ) }, ioBlock = { MediaServiceRepository.instance.getTrending(region, category) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index e1553004ac..b447f8ff06 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -5,13 +5,6 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - - + + \ No newline at end of file From 61311ea7a322ec70c057292866c4c063a3c407bd Mon Sep 17 00:00:00 2001 From: Bnyro Date: Sun, 26 Oct 2025 10:37:45 +0100 Subject: [PATCH 4/5] fix: re-use existing method for showing trending region dialog --- .../libretube/ui/fragments/HomeFragment.kt | 22 +++---------------- .../libretube/ui/fragments/TrendsFragment.kt | 22 +++++++++++-------- 2 files changed, 16 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt index 9153e33fee..034d027d64 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt @@ -19,7 +19,6 @@ import com.github.libretube.constants.PreferenceKeys.HOME_TAB_CONTENT import com.github.libretube.databinding.FragmentHomeBinding import com.github.libretube.db.DatabaseHelper import com.github.libretube.db.obj.PlaylistBookmark -import com.github.libretube.helpers.LocaleHelper import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.ui.activities.SettingsActivity import com.github.libretube.ui.adapters.CarouselPlaylist @@ -114,24 +113,9 @@ class HomeFragment : Fragment(R.layout.fragment_home) { } binding.trendingRegion.setOnClickListener { - val currentRegionPref = PreferenceHelper.getTrendingRegion(requireContext()) - - val countries = LocaleHelper.getAvailableCountries() - var selected = countries.indexOfFirst { it.code == currentRegionPref } - MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.region) - .setSingleChoiceItems( - countries.map { it.name }.toTypedArray(), - selected - ) { _, checked -> - selected = checked - } - .setNegativeButton(R.string.cancel, null) - .setPositiveButton(R.string.okay) { _, _ -> - PreferenceHelper.putString(PreferenceKeys.REGION, countries[selected].code) - fetchHomeFeed() - } - .show() + TrendsFragment.showChangeRegionDialog(requireContext()) { + fetchHomeFeed() + } } val trendingCategories = MediaServiceRepository.instance.getTrendingCategories() diff --git a/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt index 6af941da87..5e674eaa06 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt @@ -59,8 +59,8 @@ class TrendsFragment : Fragment(R.layout.fragment_trends) { } } - companion object{ - fun showChangeRegionDialog(context: Context, onPositiveButtonClick: () -> Unit){ + companion object { + fun showChangeRegionDialog(context: Context, onPositiveButtonClick: () -> Unit) { val currentRegionPref = PreferenceHelper.getTrendingRegion(context) val countries = LocaleHelper.getAvailableCountries() @@ -102,7 +102,7 @@ class TrendsAdapter(fragment: Fragment, private val categories: List Date: Sun, 26 Oct 2025 10:40:55 +0100 Subject: [PATCH 5/5] feat: add tooltips for trending region and category buttons --- app/src/main/res/layout/fragment_home.xml | 6 ++++-- app/src/main/res/layout/fragment_trends.xml | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index b447f8ff06..3f8c445993 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -102,7 +102,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - app:icon="@drawable/ic_frame" /> + app:icon="@drawable/ic_frame" + android:tooltipText="@string/category"/> + app:icon="@drawable/ic_region" + android:tooltipText="@string/region"/> diff --git a/app/src/main/res/layout/fragment_trends.xml b/app/src/main/res/layout/fragment_trends.xml index ff4d6f24fa..de6d7acf4e 100644 --- a/app/src/main/res/layout/fragment_trends.xml +++ b/app/src/main/res/layout/fragment_trends.xml @@ -27,7 +27,8 @@ android:layout_marginHorizontal="10dp" android:layout_marginVertical="4dp" app:icon="@drawable/ic_region" - app:iconSize="24dp" /> + app:iconSize="24dp" + android:tooltipText="@string/region" />