Merge pull request #1886 from Bnyro/master

New Home Page UI
This commit is contained in:
Bnyro 2022-11-17 19:02:26 +01:00 committed by GitHub
commit 505ecd6a82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 379 additions and 105 deletions

View File

@ -144,7 +144,7 @@ interface PipedApi {
): PlaylistId ): PlaylistId
@GET("user/playlists") @GET("user/playlists")
suspend fun playlists(@Header("Authorization") token: String): List<Playlists> suspend fun getUserPlaylists(@Header("Authorization") token: String): List<Playlists>
@POST("user/playlists/rename") @POST("user/playlists/rename")
suspend fun renamePlaylist( suspend fun renamePlaylist(

View File

@ -14,3 +14,13 @@ fun Context.toastFromMainThread(stringId: Int) {
).show() ).show()
} }
} }
fun Context.toastFromMainThread(text: String) {
Handler(Looper.getMainLooper()).post {
Toast.makeText(
this,
text,
Toast.LENGTH_SHORT
).show()
}
}

View File

@ -314,6 +314,8 @@ class MainActivity : BaseActivity() {
when (intent?.getStringExtra("fragmentToOpen")) { when (intent?.getStringExtra("fragmentToOpen")) {
"home" -> "home" ->
navController.navigate(R.id.homeFragment) navController.navigate(R.id.homeFragment)
"trends" ->
navController.navigate(R.id.trendsFragment)
"subscriptions" -> "subscriptions" ->
navController.navigate(R.id.subscriptionsFragment) navController.navigate(R.id.subscriptionsFragment)
"library" -> "library" ->

View File

@ -50,7 +50,7 @@ class AddToPlaylistDialog : DialogFragment() {
private fun fetchPlaylists() { private fun fetchPlaylists() {
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
val response = try { val response = try {
RetrofitInstance.authApi.playlists(token) RetrofitInstance.authApi.getUserPlaylists(token)
} catch (e: IOException) { } catch (e: IOException) {
println(e) println(e)
Log.e(TAG(), "IOException, you might not have internet connection") Log.e(TAG(), "IOException, you might not have internet connection")

View File

@ -0,0 +1,5 @@
package com.github.libretube.ui.extensions
fun <T> List<T>.withMaxSize(maxSize: Int): List<T> {
return this.filterIndexed { index, _ -> index < maxSize }
}

View File

@ -1,30 +1,28 @@
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.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentHomeBinding import com.github.libretube.databinding.FragmentHomeBinding
import com.github.libretube.extensions.TAG import com.github.libretube.ui.adapters.PlaylistsAdapter
import com.github.libretube.ui.activities.SettingsActivity
import com.github.libretube.ui.adapters.VideosAdapter import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.BaseFragment import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.ui.models.HomeModel
import com.github.libretube.util.LocaleHelper import com.github.libretube.util.LocaleHelper
import com.github.libretube.util.PreferenceHelper import kotlinx.coroutines.Dispatchers
import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
class HomeFragment : BaseFragment() { class HomeFragment : BaseFragment() {
private lateinit var binding: FragmentHomeBinding private lateinit var binding: FragmentHomeBinding
private lateinit var region: String private val viewModel: HomeModel by activityViewModels()
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -37,71 +35,65 @@ class HomeFragment : BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
val regionPref = PreferenceHelper.getString(PreferenceKeys.REGION, "sys")
// get the system default country if auto region selected binding.featuredTV.setOnClickListener {
region = if (regionPref == "sys") { findNavController().navigate(R.id.subscriptionsFragment)
LocaleHelper
.getDetectedCountry(requireContext(), "UK")
.uppercase()
} else {
regionPref
} }
fetchTrending() binding.trendingTV.setOnClickListener {
binding.homeRefresh.isEnabled = true findNavController().navigate(R.id.trendsFragment)
binding.homeRefresh.setOnRefreshListener {
fetchTrending()
}
} }
private fun fetchTrending() { binding.playlistsTV.setOnClickListener {
lifecycleScope.launchWhenCreated { findNavController().navigate(R.id.libraryFragment)
val response = try {
RetrofitInstance.api.getTrending(region)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
} finally {
binding.homeRefresh.isRefreshing = false
} }
runOnUiThread {
binding.progressBar.visibility = View.GONE
// show a [SnackBar] if there are no trending videos available lifecycleScope.launch(Dispatchers.IO) {
if (response.isEmpty()) { viewModel.fetchHome(requireContext(), LocaleHelper.getTrendingRegion(requireContext()))
Snackbar.make( }
binding.root,
R.string.change_region, viewModel.feed.observe(viewLifecycleOwner) {
Snackbar.LENGTH_LONG binding.featuredTV.visibility = View.VISIBLE
) binding.featuredRV.visibility = View.VISIBLE
.setAction( binding.progress.visibility = View.GONE
R.string.settings binding.featuredRV.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
) { binding.featuredRV.adapter = VideosAdapter(
startActivity( it.toMutableList(),
Intent( childFragmentManager,
context, forceMode = VideosAdapter.Companion.ForceMode.RELATED
SettingsActivity::class.java
)
) )
} }
.show()
return@runOnUiThread
}
binding.recview.adapter = VideosAdapter( viewModel.trending.observe(viewLifecycleOwner) {
response.toMutableList(), if (it.isEmpty()) return@observe
childFragmentManager binding.trendingTV.visibility = View.VISIBLE
binding.trendingRV.visibility = View.VISIBLE
binding.progress.visibility = View.GONE
binding.trendingRV.layoutManager = GridLayoutManager(context, 2)
binding.trendingRV.adapter = VideosAdapter(
it.toMutableList(),
childFragmentManager,
forceMode = VideosAdapter.Companion.ForceMode.TRENDING
) )
}
binding.recview.layoutManager = VideosAdapter.getLayout(requireContext()) viewModel.playlists.observe(viewLifecycleOwner) {
} if (it.isEmpty()) return@observe
binding.playlistsRV.visibility = View.VISIBLE
binding.playlistsTV.visibility = View.VISIBLE
binding.progress.visibility = View.GONE
binding.playlistsRV.layoutManager = LinearLayoutManager(context)
binding.playlistsRV.adapter = PlaylistsAdapter(it.toMutableList(), childFragmentManager)
binding.playlistsRV.adapter?.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
super.onItemRangeRemoved(positionStart, itemCount)
if (itemCount == 0) {
binding.playlistsRV.visibility = View.GONE
binding.playlistsTV.visibility = View.GONE
}
}
})
} }
} }
} }

View File

@ -99,7 +99,7 @@ class LibraryFragment : BaseFragment() {
binding.playlistRefresh.isRefreshing = true binding.playlistRefresh.isRefreshing = true
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
var playlists = try { var playlists = try {
RetrofitInstance.authApi.playlists(token) RetrofitInstance.authApi.getUserPlaylists(token)
} catch (e: IOException) { } catch (e: IOException) {
println(e) println(e)
Log.e(TAG(), "IOException, you might not have internet connection") Log.e(TAG(), "IOException, you might not have internet connection")

View File

@ -0,0 +1,96 @@
package com.github.libretube.ui.fragments
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.databinding.FragmentTrendsBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.ui.activities.SettingsActivity
import com.github.libretube.ui.adapters.VideosAdapter
import com.github.libretube.ui.base.BaseFragment
import com.github.libretube.util.LocaleHelper
import com.google.android.material.snackbar.Snackbar
import retrofit2.HttpException
import java.io.IOException
class TrendsFragment : BaseFragment() {
private lateinit var binding: FragmentTrendsBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentTrendsBinding.inflate(layoutInflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fetchTrending()
binding.homeRefresh.isEnabled = true
binding.homeRefresh.setOnRefreshListener {
fetchTrending()
}
}
private fun fetchTrending() {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.api.getTrending(
LocaleHelper.getTrendingRegion(requireContext())
)
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
} finally {
binding.homeRefresh.isRefreshing = false
}
runOnUiThread {
binding.progressBar.visibility = View.GONE
// show a [SnackBar] if there are no trending videos available
if (response.isEmpty()) {
Snackbar.make(
binding.root,
R.string.change_region,
Snackbar.LENGTH_LONG
)
.setAction(
R.string.settings
) {
startActivity(
Intent(
context,
SettingsActivity::class.java
)
)
}
.show()
return@runOnUiThread
}
binding.recview.adapter = VideosAdapter(
response.toMutableList(),
childFragmentManager
)
binding.recview.layoutManager = VideosAdapter.getLayout(requireContext())
}
}
}
}

View File

@ -0,0 +1,57 @@
package com.github.libretube.ui.models
import android.content.Context
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.Playlists
import com.github.libretube.api.obj.StreamItem
import com.github.libretube.extensions.toastFromMainThread
import com.github.libretube.ui.extensions.withMaxSize
import com.github.libretube.util.PreferenceHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class HomeModel : ViewModel() {
val feed = MutableLiveData<List<StreamItem>>()
var trending = MutableLiveData<List<StreamItem>>()
val playlists = MutableLiveData<List<Playlists>>()
suspend fun fetchHome(context: Context, trendingRegion: String) {
val token = PreferenceHelper.getToken()
val appContext = context.applicationContext
runOrError(appContext) {
if (trending.value.isNullOrEmpty()) {
trending.postValue(
RetrofitInstance.api.getTrending(trendingRegion).withMaxSize(10)
)
}
}
runOrError(appContext) {
if (feed.value.isNullOrEmpty()) {
feed.postValue(
RetrofitInstance.authApi.getFeed(token).withMaxSize(20)
)
}
}
runOrError(appContext) {
if (token == "" || !playlists.value.isNullOrEmpty()) return@runOrError
playlists.postValue(
RetrofitInstance.authApi.getUserPlaylists(token).withMaxSize(20)
)
}
}
private fun runOrError(context: Context, action: suspend () -> Unit) {
CoroutineScope(Dispatchers.IO).launch {
try {
action.invoke()
} catch (e: Exception) {
e.localizedMessage?.let { context.toastFromMainThread(it) }
}
}
}
}

View File

@ -126,4 +126,16 @@ object LocaleHelper {
} }
return locales return locales
} }
fun getTrendingRegion(context: Context): String {
val regionPref = PreferenceHelper.getString(PreferenceKeys.REGION, "sys")
// get the system default country if auto region selected
return if (regionPref == "sys") {
getDetectedCountry(context, "UK")
.uppercase()
} else {
regionPref
}
}
} }

View File

@ -1,35 +1,71 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragments.HomeFragment">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/home_refresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progress"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="10dp">
<TextView
android:id="@+id/featuredTV"
style="@style/HomeCategoryTitle"
android:text="@string/featured" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recview" android:id="@+id/featuredRV"
android:layout_width="0dp" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent" android:layout_marginHorizontal="10dp"
app:layout_constraintLeft_toLeftOf="parent" android:nestedScrollingEnabled="false"
app:layout_constraintRight_toRightOf="parent" android:visibility="gone" />
app:layout_constraintTop_toBottomOf="@+id/progressBar"
app:layout_constraintTop_toTopOf="parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> <TextView
android:id="@+id/trendingTV"
style="@style/HomeCategoryTitle"
android:text="@string/trending" />
</androidx.constraintlayout.widget.ConstraintLayout> <RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:descendantFocusability="blocksDescendants">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/trendingRV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
</RelativeLayout>
<TextView
android:id="@+id/playlistsTV"
style="@style/HomeCategoryTitle"
android:text="@string/playlists" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/playlistsRV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
</LinearLayout>
</ScrollView>
</FrameLayout>

View File

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragments.TrendsFragment">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/home_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/progressBar"
app:layout_constraintTop_toTopOf="parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -6,6 +6,12 @@
android:icon="@drawable/ic_home" android:icon="@drawable/ic_home"
android:title="@string/startpage" /> android:title="@string/startpage" />
<item
android:visible="false"
android:id="@+id/trendsFragment"
android:icon="@drawable/ic_trending"
android:title="@string/trends" />
<item <item
android:id="@+id/subscriptionsFragment" android:id="@+id/subscriptionsFragment"
android:icon="@drawable/ic_subscriptions" android:icon="@drawable/ic_subscriptions"
@ -22,10 +28,4 @@
android:icon="@drawable/ic_download_filled" android:icon="@drawable/ic_download_filled"
android:title="@string/downloads" /> android:title="@string/downloads" />
<item
android:id="@+id/watchHistoryFragment"
android:visible="false"
android:icon="@drawable/ic_history_filled"
android:title="@string/history" />
</menu> </menu>

View File

@ -4,12 +4,16 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav" android:id="@+id/nav"
app:startDestination="@id/homeFragment"> app:startDestination="@id/homeFragment">
<fragment <fragment
android:id="@+id/homeFragment" android:id="@+id/homeFragment"
android:name="com.github.libretube.ui.fragments.HomeFragment" android:name="com.github.libretube.ui.fragments.HomeFragment"
android:label="fragment_home" android:label="fragment_home"
tools:layout="@layout/fragment_home" /> tools:layout="@layout/fragment_home" />
<fragment
android:id="@+id/trendsFragment"
android:name="com.github.libretube.ui.fragments.TrendsFragment"
android:label="fragment_trends"
tools:layout="@layout/fragment_trends" />
<fragment <fragment
android:id="@+id/subscriptionsFragment" android:id="@+id/subscriptionsFragment"
android:name="com.github.libretube.ui.fragments.SubscriptionsFragment" android:name="com.github.libretube.ui.fragments.SubscriptionsFragment"

View File

@ -378,6 +378,9 @@
<string name="auto_quality">Auto</string> <string name="auto_quality">Auto</string>
<string name="limit_to_runtime">Limit to runtime</string> <string name="limit_to_runtime">Limit to runtime</string>
<string name="open_queue_from_notification">Open queue from notification</string> <string name="open_queue_from_notification">Open queue from notification</string>
<string name="trends">Trends</string>
<string name="featured">Featured</string>
<string name="trending">What\'s trending now</string>
<!-- Notification channel strings --> <!-- Notification channel strings -->
<string name="download_channel_name">Download Service</string> <string name="download_channel_name">Download Service</string>

View File

@ -206,4 +206,16 @@
<item name="android:drawableTint" tools:targetApi="m">?android:attr/textColorPrimary</item> <item name="android:drawableTint" tools:targetApi="m">?android:attr/textColorPrimary</item>
</style> </style>
<style name="HomeCategoryTitle">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">20sp</item>
<item name="android:textColor">?attr/colorControlNormal</item>
<item name="android:padding">15dp</item>
<item name="android:background">?attr/selectableItemBackground</item>
<item name="android:visibility">gone</item>
</style>
</resources> </resources>

View File

@ -12,6 +12,16 @@
android:targetPackage="com.github.libretube" android:targetPackage="com.github.libretube"
android:targetClass="com.github.libretube.ui.activities.MainActivity" /> android:targetClass="com.github.libretube.ui.activities.MainActivity" />
</shortcut> </shortcut>
<shortcut
android:shortcutId="trends"
android:enabled="true"
android:icon="@drawable/ic_trending"
android:shortcutShortLabel="@string/trends">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.github.libretube"
android:targetClass="com.github.libretube.ui.activities.MainActivity" />
</shortcut>
<shortcut <shortcut
android:shortcutId="subscriptions" android:shortcutId="subscriptions"
android:enabled="true" android:enabled="true"