From e31943f5ab9460d7fbc9a01c8790b3ff007c04a2 Mon Sep 17 00:00:00 2001 From: RafaRamos <40279132+RafaelsRamos@users.noreply.github.com> Date: Fri, 19 Jan 2024 15:03:17 +0000 Subject: [PATCH] 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> --- .../extensions/MutableLiveDataExt.kt | 7 + .../github/libretube/extensions/RunSafely.kt | 21 ++ .../ui/activities/SettingsActivity.kt | 29 +- .../libretube/ui/fragments/HomeFragment.kt | 284 +++++++++--------- .../libretube/ui/models/HomeViewModel.kt | 160 ++++++++++ app/src/main/res/layout/fragment_home.xml | 23 +- app/src/main/res/values/strings.xml | 3 + 7 files changed, 379 insertions(+), 148 deletions(-) create mode 100644 app/src/main/java/com/github/libretube/extensions/MutableLiveDataExt.kt create mode 100644 app/src/main/java/com/github/libretube/extensions/RunSafely.kt create mode 100644 app/src/main/java/com/github/libretube/ui/models/HomeViewModel.kt diff --git a/app/src/main/java/com/github/libretube/extensions/MutableLiveDataExt.kt b/app/src/main/java/com/github/libretube/extensions/MutableLiveDataExt.kt new file mode 100644 index 000000000..51b2a374f --- /dev/null +++ b/app/src/main/java/com/github/libretube/extensions/MutableLiveDataExt.kt @@ -0,0 +1,7 @@ +package com.github.libretube.extensions + +import androidx.lifecycle.MutableLiveData + +fun MutableLiveData.updateIfChanged(newValue: T) { + if (value != newValue) value = newValue +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/extensions/RunSafely.kt b/app/src/main/java/com/github/libretube/extensions/RunSafely.kt new file mode 100644 index 000000000..66db73236 --- /dev/null +++ b/app/src/main/java/com/github/libretube/extensions/RunSafely.kt @@ -0,0 +1,21 @@ +package com.github.libretube.extensions + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +suspend fun runSafely( + onSuccess: (List) -> Unit = { }, + ioBlock: suspend () -> List, +) { + withContext(Dispatchers.IO) { + val result = runCatching { ioBlock.invoke() } + .getOrNull() + ?.takeIf { it.isNotEmpty() } ?: return@withContext + + withContext(Dispatchers.Main) { + if (result.isNotEmpty()) { + onSuccess.invoke(result) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/libretube/ui/activities/SettingsActivity.kt b/app/src/main/java/com/github/libretube/ui/activities/SettingsActivity.kt index d8eaa2e8f..05819f362 100644 --- a/app/src/main/java/com/github/libretube/ui/activities/SettingsActivity.kt +++ b/app/src/main/java/com/github/libretube/ui/activities/SettingsActivity.kt @@ -2,11 +2,13 @@ package com.github.libretube.ui.activities import android.os.Bundle import androidx.activity.addCallback +import androidx.fragment.app.Fragment import androidx.fragment.app.commit import androidx.fragment.app.replace import com.github.libretube.R import com.github.libretube.databinding.ActivitySettingsBinding import com.github.libretube.ui.base.BaseActivity +import com.github.libretube.ui.preferences.InstanceSettings import com.github.libretube.ui.preferences.MainSettings class SettingsActivity : BaseActivity() { @@ -24,9 +26,7 @@ class SettingsActivity : BaseActivity() { } if (savedInstanceState == null) { - supportFragmentManager.commit { - replace(R.id.settings) - } + redirectTo() } // 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) { finishAndRemoveTask() } else { - supportFragmentManager.commit { - replace(R.id.settings) - } + redirectTo() changeTopBarText(getString(R.string.settings)) } } + + handleRedirect() + } + + private fun handleRedirect() { + val redirectKey = intent.extras?.getString(REDIRECT_KEY) + + if (redirectKey == REDIRECT_TO_INTENT_SETTINGS) redirectTo() } fun changeTopBarText(text: String) { if (this::binding.isInitialized) binding.toolbar.title = text } + + private inline fun redirectTo() { + supportFragmentManager.commit { + replace(R.id.settings) + } + } + + companion object { + const val REDIRECT_KEY = "redirect" + const val REDIRECT_TO_INTENT_SETTINGS = "intent_settings" + } } 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 2698a0174..47899dd27 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 @@ -1,5 +1,6 @@ package com.github.libretube.ui.fragments +import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -8,42 +9,34 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL import androidx.recyclerview.widget.RecyclerView import com.github.libretube.R import com.github.libretube.api.PlaylistsHelper -import com.github.libretube.api.RetrofitInstance -import com.github.libretube.api.SubscriptionHelper -import com.github.libretube.constants.PreferenceKeys +import com.github.libretube.api.obj.Playlists +import com.github.libretube.api.obj.StreamItem +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.DatabaseHolder -import com.github.libretube.enums.ContentFilter -import com.github.libretube.helpers.LocaleHelper -import com.github.libretube.helpers.PlayerHelper +import com.github.libretube.db.obj.PlaylistBookmark 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.PlaylistsAdapter 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.util.deArrow -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext +import com.google.android.material.snackbar.Snackbar class HomeFragment : Fragment() { private var _binding: FragmentHomeBinding? = null private val binding get() = _binding!! private val subscriptionsViewModel: SubscriptionsViewModel by activityViewModels() + private val homeViewModel: HomeViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, @@ -82,7 +75,50 @@ class HomeFragment : Fragment() { 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() { @@ -91,118 +127,58 @@ class HomeFragment : Fragment() { } private fun fetchHomeFeed() { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - binding.nothingHere.isGone = true - 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() } - ) + binding.nothingHere.isGone = true + val defaultItems = resources.getStringArray(R.array.homeTabItemsValues) + val visibleItems = PreferenceHelper.getStringSet(HOME_TAB_CONTENT, defaultItems.toSet()) - val binding = _binding ?: return@repeatOnLifecycle - // No category is shown because they are either empty or disabled - if (binding.progress.isVisible) { - binding.progress.isGone = true - binding.nothingHere.isVisible = true - } - } - } + homeViewModel.loadHomeFeed( + context = requireContext(), + savedFeed = subscriptionsViewModel.videoFeed.value, + visibleItems = visibleItems, + onUnusualLoadTime = ::showChangeInstanceSnackBar + ) } - private suspend fun loadTrending() { - val region = LocaleHelper.getTrendingRegion(requireContext()) - val trending = runCatching { - withContext(Dispatchers.IO) { - RetrofitInstance.api.getTrending(region).deArrow().take(10) - } - }.getOrNull()?.takeIf { it.isNotEmpty() } ?: return - val binding = _binding ?: return + private fun showTrending(streamItems: List?) { + if (streamItems == null) return makeVisible(binding.trendingRV, binding.trendingTV) binding.trendingRV.layoutManager = GridLayoutManager(context, 2) binding.trendingRV.adapter = VideosAdapter( - trending.toMutableList(), - forceMode = VideosAdapter.Companion.LayoutMode.TRENDING_ROW + streamItems.toMutableList(), + forceMode = LayoutMode.TRENDING_ROW ) } - private suspend fun loadFeed() { - val savedFeed = subscriptionsViewModel.videoFeed.value - 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 + private fun showFeed(streamItems: List?) { + if (streamItems == null) return makeVisible(binding.featuredRV, binding.featuredTV) - binding.featuredRV.layoutManager = LinearLayoutManager( - context, - LinearLayoutManager.HORIZONTAL, - false - ) - binding.featuredRV.adapter = VideosAdapter( - filteredFeed.take(20).toMutableList(), - forceMode = VideosAdapter.Companion.LayoutMode.RELATED_COLUMN - ) - binding.featuredRV.setHasFixedSize(true) + val feedVideos = streamItems.take(20).toMutableList() + with (binding.featuredRV) { + layoutManager = LinearLayoutManager(context, HORIZONTAL, false) + adapter = VideosAdapter(feedVideos, forceMode = LayoutMode.RELATED_COLUMN) + } } - private suspend fun loadBookmarks() { - val bookmarkedPlaylists = withContext(Dispatchers.IO) { - DatabaseHolder.Database.playlistBookmarkDao().getAll() - }.takeIf { it.isNotEmpty() } ?: return - val binding = _binding ?: return + private fun showBookmarks(bookmarks: List?) { + if (bookmarks == null) return makeVisible(binding.bookmarksTV, binding.bookmarksRV) - binding.bookmarksRV.layoutManager = LinearLayoutManager( - context, - LinearLayoutManager.HORIZONTAL, - false - ) - binding.bookmarksRV.adapter = PlaylistBookmarkAdapter( - bookmarkedPlaylists, - PlaylistBookmarkAdapter.Companion.BookmarkMode.HOME - ) + with (binding.bookmarksRV) { + layoutManager = LinearLayoutManager(context, HORIZONTAL, false) + adapter = PlaylistBookmarkAdapter(bookmarks.toMutableList()) + } } - private suspend fun loadPlaylists() { - val playlists = runCatching { - withContext(Dispatchers.IO) { - PlaylistsHelper.getPlaylists().take(20) - } - }.getOrNull()?.takeIf { it.isNotEmpty() } ?: return - val binding = _binding ?: return + private fun showPlaylists(playlists: List?) { + if (playlists == null) return makeVisible(binding.playlistsRV, binding.playlistsTV) binding.playlistsRV.layoutManager = LinearLayoutManager(context) binding.playlistsRV.adapter = PlaylistsAdapter( playlists.toMutableList(), - PlaylistsHelper.getPrivatePlaylistType() + playlistType = PlaylistsHelper.getPrivatePlaylistType() ) binding.playlistsRV.adapter?.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { @@ -216,46 +192,72 @@ class HomeFragment : Fragment() { }) } - private suspend fun loadVideosToContinueWatching() { - if (!PlayerHelper.watchHistoryEnabled) 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 + private fun showContinueWatching(unwatchedVideos: List?) { + if (unwatchedVideos == null) return makeVisible(binding.watchingRV, binding.watchingTV) - binding.watchingRV.layoutManager = LinearLayoutManager( - context, - LinearLayoutManager.HORIZONTAL, - false - ) + binding.watchingRV.layoutManager = LinearLayoutManager(context, HORIZONTAL, false) binding.watchingRV.adapter = VideosAdapter( 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) { - views.forEach { - 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" + views.forEach { it.isVisible = true } } } \ No newline at end of file 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 new file mode 100644 index 000000000..3c28580ea --- /dev/null +++ b/app/src/main/java/com/github/libretube/ui/models/HomeViewModel.kt @@ -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> = MutableLiveData(null) + + val feed: MutableLiveData> = MutableLiveData(null) + + val bookmarks: MutableLiveData> = MutableLiveData(null) + + val playlists: MutableLiveData> = MutableLiveData(null) + + val continueWatching: MutableLiveData> = MutableLiveData(null) + + val isLoading: MutableLiveData = MutableLiveData(true) + + val loadedSuccessfully: MutableLiveData = MutableLiveData(false) + + private var loadHomeJob: Job? = null + + fun loadHomeFeed( + context: Context, + savedFeed: List? = null, + visibleItems: Set, + 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? = 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 { + val videos = DatabaseHolder.Database.watchHistoryDao().getAll() + return DatabaseHelper + .filterUnwatched(videos.map { it.toStreamItem() }) + .reversed() + .take(20) + } + + private suspend fun tryLoadFeed(savedFeed: List?): List { + val feed = if (useSavedFeed && !savedFeed.isNullOrEmpty()) { + savedFeed + } else { + SubscriptionHelper.getFeed() + } + + return if (hideWatched) feed.filterWatched() else feed + } + + private suspend fun List.filterWatched(): List { + 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" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 005294f13..073d32a68 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -1,7 +1,8 @@ + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 60c935a34..53fc48254 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -301,6 +301,8 @@ Play next Navigation bar Trending seems to be unavailable for the current region. Please select another in the settings. + Change instance + Loading is taking more than usual. Consider changing instance Pitch Filename Invalid filename! @@ -504,6 +506,7 @@ Invalid input Add to group %.2f%% uptime + Change Download Service