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

View File

@ -5,7 +5,9 @@ import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
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.helpers.PreferenceHelper
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DeleteAccountDialog(
private val onLogout: () -> Unit
@ -39,20 +44,24 @@ class DeleteAccountDialog(
}
private fun deleteAccount(password: String) {
lifecycleScope.launchWhenCreated {
val token = PreferenceHelper.getToken()
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val token = PreferenceHelper.getToken()
try {
RetrofitInstance.authApi.deleteAccount(token, DeleteUserRequest(password))
} catch (e: Exception) {
Log.e(TAG(), e.toString())
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
try {
withContext(Dispatchers.IO) {
RetrofitInstance.authApi.deleteAccount(token, DeleteUserRequest(password))
}
} catch (e: Exception) {
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.Toast
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
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.util.TextUtils
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.IOException
import retrofit2.HttpException
@ -57,20 +62,24 @@ class DownloadDialog(
}
private fun fetchAvailableSources(binding: DialogDownloadBinding) {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.api.getStreams(videoId)
} 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
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getStreams(videoId)
}
} 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@repeatOnLifecycle
} 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 androidx.core.view.children
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
@ -105,93 +107,95 @@ class ChannelFragment : Fragment() {
}
private fun fetchChannel() {
lifecycleScope.launchWhenCreated {
val response = try {
withContext(Dispatchers.IO) {
if (channelId != null) {
RetrofitInstance.api.getChannel(channelId!!)
} else {
RetrofitInstance.api.getChannelByName(channelName!!)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try {
withContext(Dispatchers.IO) {
if (channelId != null) {
RetrofitInstance.api.getChannel(channelId!!)
} 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 = {
fetchChannelNextPage()
}
ImageHelper.loadImage(response.bannerUrl, binding.channelBanner)
ImageHelper.loadImage(response.avatarUrl, binding.channelImage)
// fetch and update the subscription status
isSubscribed = SubscriptionHelper.isSubscribed(channelId!!)
if (isSubscribed == null) return@launchWhenCreated
binding.channelSubscribe.setupSubscriptionButton(
channelId,
channelName,
binding.notificationBell
)
binding.channelShare.setOnClickListener {
val shareDialog = ShareDialog(
response.id!!.toID(),
ShareObjectType.CHANNEL,
shareData
// recyclerview of the videos by the channel
channelAdapter = VideosAdapter(
response.relatedStreams.toMutableList(),
forceMode = VideosAdapter.Companion.ForceMode.CHANNEL
)
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() {
fun run() {
if (nextPage == null || isLoading) return
isLoading = true
binding.channelRefresh.isRefreshing = true
if (nextPage == null || isLoading) return
isLoading = true
binding.channelRefresh.isRefreshing = true
lifecycleScope.launchWhenCreated {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try {
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage!!)
withContext(Dispatchers.IO) {
RetrofitInstance.api.getChannelNextPage(channelId!!, nextPage!!)
}
} catch (e: IOException) {
binding.channelRefresh.isRefreshing = false
Log.e(TAG(), "IOException, you might not have internet connection")
return@launchWhenCreated
return@repeatOnLifecycle
} catch (e: HttpException) {
binding.channelRefresh.isRefreshing = false
Log.e(TAG(), "HttpException, unexpected response," + e.response())
return@launchWhenCreated
return@repeatOnLifecycle
}
nextPage = response.nextpage
channelAdapter?.insertItems(response.relatedStreams)
@ -276,7 +282,6 @@ class ChannelFragment : Fragment() {
binding.channelRefresh.isRefreshing = false
}
}
run()
}
private fun fetchTabNextPage(

View File

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

View File

@ -11,7 +11,9 @@ import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
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.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -122,47 +124,49 @@ class LibraryFragment : Fragment() {
private fun fetchPlaylists() {
binding.playlistRefresh.isRefreshing = true
lifecycleScope.launchWhenCreated {
var playlists = try {
withContext(Dispatchers.IO) {
PlaylistsHelper.getPlaylists()
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@launchWhenCreated
} finally {
binding.playlistRefresh.isRefreshing = false
}
if (playlists.isNotEmpty()) {
playlists = when (
PreferenceHelper.getString(PreferenceKeys.PLAYLISTS_ORDER, "recent")
) {
"recent" -> playlists
"recent_reversed" -> playlists.reversed()
"name" -> playlists.sortedBy { it.name?.lowercase() }
"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)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
var playlists = try {
withContext(Dispatchers.IO) {
PlaylistsHelper.getPlaylists()
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
return@repeatOnLifecycle
} finally {
binding.playlistRefresh.isRefreshing = false
}
if (playlists.isNotEmpty()) {
playlists = when (
PreferenceHelper.getString(PreferenceKeys.PLAYLISTS_ORDER, "recent")
) {
"recent" -> playlists
"recent_reversed" -> playlists.reversed()
"name" -> playlists.sortedBy { it.name?.lowercase() }
"name_reversed" -> playlists.sortedBy { it.name?.lowercase() }.reversed()
else -> playlists
}
})
binding.nothingHere.visibility = View.GONE
binding.playlistRecView.adapter = playlistsAdapter
} else {
binding.nothingHere.visibility = View.VISIBLE
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
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.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.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
@ -109,112 +111,109 @@ class PlaylistFragment : Fragment() {
private fun fetchPlaylist() {
binding.playlistScrollview.visibility = View.GONE
lifecycleScope.launchWhenCreated {
val response = try {
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
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try {
withContext(Dispatchers.IO) {
PlaylistsHelper.getPlaylist(playlistId!!)
}
).show(
childFragmentManager,
PlaylistOptionsBottomSheet::class.java.name
)
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
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 {
if (playlistFeed.isEmpty()) return@setOnClickListener
NavigationHelper.navigateVideo(
requireContext(),
response.relatedStreams.first().url?.toID(),
playlistId
)
}
binding.playlistName.setOnClickListener {
binding.playlistName.maxLines =
if (binding.playlistName.maxLines == 2) Int.MAX_VALUE else 2
}
if (playlistType == PlaylistType.PUBLIC) {
binding.bookmark.setOnClickListener {
isBookmarked = !isBookmarked
updateBookmarkRes()
lifecycleScope.launch(Dispatchers.IO) {
if (!isBookmarked) {
DatabaseHolder.Database.playlistBookmarkDao()
.deleteById(playlistId!!)
} else {
DatabaseHolder.Database.playlistBookmarkDao()
.insert(response.toPlaylistBookmark(playlistId!!))
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
}
}
}
} 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
).show(
childFragmentManager,
PlaylistOptionsBottomSheet::class.java.name
)
}
}
playlistAdapter = PlaylistAdapter(
playlistFeed,
playlistId!!,
playlistType
)
binding.playAll.setOnClickListener {
if (playlistFeed.isEmpty()) return@setOnClickListener
NavigationHelper.navigateVideo(
requireContext(),
response.relatedStreams.first().url?.toID(),
playlistId
)
}
// 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
if (playlistType == PlaylistType.PUBLIC) {
binding.bookmark.setOnClickListener {
isBookmarked = !isBookmarked
updateBookmarkRes()
lifecycleScope.launch(Dispatchers.IO) {
if (!isBookmarked) {
DatabaseHolder.Database.playlistBookmarkDao()
.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.playlistScrollview.viewTreeObserver.addOnScrollChangedListener {
@ -232,41 +231,42 @@ class PlaylistFragment : Fragment() {
}
}
// listener for swiping to the left or right
if (playlistType != PlaylistType.PUBLIC) {
val itemTouchCallback = object : ItemTouchHelper.SimpleCallback(
0,
ItemTouchHelper.LEFT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
// listener for swiping to the left or right
if (playlistType != PlaylistType.PUBLIC) {
val itemTouchCallback = object : ItemTouchHelper.SimpleCallback(
0,
ItemTouchHelper.LEFT
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(
viewHolder: RecyclerView.ViewHolder,
direction: Int
) {
val position = viewHolder.absoluteAdapterPosition
playlistAdapter!!.removeFromPlaylist(requireContext(), position)
}
}
override fun onSwiped(
viewHolder: RecyclerView.ViewHolder,
direction: Int
) {
val position = viewHolder.absoluteAdapterPosition
playlistAdapter!!.removeFromPlaylist(requireContext(), position)
}
val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
itemTouchHelper.attachToRecyclerView(binding.playlistRecView)
}
val itemTouchHelper = ItemTouchHelper(itemTouchCallback)
itemTouchHelper.attachToRecyclerView(binding.playlistRecView)
}
lifecycleScope.launch(Dispatchers.IO) {
// update the playlist thumbnail if bookmarked
val playlistBookmark = DatabaseHolder.Database.playlistBookmarkDao().getAll()
.firstOrNull { it.playlistId == playlistId }
playlistBookmark?.let {
if (it.thumbnailUrl != response.thumbnailUrl) {
it.thumbnailUrl = response.thumbnailUrl
DatabaseHolder.Database.playlistBookmarkDao().update(it)
withContext(Dispatchers.IO) {
// update the playlist thumbnail if bookmarked
val playlistBookmark = DatabaseHolder.Database.playlistBookmarkDao().getAll()
.firstOrNull { it.playlistId == playlistId }
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
isLoading = true
lifecycleScope.launchWhenCreated {
val response = try {
// load locally stored playlists with the auth api
if (playlistType == PlaylistType.PRIVATE) {
RetrofitInstance.authApi.getPlaylistNextPage(playlistId!!, nextPage!!)
} else {
RetrofitInstance.api.getPlaylistNextPage(playlistId!!, nextPage!!)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try {
withContext(Dispatchers.IO) {
// load locally stored playlists with the auth api
if (playlistType == PlaylistType.PRIVATE) {
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
playlistAdapter?.updateItems(response.relatedStreams)
isLoading = false
nextPage = response.nextpage
playlistAdapter?.updateItems(response.relatedStreams)
isLoading = false
}
}
}
}

View File

@ -7,7 +7,9 @@ import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.api.RetrofitInstance
import com.github.libretube.databinding.FragmentSearchBinding
@ -69,20 +71,24 @@ class SearchFragment : Fragment() {
}
private fun fetchSuggestions(query: String) {
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.api.getSuggestions(query)
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return@launchWhenCreated
}
// only load the suggestions if the input field didn't get cleared yet
val suggestionsAdapter = SearchSuggestionsAdapter(
response.reversed(),
(activity as MainActivity).searchView
)
if (isAdded && !viewModel.searchQuery.value.isNullOrEmpty()) {
binding.suggestionsRecycler.adapter = suggestionsAdapter
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getSuggestions(query)
}
} catch (e: Exception) {
Log.e(TAG(), e.toString())
return@repeatOnLifecycle
}
// only load the suggestions if the input field didn't get cleared yet
val suggestionsAdapter = SearchSuggestionsAdapter(
response.reversed(),
(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 androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
@ -87,49 +89,53 @@ class SearchResultFragment : Fragment() {
}
private fun fetchSearch() {
lifecycleScope.launchWhenCreated {
view?.let { context?.hideKeyboard(it) }
val response = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getSearchResults(query, apiSearchFilter)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
view?.let { context?.hideKeyboard(it) }
val response = try {
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) {
println(e)
Log.e(TAG(), "IOException, you might not have internet connection $e")
return@launchWhenCreated
} catch (e: HttpException) {
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
}
searchAdapter = SearchAdapter()
binding.searchRecycler.adapter = searchAdapter
searchAdapter.submitList(response.items)
binding.noSearchResult.isVisible = response.items.isEmpty()
nextPage = response.nextpage
}
}
private fun fetchNextSearchItems() {
lifecycleScope.launchWhenCreated {
val response = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getSearchResultsNextPage(
query,
apiSearchFilter,
nextPage!!
)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try {
withContext(Dispatchers.IO) {
RetrofitInstance.api.getSearchResultsNextPage(
query,
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.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.github.libretube.R
import com.github.libretube.api.RetrofitInstance
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 java.io.IOException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.HttpException
@ -51,40 +54,42 @@ class TrendsFragment : Fragment() {
}
private fun fetchTrending() {
lifecycleScope.launchWhenCreated {
val response = try {
withContext(Dispatchers.IO) {
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)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val response = try {
withContext(Dispatchers.IO) {
val region = LocaleHelper.getTrendingRegion(requireContext())
RetrofitInstance.api.getTrending(region)
}
.show()
return@launchWhenCreated
}
} 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@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())
binding.recview.layoutManager = VideosAdapter.getLayout(requireContext())
val binding = _binding ?: return@repeatOnLifecycle
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.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.SwitchPreferenceCompat
@ -123,45 +125,51 @@ class InstanceSettings : BasePreferenceFragment() {
private fun initInstancesPref(instancePrefs: List<ListPreference>) {
val appContext = requireContext().applicationContext
lifecycleScope.launchWhenCreated {
val customInstances = withContext(Dispatchers.IO) {
Database.customInstanceDao().getAll()
}
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
val customInstances = withContext(Dispatchers.IO) {
Database.customInstanceDao().getAll()
}
for (instancePref in instancePrefs) {
instancePref.summaryProvider =
Preference.SummaryProvider<ListPreference> { preference ->
preference.entry
}
}
for (instancePref in instancePrefs) {
instancePref.summaryProvider =
Preference.SummaryProvider<ListPreference> { preference ->
preference.entry
}
}
// fetch official public instances from kavin.rocks as well as tokhmi.xyz as fallback
val instances = withContext(Dispatchers.IO) {
runCatching {
RetrofitInstance.externalApi.getInstances(PIPED_INSTANCES_URL).toMutableList()
}.getOrNull() ?: runCatching {
RetrofitInstance.externalApi.getInstances(FALLBACK_INSTANCES_URL).toMutableList()
}.getOrNull() ?: run {
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)
// fetch official public instances from kavin.rocks as well as tokhmi.xyz as
// fallback
val instances = withContext(Dispatchers.IO) {
runCatching {
RetrofitInstance.externalApi.getInstances(PIPED_INSTANCES_URL)
.toMutableList()
}.getOrNull() ?: runCatching {
RetrofitInstance.externalApi.getInstances(FALLBACK_INSTANCES_URL)
.toMutableList()
}.getOrNull() ?: run {
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 }
.toMutableList()
.sortedBy { it.name }
.toMutableList()
instances.addAll(customInstances.map { Instances(it.name, it.apiUrl) })
instances.addAll(customInstances.map { Instances(it.name, it.apiUrl) })
for (instancePref in instancePrefs) {
// add custom instances to the list preference
instancePref.entries = instances.map { it.name }.toTypedArray()
instancePref.entryValues = instances.map { it.apiUrl }.toTypedArray()
instancePref.summaryProvider =
Preference.SummaryProvider<ListPreference> { preference ->
preference.entry
}
for (instancePref in instancePrefs) {
// add custom instances to the list preference
instancePref.entries = instances.map { it.name }.toTypedArray()
instancePref.entryValues = instances.map { it.apiUrl }.toTypedArray()
instancePref.summaryProvider =
Preference.SummaryProvider<ListPreference> { preference ->
preference.entry
}
}
}
}
}