diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/AddToPlaylistDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/AddToPlaylistDialog.kt index 2c56bcb7b..cdaeb60cf 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/AddToPlaylistDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/AddToPlaylistDialog.kt @@ -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() + } } } } diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/DeleteAccountDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/DeleteAccountDialog.kt index 0919b10bd..80580e952 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/DeleteAccountDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/DeleteAccountDialog.kt @@ -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() } } } diff --git a/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt b/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt index f7a586892..d0a3ac816 100644 --- a/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt +++ b/app/src/main/java/com/github/libretube/ui/dialogs/DownloadDialog.kt @@ -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) } } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt index 2252540fd..067491541 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/ChannelFragment.kt @@ -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( diff --git a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt index f81b8a691..560458f1b 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/HomeFragment.kt @@ -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() } + ) + } } } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/LibraryFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/LibraryFragment.kt index bcb0f35ce..6b2fb9e89 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/LibraryFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/LibraryFragment.kt @@ -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 + } } } } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/PlaylistFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/PlaylistFragment.kt index 909ee0a78..18f5306da 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/PlaylistFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/PlaylistFragment.kt @@ -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 + } } } } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/SearchFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/SearchFragment.kt index e4a86b088..8326d1b39 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/SearchFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/SearchFragment.kt @@ -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 + } } } } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt index 511ce6dbd..0cf3b077b 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/SearchResultFragment.kt @@ -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) } } } diff --git a/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt b/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt index 332fe2575..192ea8328 100644 --- a/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt +++ b/app/src/main/java/com/github/libretube/ui/fragments/TrendsFragment.kt @@ -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()) + } } } } diff --git a/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt b/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt index 91b67eab4..a32df4b8d 100644 --- a/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt +++ b/app/src/main/java/com/github/libretube/ui/preferences/InstanceSettings.kt @@ -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) { 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 { preference -> - preference.entry - } - } + for (instancePref in instancePrefs) { + instancePref.summaryProvider = + Preference.SummaryProvider { 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 { 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 { preference -> + preference.entry + } + } } } }