Merge pull request #3527 from Isira-Seneviratne/repeatOnLifecycle

Switch to repeatOnLifecycle extension.
This commit is contained in:
Isira Seneviratne 2023-04-12 07:14:06 +05:30 committed by GitHub
commit 2ba3ebf358
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 508 additions and 442 deletions

View File

@ -7,7 +7,9 @@ import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
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.RetrofitInstance
@ -45,33 +47,34 @@ class AddToPlaylistDialog(
} }
private fun fetchPlaylists(binding: DialogAddToPlaylistBinding) { private fun fetchPlaylists(binding: DialogAddToPlaylistBinding) {
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val response = try { repeatOnLifecycle(Lifecycle.State.CREATED) {
PlaylistsHelper.getPlaylists() val response = try {
} catch (e: Exception) { PlaylistsHelper.getPlaylists()
Log.e(TAG(), e.toString()) } catch (e: Exception) {
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show() Log.e(TAG(), e.toString())
return@launchWhenCreated Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
} return@repeatOnLifecycle
if (response.isEmpty()) return@launchWhenCreated }
val names = response.mapNotNull { it.name } if (response.isEmpty()) return@repeatOnLifecycle
val arrayAdapter = val names = response.mapNotNull { it.name }
ArrayAdapter(requireContext(), R.layout.dropdown_item, names) val arrayAdapter = ArrayAdapter(requireContext(), R.layout.dropdown_item, names)
binding.playlistsSpinner.adapter = arrayAdapter binding.playlistsSpinner.adapter = arrayAdapter
// select the last used playlist // select the last used playlist
viewModel.lastSelectedPlaylistId?.let { id -> viewModel.lastSelectedPlaylistId?.let { id ->
binding.playlistsSpinner.setSelection( binding.playlistsSpinner.setSelection(
response.indexOfFirst { it.id == id }.takeIf { it >= 0 } ?: 0 response.indexOfFirst { it.id == id }.takeIf { it >= 0 } ?: 0
) )
} }
binding.addToPlaylist.setOnClickListener { binding.addToPlaylist.setOnClickListener {
val index = binding.playlistsSpinner.selectedItemPosition val index = binding.playlistsSpinner.selectedItemPosition
viewModel.lastSelectedPlaylistId = response[index].id!! viewModel.lastSelectedPlaylistId = response[index].id!!
dialog?.hide() dialog?.hide()
lifecycleScope.launch { lifecycleScope.launch {
addToPlaylist(response[index].id!!) addToPlaylist(response[index].id!!)
dialog?.dismiss() dialog?.dismiss()
}
} }
} }
} }

View File

@ -5,7 +5,9 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.DeleteUserRequest import com.github.libretube.api.obj.DeleteUserRequest
@ -13,6 +15,9 @@ import com.github.libretube.databinding.DialogDeleteAccountBinding
import com.github.libretube.extensions.TAG import com.github.libretube.extensions.TAG
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DeleteAccountDialog( class DeleteAccountDialog(
private val onLogout: () -> Unit private val onLogout: () -> Unit
@ -39,20 +44,24 @@ class DeleteAccountDialog(
} }
private fun deleteAccount(password: String) { private fun deleteAccount(password: String) {
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val token = PreferenceHelper.getToken() repeatOnLifecycle(Lifecycle.State.CREATED) {
val token = PreferenceHelper.getToken()
try { try {
RetrofitInstance.authApi.deleteAccount(token, DeleteUserRequest(password)) withContext(Dispatchers.IO) {
} catch (e: Exception) { RetrofitInstance.authApi.deleteAccount(token, DeleteUserRequest(password))
Log.e(TAG(), e.toString()) }
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show() } catch (e: Exception) {
return@launchWhenCreated Log.e(TAG(), e.toString())
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@repeatOnLifecycle
}
Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show()
onLogout.invoke()
dialog?.dismiss()
} }
Toast.makeText(context, R.string.success, Toast.LENGTH_SHORT).show()
onLogout.invoke()
dialog?.dismiss()
} }
} }
} }

View File

@ -8,7 +8,9 @@ import android.view.View
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.api.obj.PipedStream import com.github.libretube.api.obj.PipedStream
@ -21,6 +23,9 @@ import com.github.libretube.helpers.DownloadHelper
import com.github.libretube.helpers.PreferenceHelper import com.github.libretube.helpers.PreferenceHelper
import com.github.libretube.util.TextUtils import com.github.libretube.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException import java.io.IOException
import retrofit2.HttpException import retrofit2.HttpException
@ -57,20 +62,24 @@ class DownloadDialog(
} }
private fun fetchAvailableSources(binding: DialogDownloadBinding) { private fun fetchAvailableSources(binding: DialogDownloadBinding) {
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val response = try { repeatOnLifecycle(Lifecycle.State.CREATED) {
RetrofitInstance.api.getStreams(videoId) val response = try {
} catch (e: IOException) { withContext(Dispatchers.IO) {
println(e) RetrofitInstance.api.getStreams(videoId)
Log.e(TAG(), "IOException, you might not have internet connection") }
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show() } catch (e: IOException) {
return@launchWhenCreated println(e)
} catch (e: HttpException) { Log.e(TAG(), "IOException, you might not have internet connection")
Log.e(TAG(), "HttpException, unexpected response") Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show() return@repeatOnLifecycle
return@launchWhenCreated } catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
return@repeatOnLifecycle
}
initDownloadOptions(binding, response)
} }
initDownloadOptions(binding, response)
} }
} }

View File

@ -8,7 +8,9 @@ import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.core.view.children import androidx.core.view.children
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
@ -105,93 +107,95 @@ class ChannelFragment : Fragment() {
} }
private fun fetchChannel() { private fun fetchChannel() {
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val response = try { repeatOnLifecycle(Lifecycle.State.CREATED) {
withContext(Dispatchers.IO) { val response = try {
if (channelId != null) { withContext(Dispatchers.IO) {
RetrofitInstance.api.getChannel(channelId!!) if (channelId != null) {
} else { RetrofitInstance.api.getChannel(channelId!!)
RetrofitInstance.api.getChannelByName(channelName!!) } else {
RetrofitInstance.api.getChannelByName(channelName!!)
}
}
} catch (e: IOException) {
binding.channelRefresh.isRefreshing = false
Log.e(TAG(), "IOException, you might not have internet connection")
return@repeatOnLifecycle
} catch (e: HttpException) {
binding.channelRefresh.isRefreshing = false
Log.e(TAG(), "HttpException, unexpected response")
return@repeatOnLifecycle
}
// needed if the channel gets loaded by the ID
channelId = response.id
channelName = response.name
val shareData = ShareData(currentChannel = response.name)
onScrollEnd = {
fetchChannelNextPage()
}
// fetch and update the subscription status
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
if (isSubscribed == null) return@repeatOnLifecycle
binding.channelSubscribe.setupSubscriptionButton(
channelId,
channelName,
binding.notificationBell
)
binding.channelShare.setOnClickListener {
val shareDialog = ShareDialog(
response.id!!.toID(),
ShareObjectType.CHANNEL,
shareData
)
shareDialog.show(childFragmentManager, ShareDialog::class.java.name)
}
nextPage = response.nextpage
isLoading = false
binding.channelRefresh.isRefreshing = false
binding.channelScrollView.visibility = View.VISIBLE
binding.channelName.text = response.name
if (response.verified) {
binding.channelName.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
R.drawable.ic_verified,
0
)
}
binding.channelSubs.text = resources.getString(
R.string.subscribers,
response.subscriberCount.formatShort()
)
if (response.description.isBlank()) {
binding.channelDescription.visibility = View.GONE
} else {
binding.channelDescription.text = response.description.trim()
}
binding.channelDescription.setOnClickListener {
(it as TextView).apply {
it.maxLines = if (it.maxLines == Int.MAX_VALUE) 2 else Int.MAX_VALUE
} }
} }
} catch (e: IOException) {
binding.channelRefresh.isRefreshing = false
Log.e(TAG(), "IOException, you might not have internet connection")
return@launchWhenCreated
} catch (e: HttpException) {
binding.channelRefresh.isRefreshing = false
Log.e(TAG(), "HttpException, unexpected response")
return@launchWhenCreated
}
// needed if the channel gets loaded by the ID
channelId = response.id
channelName = response.name
val shareData = ShareData(currentChannel = response.name)
onScrollEnd = { ImageHelper.loadImage(response.bannerUrl, binding.channelBanner)
fetchChannelNextPage() ImageHelper.loadImage(response.avatarUrl, binding.channelImage)
}
// fetch and update the subscription status // recyclerview of the videos by the channel
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!) channelAdapter = VideosAdapter(
if (isSubscribed == null) return@launchWhenCreated response.relatedStreams.toMutableList(),
forceMode = VideosAdapter.Companion.ForceMode.CHANNEL
binding.channelSubscribe.setupSubscriptionButton(
channelId,
channelName,
binding.notificationBell
)
binding.channelShare.setOnClickListener {
val shareDialog = ShareDialog(
response.id!!.toID(),
ShareObjectType.CHANNEL,
shareData
) )
shareDialog.show(childFragmentManager, ShareDialog::class.java.name) binding.channelRecView.adapter = channelAdapter
setupTabs(response.tabs)
} }
nextPage = response.nextpage
isLoading = false
binding.channelRefresh.isRefreshing = false
binding.channelScrollView.visibility = View.VISIBLE
binding.channelName.text = response.name
if (response.verified) {
binding.channelName.setCompoundDrawablesWithIntrinsicBounds(
0,
0,
R.drawable.ic_verified,
0
)
}
binding.channelSubs.text = resources.getString(
R.string.subscribers,
response.subscriberCount.formatShort()
)
if (response.description.isBlank()) {
binding.channelDescription.visibility = View.GONE
} else {
binding.channelDescription.text = response.description.trim()
}
binding.channelDescription.setOnClickListener {
(it as TextView).apply {
it.maxLines = if (it.maxLines == Int.MAX_VALUE) 2 else Int.MAX_VALUE
}
}
ImageHelper.loadImage(response.bannerUrl, binding.channelBanner)
ImageHelper.loadImage(response.avatarUrl, binding.channelImage)
// recyclerview of the videos by the channel
channelAdapter = VideosAdapter(
response.relatedStreams.toMutableList(),
forceMode = VideosAdapter.Companion.ForceMode.CHANNEL
)
binding.channelRecView.adapter = channelAdapter
setupTabs(response.tabs)
} }
} }
@ -253,22 +257,24 @@ class ChannelFragment : Fragment() {
} }
private fun fetchChannelNextPage() { private fun fetchChannelNextPage() {
fun run() { if (nextPage == null || isLoading) return
if (nextPage == null || isLoading) return isLoading = true
isLoading = true binding.channelRefresh.isRefreshing = true
binding.channelRefresh.isRefreshing = true
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try { val response = try {
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage!!) withContext(Dispatchers.IO) {
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage!!)
}
} catch (e: IOException) { } catch (e: IOException) {
binding.channelRefresh.isRefreshing = false binding.channelRefresh.isRefreshing = false
Log.e(TAG(), "IOException, you might not have internet connection") Log.e(TAG(), "IOException, you might not have internet connection")
return@launchWhenCreated return@repeatOnLifecycle
} catch (e: HttpException) { } catch (e: HttpException) {
binding.channelRefresh.isRefreshing = false binding.channelRefresh.isRefreshing = false
Log.e(TAG(), "HttpException, unexpected response," + e.response()) Log.e(TAG(), "HttpException, unexpected response," + e.response())
return@launchWhenCreated return@repeatOnLifecycle
} }
nextPage = response.nextpage nextPage = response.nextpage
channelAdapter?.insertItems(response.relatedStreams) channelAdapter?.insertItems(response.relatedStreams)
@ -276,7 +282,6 @@ class ChannelFragment : Fragment() {
binding.channelRefresh.isRefreshing = false binding.channelRefresh.isRefreshing = false
} }
} }
run()
} }
private fun fetchTabNextPage( private fun fetchTabNextPage(

View File

@ -6,7 +6,9 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
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.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
@ -25,6 +27,9 @@ 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.models.SubscriptionsViewModel import com.github.libretube.ui.models.SubscriptionsViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class HomeFragment : Fragment() { class HomeFragment : Fragment() {
@ -75,13 +80,15 @@ class HomeFragment : Fragment() {
} }
private fun fetchHomeFeed() { private fun fetchHomeFeed() {
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
loadTrending() repeatOnLifecycle(Lifecycle.State.CREATED) {
loadBookmarks() awaitAll(
} async { loadTrending() },
lifecycleScope.launchWhenCreated { async { loadBookmarks() },
loadFeed() async { loadFeed() },
loadPlaylists() async { loadPlaylists() }
)
}
} }
} }

View File

@ -11,7 +11,9 @@ import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
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.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -122,47 +124,49 @@ class LibraryFragment : Fragment() {
private fun fetchPlaylists() { private fun fetchPlaylists() {
binding.playlistRefresh.isRefreshing = true binding.playlistRefresh.isRefreshing = true
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
var playlists = try { repeatOnLifecycle(Lifecycle.State.CREATED) {
withContext(Dispatchers.IO) { var playlists = try {
PlaylistsHelper.getPlaylists() withContext(Dispatchers.IO) {
} PlaylistsHelper.getPlaylists()
} catch (e: Exception) { }
Log.e(TAG(), e.toString()) } catch (e: Exception) {
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show() Log.e(TAG(), e.toString())
return@launchWhenCreated Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
} finally { return@repeatOnLifecycle
binding.playlistRefresh.isRefreshing = false } finally {
} binding.playlistRefresh.isRefreshing = false
if (playlists.isNotEmpty()) { }
playlists = when ( if (playlists.isNotEmpty()) {
PreferenceHelper.getString(PreferenceKeys.PLAYLISTS_ORDER, "recent") playlists = when (
) { PreferenceHelper.getString(PreferenceKeys.PLAYLISTS_ORDER, "recent")
"recent" -> playlists ) {
"recent_reversed" -> playlists.reversed() "recent" -> playlists
"name" -> playlists.sortedBy { it.name?.lowercase() } "recent_reversed" -> playlists.reversed()
"name_reversed" -> playlists.sortedBy { it.name?.lowercase() }.reversed() "name" -> playlists.sortedBy { it.name?.lowercase() }
else -> playlists "name_reversed" -> playlists.sortedBy { it.name?.lowercase() }.reversed()
} else -> playlists
val playlistsAdapter = PlaylistsAdapter(
playlists.toMutableList(),
PlaylistsHelper.getPrivatePlaylistType()
)
// listen for playlists to become deleted
playlistsAdapter.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
binding.nothingHere.isVisible = playlistsAdapter.itemCount == 0
super.onItemRangeRemoved(positionStart, itemCount)
} }
})
binding.nothingHere.visibility = View.GONE val playlistsAdapter = PlaylistsAdapter(
binding.playlistRecView.adapter = playlistsAdapter playlists.toMutableList(),
} else { PlaylistsHelper.getPrivatePlaylistType()
binding.nothingHere.visibility = View.VISIBLE )
// listen for playlists to become deleted
playlistsAdapter.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
binding.nothingHere.isVisible = playlistsAdapter.itemCount == 0
super.onItemRangeRemoved(positionStart, itemCount)
}
})
binding.nothingHere.visibility = View.GONE
binding.playlistRecView.adapter = playlistsAdapter
} else {
binding.nothingHere.visibility = View.VISIBLE
}
} }
} }
} }

View File

@ -9,7 +9,9 @@ import android.view.ViewGroup
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
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.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -109,112 +111,109 @@ class PlaylistFragment : Fragment() {
private fun fetchPlaylist() { private fun fetchPlaylist() {
binding.playlistScrollview.visibility = View.GONE binding.playlistScrollview.visibility = View.GONE
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val response = try { repeatOnLifecycle(Lifecycle.State.CREATED) {
withContext(Dispatchers.IO) { val response = try {
PlaylistsHelper.getPlaylist(playlistId!!) withContext(Dispatchers.IO) {
} PlaylistsHelper.getPlaylist(playlistId!!)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return@launchWhenCreated
}
playlistFeed = response.relatedStreams.toMutableList()
binding.playlistScrollview.visibility = View.VISIBLE
nextPage = response.nextpage
playlistName = response.name
isLoading = false
ImageHelper.loadImage(response.thumbnailUrl, binding.thumbnail)
binding.playlistProgress.visibility = View.GONE
binding.playlistName.text = response.name
binding.playlistName.setOnClickListener {
binding.playlistName.maxLines =
if (binding.playlistName.maxLines == 2) Int.MAX_VALUE else 2
}
binding.playlistInfo.text = getChannelAndVideoString(response, response.videos)
// show playlist options
binding.optionsMenu.setOnClickListener {
PlaylistOptionsBottomSheet(
playlistId = playlistId.orEmpty(),
playlistName = playlistName.orEmpty(),
playlistType = playlistType,
onDelete = {
findNavController().popBackStack()
},
onRename = {
binding.playlistName.text = it
playlistName = it
} }
).show( } catch (e: Exception) {
childFragmentManager, Log.e(TAG(), e.toString())
PlaylistOptionsBottomSheet::class.java.name return@repeatOnLifecycle
) }
} playlistFeed = response.relatedStreams.toMutableList()
binding.playlistScrollview.visibility = View.VISIBLE
nextPage = response.nextpage
playlistName = response.name
isLoading = false
ImageHelper.loadImage(response.thumbnailUrl, binding.thumbnail)
binding.playlistProgress.visibility = View.GONE
binding.playlistName.text = response.name
binding.playAll.setOnClickListener { binding.playlistName.setOnClickListener {
if (playlistFeed.isEmpty()) return@setOnClickListener binding.playlistName.maxLines =
NavigationHelper.navigateVideo( if (binding.playlistName.maxLines == 2) Int.MAX_VALUE else 2
requireContext(), }
response.relatedStreams.first().url?.toID(),
playlistId
)
}
if (playlistType == PlaylistType.PUBLIC) { binding.playlistInfo.text = getChannelAndVideoString(response, response.videos)
binding.bookmark.setOnClickListener {
isBookmarked = !isBookmarked // show playlist options
updateBookmarkRes() binding.optionsMenu.setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) { PlaylistOptionsBottomSheet(
if (!isBookmarked) { playlistId = playlistId.orEmpty(),
DatabaseHolder.Database.playlistBookmarkDao() playlistName = playlistName.orEmpty(),
.deleteById(playlistId!!) playlistType = playlistType,
} else { onDelete = {
DatabaseHolder.Database.playlistBookmarkDao() findNavController().popBackStack()
.insert(response.toPlaylistBookmark(playlistId!!)) },
onRename = {
binding.playlistName.text = it
playlistName = it
} }
} ).show(
} childFragmentManager,
} else { PlaylistOptionsBottomSheet::class.java.name
// private playlist, means shuffle is possible because all videos are received at once
binding.bookmark.setIconResource(R.drawable.ic_shuffle)
binding.bookmark.text = getString(R.string.shuffle)
binding.bookmark.setOnClickListener {
if (playlistFeed.isEmpty()) return@setOnClickListener
val queue = playlistFeed.shuffled()
PlayingQueue.resetToDefaults()
PlayingQueue.add(*queue.toTypedArray())
NavigationHelper.navigateVideo(
requireContext(),
queue.first().url?.toID(),
playlistId = playlistId,
keepQueue = true
) )
} }
}
playlistAdapter = PlaylistAdapter( binding.playAll.setOnClickListener {
playlistFeed, if (playlistFeed.isEmpty()) return@setOnClickListener
playlistId!!, NavigationHelper.navigateVideo(
playlistType requireContext(),
) response.relatedStreams.first().url?.toID(),
playlistId
)
}
// listen for playlist items to become deleted if (playlistType == PlaylistType.PUBLIC) {
playlistAdapter!!.registerAdapterDataObserver(object : binding.bookmark.setOnClickListener {
RecyclerView.AdapterDataObserver() { isBookmarked = !isBookmarked
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { updateBookmarkRes()
if (positionStart == 0) { lifecycleScope.launch(Dispatchers.IO) {
ImageHelper.loadImage( if (!isBookmarked) {
playlistFeed.firstOrNull()?.thumbnail ?: "", DatabaseHolder.Database.playlistBookmarkDao()
binding.thumbnail .deleteById(playlistId!!)
} else {
DatabaseHolder.Database.playlistBookmarkDao()
.insert(response.toPlaylistBookmark(playlistId!!))
}
}
}
} else {
// private playlist, means shuffle is possible because all videos are received at once
binding.bookmark.setIconResource(R.drawable.ic_shuffle)
binding.bookmark.text = getString(R.string.shuffle)
binding.bookmark.setOnClickListener {
if (playlistFeed.isEmpty()) return@setOnClickListener
val queue = playlistFeed.shuffled()
PlayingQueue.resetToDefaults()
PlayingQueue.add(*queue.toTypedArray())
NavigationHelper.navigateVideo(
requireContext(),
queue.first().url?.toID(),
playlistId = playlistId,
keepQueue = true
) )
} }
binding.playlistInfo.text =
getChannelAndVideoString(response, playlistFeed.size)
} }
})
playlistAdapter = PlaylistAdapter(playlistFeed, playlistId!!, playlistType)
// listen for playlist items to become deleted
playlistAdapter!!.registerAdapterDataObserver(object :
RecyclerView.AdapterDataObserver() {
override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) {
if (positionStart == 0) {
ImageHelper.loadImage(
playlistFeed.firstOrNull()?.thumbnail ?: "",
binding.thumbnail
)
}
binding.playlistInfo.text =
getChannelAndVideoString(response, playlistFeed.size)
}
})
binding.playlistRecView.adapter = playlistAdapter binding.playlistRecView.adapter = playlistAdapter
binding.playlistScrollview.viewTreeObserver.addOnScrollChangedListener { binding.playlistScrollview.viewTreeObserver.addOnScrollChangedListener {
@ -232,41 +231,42 @@ class PlaylistFragment : Fragment() {
} }
} }
// listener for swiping to the left or right // listener for swiping to the left or right
if (playlistType != PlaylistType.PUBLIC) { if (playlistType != PlaylistType.PUBLIC) {
val itemTouchCallback = object : ItemTouchHelper.SimpleCallback( val itemTouchCallback = object : ItemTouchHelper.SimpleCallback(
0, 0,
ItemTouchHelper.LEFT ItemTouchHelper.LEFT
) { ) {
override fun onMove( override fun onMove(
recyclerView: RecyclerView, recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder, viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder target: RecyclerView.ViewHolder
): Boolean { ): Boolean {
return false return false
}
override fun onSwiped(
viewHolder: RecyclerView.ViewHolder,
direction: Int
) {
val position = viewHolder.absoluteAdapterPosition
playlistAdapter!!.removeFromPlaylist(requireContext(), position)
}
} }
override fun onSwiped( val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
viewHolder: RecyclerView.ViewHolder, itemTouchHelper.attachToRecyclerView(binding.playlistRecView)
direction: Int
) {
val position = viewHolder.absoluteAdapterPosition
playlistAdapter!!.removeFromPlaylist(requireContext(), position)
}
} }
val itemTouchHelper = ItemTouchHelper(itemTouchCallback) withContext(Dispatchers.IO) {
itemTouchHelper.attachToRecyclerView(binding.playlistRecView) // update the playlist thumbnail if bookmarked
} val playlistBookmark = DatabaseHolder.Database.playlistBookmarkDao().getAll()
.firstOrNull { it.playlistId == playlistId }
lifecycleScope.launch(Dispatchers.IO) { playlistBookmark?.let {
// update the playlist thumbnail if bookmarked if (it.thumbnailUrl != response.thumbnailUrl) {
val playlistBookmark = DatabaseHolder.Database.playlistBookmarkDao().getAll() it.thumbnailUrl = response.thumbnailUrl
.firstOrNull { it.playlistId == playlistId } DatabaseHolder.Database.playlistBookmarkDao().update(it)
playlistBookmark?.let { }
if (it.thumbnailUrl != response.thumbnailUrl) {
it.thumbnailUrl = response.thumbnailUrl
DatabaseHolder.Database.playlistBookmarkDao().update(it)
} }
} }
} }
@ -284,22 +284,26 @@ class PlaylistFragment : Fragment() {
if (nextPage == null || isLoading) return if (nextPage == null || isLoading) return
isLoading = true isLoading = true
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val response = try { repeatOnLifecycle(Lifecycle.State.CREATED) {
// load locally stored playlists with the auth api val response = try {
if (playlistType == PlaylistType.PRIVATE) { withContext(Dispatchers.IO) {
RetrofitInstance.authApi.getPlaylistNextPage(playlistId!!, nextPage!!) // load locally stored playlists with the auth api
} else { if (playlistType == PlaylistType.PRIVATE) {
RetrofitInstance.api.getPlaylistNextPage(playlistId!!, nextPage!!) RetrofitInstance.authApi.getPlaylistNextPage(playlistId!!, nextPage!!)
} else {
RetrofitInstance.api.getPlaylistNextPage(playlistId!!, nextPage!!)
}
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return@repeatOnLifecycle
} }
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return@launchWhenCreated
}
nextPage = response.nextpage nextPage = response.nextpage
playlistAdapter?.updateItems(response.relatedStreams) playlistAdapter?.updateItems(response.relatedStreams)
isLoading = false isLoading = false
}
} }
} }
} }

View File

@ -7,7 +7,9 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
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.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.databinding.FragmentSearchBinding import com.github.libretube.databinding.FragmentSearchBinding
@ -69,20 +71,24 @@ class SearchFragment : Fragment() {
} }
private fun fetchSuggestions(query: String) { private fun fetchSuggestions(query: String) {
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val response = try { repeatOnLifecycle(Lifecycle.State.CREATED) {
RetrofitInstance.api.getSuggestions(query) val response = try {
} catch (e: Exception) { withContext(Dispatchers.IO) {
Log.e(TAG(), e.toString()) RetrofitInstance.api.getSuggestions(query)
return@launchWhenCreated }
} } catch (e: Exception) {
// only load the suggestions if the input field didn't get cleared yet Log.e(TAG(), e.toString())
val suggestionsAdapter = SearchSuggestionsAdapter( return@repeatOnLifecycle
response.reversed(), }
(activity as MainActivity).searchView // only load the suggestions if the input field didn't get cleared yet
) val suggestionsAdapter = SearchSuggestionsAdapter(
if (isAdded && !viewModel.searchQuery.value.isNullOrEmpty()) { response.reversed(),
binding.suggestionsRecycler.adapter = suggestionsAdapter (activity as MainActivity).searchView
)
if (isAdded && !viewModel.searchQuery.value.isNullOrEmpty()) {
binding.suggestionsRecycler.adapter = suggestionsAdapter
}
} }
} }
} }

View File

@ -7,7 +7,9 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
@ -87,49 +89,53 @@ class SearchResultFragment : Fragment() {
} }
private fun fetchSearch() { private fun fetchSearch() {
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
view?.let { context?.hideKeyboard(it) } repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try { view?.let { context?.hideKeyboard(it) }
withContext(Dispatchers.IO) { val response = try {
RetrofitInstance.api.getSearchResults(query, apiSearchFilter) withContext(Dispatchers.IO) {
RetrofitInstance.api.getSearchResults(query, apiSearchFilter)
}
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection $e")
return@repeatOnLifecycle
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
return@repeatOnLifecycle
} }
} catch (e: IOException) { searchAdapter = SearchAdapter()
println(e) binding.searchRecycler.adapter = searchAdapter
Log.e(TAG(), "IOException, you might not have internet connection $e") searchAdapter.submitList(response.items)
return@launchWhenCreated binding.noSearchResult.isVisible = response.items.isEmpty()
} catch (e: HttpException) { nextPage = response.nextpage
Log.e(TAG(), "HttpException, unexpected response")
return@launchWhenCreated
} }
searchAdapter = SearchAdapter()
binding.searchRecycler.adapter = searchAdapter
searchAdapter.submitList(response.items)
binding.noSearchResult.isVisible = response.items.isEmpty()
nextPage = response.nextpage
} }
} }
private fun fetchNextSearchItems() { private fun fetchNextSearchItems() {
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val response = try { repeatOnLifecycle(Lifecycle.State.CREATED) {
withContext(Dispatchers.IO) { val response = try {
RetrofitInstance.api.getSearchResultsNextPage( withContext(Dispatchers.IO) {
query, RetrofitInstance.api.getSearchResultsNextPage(
apiSearchFilter, query,
nextPage!! apiSearchFilter,
) nextPage!!
)
}
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
return@repeatOnLifecycle
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response," + e.response())
return@repeatOnLifecycle
}
nextPage = response.nextpage!!
if (response.items.isNotEmpty()) {
searchAdapter.submitList(searchAdapter.currentList + response.items)
} }
} catch (e: IOException) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection")
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response," + e.response())
return@launchWhenCreated
}
nextPage = response.nextpage!!
if (response.items.isNotEmpty()) {
searchAdapter.submitList(searchAdapter.currentList + response.items)
} }
} }
} }

View File

@ -8,7 +8,9 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.github.libretube.R import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance import com.github.libretube.api.RetrofitInstance
import com.github.libretube.databinding.FragmentTrendsBinding import com.github.libretube.databinding.FragmentTrendsBinding
@ -19,6 +21,7 @@ import com.github.libretube.ui.adapters.VideosAdapter
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import java.io.IOException import java.io.IOException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import retrofit2.HttpException import retrofit2.HttpException
@ -51,40 +54,42 @@ class TrendsFragment : Fragment() {
} }
private fun fetchTrending() { private fun fetchTrending() {
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val response = try { repeatOnLifecycle(Lifecycle.State.CREATED) {
withContext(Dispatchers.IO) { val response = try {
val region = LocaleHelper.getTrendingRegion(requireContext()) withContext(Dispatchers.IO) {
RetrofitInstance.api.getTrending(region) val region = LocaleHelper.getTrendingRegion(requireContext())
} 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
}
val binding = _binding ?: return@launchWhenCreated
binding.homeRefresh.isRefreshing = false
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) {
val settingsIntent = Intent(context, SettingsActivity::class.java)
startActivity(settingsIntent)
} }
.show() } catch (e: IOException) {
return@launchWhenCreated println(e)
} Log.e(TAG(), "IOException, you might not have internet connection")
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@repeatOnLifecycle
} catch (e: HttpException) {
Log.e(TAG(), "HttpException, unexpected response")
Toast.makeText(context, R.string.server_error, Toast.LENGTH_SHORT).show()
return@repeatOnLifecycle
}
binding.recview.adapter = VideosAdapter(response.toMutableList()) val binding = _binding ?: return@repeatOnLifecycle
binding.recview.layoutManager = VideosAdapter.getLayout(requireContext()) binding.homeRefresh.isRefreshing = false
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) {
val settingsIntent = Intent(context, SettingsActivity::class.java)
startActivity(settingsIntent)
}
.show()
return@repeatOnLifecycle
}
binding.recview.adapter = VideosAdapter(response.toMutableList())
binding.recview.layoutManager = VideosAdapter.getLayout(requireContext())
}
} }
} }
} }

View File

@ -3,7 +3,9 @@ package com.github.libretube.ui.preferences
import android.os.Bundle import android.os.Bundle
import android.widget.Toast import android.widget.Toast
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
@ -123,45 +125,51 @@ class InstanceSettings : BasePreferenceFragment() {
private fun initInstancesPref(instancePrefs: List<ListPreference>) { private fun initInstancesPref(instancePrefs: List<ListPreference>) {
val appContext = requireContext().applicationContext val appContext = requireContext().applicationContext
lifecycleScope.launchWhenCreated { lifecycleScope.launch {
val customInstances = withContext(Dispatchers.IO) { repeatOnLifecycle(Lifecycle.State.CREATED) {
Database.customInstanceDao().getAll() val customInstances = withContext(Dispatchers.IO) {
} Database.customInstanceDao().getAll()
}
for (instancePref in instancePrefs) { for (instancePref in instancePrefs) {
instancePref.summaryProvider = instancePref.summaryProvider =
Preference.SummaryProvider<ListPreference> { preference -> Preference.SummaryProvider<ListPreference> { preference ->
preference.entry preference.entry
} }
} }
// fetch official public instances from kavin.rocks as well as tokhmi.xyz as fallback // fetch official public instances from kavin.rocks as well as tokhmi.xyz as
val instances = withContext(Dispatchers.IO) { // fallback
runCatching { val instances = withContext(Dispatchers.IO) {
RetrofitInstance.externalApi.getInstances(PIPED_INSTANCES_URL).toMutableList() runCatching {
}.getOrNull() ?: runCatching { RetrofitInstance.externalApi.getInstances(PIPED_INSTANCES_URL)
RetrofitInstance.externalApi.getInstances(FALLBACK_INSTANCES_URL).toMutableList() .toMutableList()
}.getOrNull() ?: run { }.getOrNull() ?: runCatching {
appContext.toastFromMainDispatcher(R.string.failed_fetching_instances) RetrofitInstance.externalApi.getInstances(FALLBACK_INSTANCES_URL)
val instanceNames = resources.getStringArray(R.array.instances) .toMutableList()
resources.getStringArray(R.array.instancesValue).mapIndexed { index, instanceValue -> }.getOrNull() ?: run {
Instances(instanceNames[index], instanceValue) appContext.toastFromMainDispatcher(R.string.failed_fetching_instances)
val instanceNames = resources.getStringArray(R.array.instances)
resources.getStringArray(R.array.instancesValue)
.mapIndexed { index, instanceValue ->
Instances(instanceNames[index], instanceValue)
}
} }
} }
} .sortedBy { it.name }
.sortedBy { it.name } .toMutableList()
.toMutableList()
instances.addAll(customInstances.map { Instances(it.name, it.apiUrl) }) instances.addAll(customInstances.map { Instances(it.name, it.apiUrl) })
for (instancePref in instancePrefs) { for (instancePref in instancePrefs) {
// add custom instances to the list preference // add custom instances to the list preference
instancePref.entries = instances.map { it.name }.toTypedArray() instancePref.entries = instances.map { it.name }.toTypedArray()
instancePref.entryValues = instances.map { it.apiUrl }.toTypedArray() instancePref.entryValues = instances.map { it.apiUrl }.toTypedArray()
instancePref.summaryProvider = instancePref.summaryProvider =
Preference.SummaryProvider<ListPreference> { preference -> Preference.SummaryProvider<ListPreference> { preference ->
preference.entry preference.entry
} }
}
} }
} }
} }