refactor: simplify add to playlist dialog (#7074)

This commit is contained in:
Thomas W. 2025-02-15 14:54:21 +01:00 committed by GitHub
parent 20db67d229
commit 2ae689c74d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 141 additions and 79 deletions

View File

@ -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)

View File

@ -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

View File

@ -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<Playlists>()
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)

View File

@ -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<StreamItem>(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<Playlists> = 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(),
)
}
}
}
}