From 2ae689c74d818dfcee777a6b04a2a2dae68f001a Mon Sep 17 00:00:00 2001 From: "Thomas W." Date: Sat, 15 Feb 2025 14:54:21 +0100 Subject: [PATCH] refactor: simplify add to playlist dialog (#7074) --- .../github/libretube/api/PlaylistsHelper.kt | 4 +- .../com/github/libretube/api/obj/Playlists.kt | 5 +- .../ui/dialogs/AddToPlaylistDialog.kt | 105 +++++------------ .../libretube/ui/models/PlaylistViewModel.kt | 106 +++++++++++++++++- 4 files changed, 141 insertions(+), 79 deletions(-) diff --git a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt index c158a4601..8b7442617 100644 --- a/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt +++ b/app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt @@ -71,7 +71,9 @@ object PlaylistsHelper { playlistsRepository.createPlaylist(playlistName) suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem) = - playlistsRepository.addToPlaylist(playlistId, *videos) + withContext(Dispatchers.IO) { + playlistsRepository.addToPlaylist(playlistId, *videos) + } suspend fun renamePlaylist(playlistId: String, newName: String) = playlistsRepository.renamePlaylist(playlistId, newName) diff --git a/app/src/main/java/com/github/libretube/api/obj/Playlists.kt b/app/src/main/java/com/github/libretube/api/obj/Playlists.kt index daede3002..ebca66152 100644 --- a/app/src/main/java/com/github/libretube/api/obj/Playlists.kt +++ b/app/src/main/java/com/github/libretube/api/obj/Playlists.kt @@ -1,12 +1,15 @@ package com.github.libretube.api.obj +import android.os.Parcelable +import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable @Serializable +@Parcelize data class Playlists( val id: String? = null, var name: String? = null, var shortDescription: String? = null, val thumbnail: String? = null, val videos: Long = 0 -) +) : Parcelable 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 290f45b47..026d90713 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 @@ -1,6 +1,5 @@ package com.github.libretube.ui.dialogs -import android.annotation.SuppressLint import android.app.Dialog import android.content.DialogInterface import android.os.Bundle @@ -8,34 +7,24 @@ import android.util.Log import android.widget.Toast import androidx.core.os.bundleOf import androidx.fragment.app.DialogFragment -import androidx.fragment.app.activityViewModels import androidx.fragment.app.setFragmentResult -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle +import androidx.fragment.app.viewModels import com.github.libretube.R -import com.github.libretube.api.PlaylistsHelper -import com.github.libretube.api.obj.Playlists import com.github.libretube.api.obj.StreamItem import com.github.libretube.constants.IntentData import com.github.libretube.databinding.DialogAddToPlaylistBinding -import com.github.libretube.extensions.TAG import com.github.libretube.extensions.parcelable -import com.github.libretube.extensions.toastFromMainDispatcher import com.github.libretube.ui.models.PlaylistViewModel -import com.github.libretube.util.PlayingQueue import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.launch /** * Dialog to insert new videos to a playlist * videoId: The id of the video to add. If non is provided, insert the whole playing queue */ class AddToPlaylistDialog : DialogFragment() { - private var videoInfo: StreamItem? = null - private val viewModel: PlaylistViewModel by activityViewModels() - var playlists = emptyList() + private var videoInfo: StreamItem? = null + private val viewModel: PlaylistViewModel by viewModels { PlaylistViewModel.Factory } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -44,19 +33,39 @@ class AddToPlaylistDialog : DialogFragment() { } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val binding = DialogAddToPlaylistBinding.inflate(layoutInflater) - childFragmentManager.setFragmentResultListener( CreatePlaylistDialog.CREATE_PLAYLIST_DIALOG_REQUEST_KEY, this ) { _, resultBundle -> val addedToPlaylist = resultBundle.getBoolean(IntentData.playlistTask) if (addedToPlaylist) { - fetchPlaylists(binding) + viewModel.fetchPlaylists() } } - fetchPlaylists(binding) + val binding = DialogAddToPlaylistBinding.inflate(layoutInflater) + viewModel.uiState.observe(this) { (lastSelectedPlaylistId, playlists, msg, saved) -> + binding.playlistsSpinner.items = playlists.mapNotNull { it.name } + + // select the last used playlist + lastSelectedPlaylistId?.let { id -> + binding.playlistsSpinner.selectedItemPosition = playlists + .indexOfFirst { it.id == id } + .takeIf { it >= 0 } ?: 0 + } + + msg?.let { + with(binding.root.context) { + Toast.makeText(this, getString(it.resId, it.formatArgs), Toast.LENGTH_SHORT).show() + } + viewModel.onMessageShown() + } + + saved?.let { + dismiss() + viewModel.onDismissed() + } + } return MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.addToPlaylist) @@ -65,71 +74,17 @@ class AddToPlaylistDialog : DialogFragment() { .setView(binding.root) .show() .apply { + // Click listeners without closing the dialog getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener { CreatePlaylistDialog().show(childFragmentManager, null) } getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener { - val playlistIndex = binding.playlistsSpinner.selectedItemPosition - - val playlist = playlists.getOrElse(playlistIndex) { return@setOnClickListener } - viewModel.lastSelectedPlaylistId = playlist.id!! - - dialog?.hide() - lifecycleScope.launch { - addToPlaylist(playlist.id, playlist.name!!) - dialog?.dismiss() - } + val selectedItemPosition = binding.playlistsSpinner.selectedItemPosition + viewModel.onAddToPlaylist(selectedItemPosition) } } } - private fun fetchPlaylists(binding: DialogAddToPlaylistBinding) { - lifecycleScope.launch { - repeatOnLifecycle(Lifecycle.State.CREATED) { - playlists = try { - PlaylistsHelper.getPlaylists() - } catch (e: Exception) { - Log.e(TAG(), e.toString()) - Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show() - return@repeatOnLifecycle - }.filter { !it.name.isNullOrEmpty() } - - binding.playlistsSpinner.items = playlists.map { it.name!! } - - if (playlists.isEmpty()) return@repeatOnLifecycle - - // select the last used playlist - viewModel.lastSelectedPlaylistId?.let { id -> - binding.playlistsSpinner.selectedItemPosition = playlists - .indexOfFirst { it.id == id } - .takeIf { it >= 0 } ?: 0 - } - } - } - } - - @SuppressLint("StringFormatInvalid") - private suspend fun addToPlaylist(playlistId: String, playlistName: String) { - val appContext = context?.applicationContext ?: return - val streams = videoInfo?.let { listOf(it) } ?: PlayingQueue.getStreams() - - val success = try { - if (streams.isEmpty()) throw IllegalArgumentException() - PlaylistsHelper.addToPlaylist(playlistId, *streams.toTypedArray()) - } catch (e: Exception) { - Log.e(TAG(), e.toString()) - appContext.toastFromMainDispatcher(R.string.unknown_error) - return - } - if (success) { - appContext.toastFromMainDispatcher( - appContext.getString(R.string.added_to_playlist, playlistName) - ) - } else { - appContext.toastFromMainDispatcher(R.string.fail) - } - } - override fun onDismiss(dialog: DialogInterface) { super.onDismiss(dialog) diff --git a/app/src/main/java/com/github/libretube/ui/models/PlaylistViewModel.kt b/app/src/main/java/com/github/libretube/ui/models/PlaylistViewModel.kt index 700fc9670..54d21f118 100644 --- a/app/src/main/java/com/github/libretube/ui/models/PlaylistViewModel.kt +++ b/app/src/main/java/com/github/libretube/ui/models/PlaylistViewModel.kt @@ -1,7 +1,109 @@ package com.github.libretube.ui.models +import android.os.Parcelable +import androidx.annotation.StringRes +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel +import androidx.lifecycle.asLiveData +import androidx.lifecycle.createSavedStateHandle +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.github.libretube.R +import com.github.libretube.api.PlaylistsHelper +import com.github.libretube.api.obj.Playlists +import com.github.libretube.api.obj.StreamItem +import com.github.libretube.constants.IntentData +import com.github.libretube.util.PlayingQueue +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.RawValue -class PlaylistViewModel : ViewModel() { - var lastSelectedPlaylistId: String? = null +class PlaylistViewModel( + private val savedStateHandle: SavedStateHandle, +) : ViewModel() { + + private val _uiState = savedStateHandle.getStateFlow(UI_STATE, UiState()) + val uiState = _uiState.asLiveData() + + init { + fetchPlaylists() + } + + fun fetchPlaylists() { + viewModelScope.launch { + kotlin.runCatching { + PlaylistsHelper.getPlaylists() + }.onSuccess { playlists -> + savedStateHandle[UI_STATE] = _uiState.value.copy( + playlists = playlists.filterNot { list -> list.name.isNullOrEmpty() } + ) + }.onFailure { + savedStateHandle[UI_STATE] = _uiState.value.copy( + message = UiState.Message(R.string.unknown_error) + ) + } + } + } + + fun onAddToPlaylist(playlistIndex: Int) { + val playlist = _uiState.value.playlists.getOrElse(playlistIndex) { return } + savedStateHandle[UI_STATE] = _uiState.value.copy(lastSelectedPlaylistId = playlist.id) + + val videoInfo = savedStateHandle.get(IntentData.videoInfo) + val streams = videoInfo?.let { listOf(it) } ?: PlayingQueue.getStreams() + + viewModelScope.launch { + kotlin.runCatching { + if (streams.isEmpty()) { + throw IllegalArgumentException() + } + PlaylistsHelper.addToPlaylist(playlist.id!!, *streams.toTypedArray()) + }.onSuccess { + savedStateHandle[UI_STATE] = _uiState.value.copy( + message = UiState.Message(R.string.added_to_playlist, listOf(playlist.name!!)), + saved = Unit, + ) + } + .onFailure { + savedStateHandle[UI_STATE] = _uiState.value.copy( + message = UiState.Message(R.string.unknown_error) + ) + } + } + } + + fun onMessageShown() { + savedStateHandle[UI_STATE] = _uiState.value.copy(message = null) + } + + fun onDismissed() { + savedStateHandle[UI_STATE] = _uiState.value.copy(saved = null) + } + + @Parcelize + data class UiState( + val lastSelectedPlaylistId: String? = null, + val playlists: List = emptyList(), + val message: Message? = null, + val saved: Unit? = null, + ) : Parcelable { + @Parcelize + data class Message( + @StringRes val resId: Int, + val formatArgs: List<@RawValue Any>? = null, + ) : Parcelable + } + + companion object { + private const val UI_STATE = "ui_state" + + val Factory = viewModelFactory { + initializer { + PlaylistViewModel( + savedStateHandle = createSavedStateHandle(), + ) + } + } + } }