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
@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")
suspend fun renamePlaylist(

View File

@ -14,3 +14,13 @@ fun Context.toastFromMainThread(stringId: Int) {
).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")) {
"home" ->
navController.navigate(R.id.homeFragment)
"trends" ->
navController.navigate(R.id.trendsFragment)
"subscriptions" ->
navController.navigate(R.id.subscriptionsFragment)
"library" ->

View File

@ -50,7 +50,7 @@ class AddToPlaylistDialog : DialogFragment() {
private fun fetchPlaylists() {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.authApi.playlists(token)
RetrofitInstance.authApi.getUserPlaylists(token)
} catch (e: IOException) {
println(e)
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
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.fragment.app.activityViewModels
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.api.RetrofitInstance
import com.github.libretube.constants.PreferenceKeys
import com.github.libretube.databinding.FragmentHomeBinding
import com.github.libretube.extensions.TAG
import com.github.libretube.ui.activities.SettingsActivity
import com.github.libretube.ui.adapters.PlaylistsAdapter
import com.github.libretube.ui.adapters.VideosAdapter
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.PreferenceHelper
import com.google.android.material.snackbar.Snackbar
import retrofit2.HttpException
import java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class HomeFragment : BaseFragment() {
private lateinit var binding: FragmentHomeBinding
private lateinit var region: String
private val viewModel: HomeModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
@ -37,71 +35,65 @@ class HomeFragment : BaseFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val regionPref = PreferenceHelper.getString(PreferenceKeys.REGION, "sys")
// get the system default country if auto region selected
region = if (regionPref == "sys") {
LocaleHelper
.getDetectedCountry(requireContext(), "UK")
.uppercase()
} else {
regionPref
binding.featuredTV.setOnClickListener {
findNavController().navigate(R.id.subscriptionsFragment)
}
fetchTrending()
binding.homeRefresh.isEnabled = true
binding.homeRefresh.setOnRefreshListener {
fetchTrending()
binding.trendingTV.setOnClickListener {
findNavController().navigate(R.id.trendsFragment)
}
}
private fun fetchTrending() {
lifecycleScope.launchWhenCreated {
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
binding.playlistsTV.setOnClickListener {
findNavController().navigate(R.id.libraryFragment)
}
// 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
)
)
lifecycleScope.launch(Dispatchers.IO) {
viewModel.fetchHome(requireContext(), LocaleHelper.getTrendingRegion(requireContext()))
}
viewModel.feed.observe(viewLifecycleOwner) {
binding.featuredTV.visibility = View.VISIBLE
binding.featuredRV.visibility = View.VISIBLE
binding.progress.visibility = View.GONE
binding.featuredRV.layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
binding.featuredRV.adapter = VideosAdapter(
it.toMutableList(),
childFragmentManager,
forceMode = VideosAdapter.Companion.ForceMode.RELATED
)
}
viewModel.trending.observe(viewLifecycleOwner) {
if (it.isEmpty()) return@observe
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
)
}
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
}
.show()
return@runOnUiThread
}
binding.recview.adapter = VideosAdapter(
response.toMutableList(),
childFragmentManager
)
binding.recview.layoutManager = VideosAdapter.getLayout(requireContext())
}
}
})
}
}
}

View File

@ -99,7 +99,7 @@ class LibraryFragment : BaseFragment() {
binding.playlistRefresh.isRefreshing = true
lifecycleScope.launchWhenCreated {
var playlists = try {
RetrofitInstance.authApi.playlists(token)
RetrofitInstance.authApi.getUserPlaylists(token)
} catch (e: IOException) {
println(e)
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
}
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"?>
<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"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.fragments.HomeFragment">
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:id="@+id/progress"
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" />
android:layout_height="match_parent"
android:layout_gravity="center" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/home_refresh"
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content">
<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" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="10dp">
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<TextView
android:id="@+id/featuredTV"
style="@style/HomeCategoryTitle"
android:text="@string/featured" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/featuredRV"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:nestedScrollingEnabled="false"
android:visibility="gone" />
<TextView
android:id="@+id/trendingTV"
style="@style/HomeCategoryTitle"
android:text="@string/trending" />
<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:title="@string/startpage" />
<item
android:visible="false"
android:id="@+id/trendsFragment"
android:icon="@drawable/ic_trending"
android:title="@string/trends" />
<item
android:id="@+id/subscriptionsFragment"
android:icon="@drawable/ic_subscriptions"
@ -22,10 +28,4 @@
android:icon="@drawable/ic_download_filled"
android:title="@string/downloads" />
<item
android:id="@+id/watchHistoryFragment"
android:visible="false"
android:icon="@drawable/ic_history_filled"
android:title="@string/history" />
</menu>

View File

@ -4,12 +4,16 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.github.libretube.ui.fragments.HomeFragment"
android:label="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
android:id="@+id/subscriptionsFragment"
android:name="com.github.libretube.ui.fragments.SubscriptionsFragment"

View File

@ -378,6 +378,9 @@
<string name="auto_quality">Auto</string>
<string name="limit_to_runtime">Limit to runtime</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 -->
<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>
</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>

View File

@ -12,6 +12,16 @@
android:targetPackage="com.github.libretube"
android:targetClass="com.github.libretube.ui.activities.MainActivity" />
</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
android:shortcutId="subscriptions"
android:enabled="true"