Skip to content

Commit 2ae689c

Browse files
refactor: simplify add to playlist dialog (libre-tube#7074)
1 parent 20db67d commit 2ae689c

File tree

4 files changed

+141
-79
lines changed

4 files changed

+141
-79
lines changed

app/src/main/java/com/github/libretube/api/PlaylistsHelper.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ object PlaylistsHelper {
7171
playlistsRepository.createPlaylist(playlistName)
7272

7373
suspend fun addToPlaylist(playlistId: String, vararg videos: StreamItem) =
74-
playlistsRepository.addToPlaylist(playlistId, *videos)
74+
withContext(Dispatchers.IO) {
75+
playlistsRepository.addToPlaylist(playlistId, *videos)
76+
}
7577

7678
suspend fun renamePlaylist(playlistId: String, newName: String) =
7779
playlistsRepository.renamePlaylist(playlistId, newName)
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
package com.github.libretube.api.obj
22

3+
import android.os.Parcelable
4+
import kotlinx.parcelize.Parcelize
35
import kotlinx.serialization.Serializable
46

57
@Serializable
8+
@Parcelize
69
data class Playlists(
710
val id: String? = null,
811
var name: String? = null,
912
var shortDescription: String? = null,
1013
val thumbnail: String? = null,
1114
val videos: Long = 0
12-
)
15+
) : Parcelable

app/src/main/java/com/github/libretube/ui/dialogs/AddToPlaylistDialog.kt

Lines changed: 30 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,30 @@
11
package com.github.libretube.ui.dialogs
22

3-
import android.annotation.SuppressLint
43
import android.app.Dialog
54
import android.content.DialogInterface
65
import android.os.Bundle
76
import android.util.Log
87
import android.widget.Toast
98
import androidx.core.os.bundleOf
109
import androidx.fragment.app.DialogFragment
11-
import androidx.fragment.app.activityViewModels
1210
import androidx.fragment.app.setFragmentResult
13-
import androidx.lifecycle.Lifecycle
14-
import androidx.lifecycle.lifecycleScope
15-
import androidx.lifecycle.repeatOnLifecycle
11+
import androidx.fragment.app.viewModels
1612
import com.github.libretube.R
17-
import com.github.libretube.api.PlaylistsHelper
18-
import com.github.libretube.api.obj.Playlists
1913
import com.github.libretube.api.obj.StreamItem
2014
import com.github.libretube.constants.IntentData
2115
import com.github.libretube.databinding.DialogAddToPlaylistBinding
22-
import com.github.libretube.extensions.TAG
2316
import com.github.libretube.extensions.parcelable
24-
import com.github.libretube.extensions.toastFromMainDispatcher
2517
import com.github.libretube.ui.models.PlaylistViewModel
26-
import com.github.libretube.util.PlayingQueue
2718
import com.google.android.material.dialog.MaterialAlertDialogBuilder
28-
import kotlinx.coroutines.launch
2919

3020
/**
3121
* Dialog to insert new videos to a playlist
3222
* videoId: The id of the video to add. If non is provided, insert the whole playing queue
3323
*/
3424
class AddToPlaylistDialog : DialogFragment() {
35-
private var videoInfo: StreamItem? = null
36-
private val viewModel: PlaylistViewModel by activityViewModels()
3725

38-
var playlists = emptyList<Playlists>()
26+
private var videoInfo: StreamItem? = null
27+
private val viewModel: PlaylistViewModel by viewModels { PlaylistViewModel.Factory }
3928

4029
override fun onCreate(savedInstanceState: Bundle?) {
4130
super.onCreate(savedInstanceState)
@@ -44,19 +33,39 @@ class AddToPlaylistDialog : DialogFragment() {
4433
}
4534

4635
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
47-
val binding = DialogAddToPlaylistBinding.inflate(layoutInflater)
48-
4936
childFragmentManager.setFragmentResultListener(
5037
CreatePlaylistDialog.CREATE_PLAYLIST_DIALOG_REQUEST_KEY,
5138
this
5239
) { _, resultBundle ->
5340
val addedToPlaylist = resultBundle.getBoolean(IntentData.playlistTask)
5441
if (addedToPlaylist) {
55-
fetchPlaylists(binding)
42+
viewModel.fetchPlaylists()
5643
}
5744
}
5845

59-
fetchPlaylists(binding)
46+
val binding = DialogAddToPlaylistBinding.inflate(layoutInflater)
47+
viewModel.uiState.observe(this) { (lastSelectedPlaylistId, playlists, msg, saved) ->
48+
binding.playlistsSpinner.items = playlists.mapNotNull { it.name }
49+
50+
// select the last used playlist
51+
lastSelectedPlaylistId?.let { id ->
52+
binding.playlistsSpinner.selectedItemPosition = playlists
53+
.indexOfFirst { it.id == id }
54+
.takeIf { it >= 0 } ?: 0
55+
}
56+
57+
msg?.let {
58+
with(binding.root.context) {
59+
Toast.makeText(this, getString(it.resId, it.formatArgs), Toast.LENGTH_SHORT).show()
60+
}
61+
viewModel.onMessageShown()
62+
}
63+
64+
saved?.let {
65+
dismiss()
66+
viewModel.onDismissed()
67+
}
68+
}
6069

6170
return MaterialAlertDialogBuilder(requireContext())
6271
.setTitle(R.string.addToPlaylist)
@@ -65,71 +74,17 @@ class AddToPlaylistDialog : DialogFragment() {
6574
.setView(binding.root)
6675
.show()
6776
.apply {
77+
// Click listeners without closing the dialog
6878
getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener {
6979
CreatePlaylistDialog().show(childFragmentManager, null)
7080
}
7181
getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
72-
val playlistIndex = binding.playlistsSpinner.selectedItemPosition
73-
74-
val playlist = playlists.getOrElse(playlistIndex) { return@setOnClickListener }
75-
viewModel.lastSelectedPlaylistId = playlist.id!!
76-
77-
dialog?.hide()
78-
lifecycleScope.launch {
79-
addToPlaylist(playlist.id, playlist.name!!)
80-
dialog?.dismiss()
81-
}
82+
val selectedItemPosition = binding.playlistsSpinner.selectedItemPosition
83+
viewModel.onAddToPlaylist(selectedItemPosition)
8284
}
8385
}
8486
}
8587

86-
private fun fetchPlaylists(binding: DialogAddToPlaylistBinding) {
87-
lifecycleScope.launch {
88-
repeatOnLifecycle(Lifecycle.State.CREATED) {
89-
playlists = try {
90-
PlaylistsHelper.getPlaylists()
91-
} catch (e: Exception) {
92-
Log.e(TAG(), e.toString())
93-
Toast.makeText(context, R.string.unknown_error, Toast.LENGTH_SHORT).show()
94-
return@repeatOnLifecycle
95-
}.filter { !it.name.isNullOrEmpty() }
96-
97-
binding.playlistsSpinner.items = playlists.map { it.name!! }
98-
99-
if (playlists.isEmpty()) return@repeatOnLifecycle
100-
101-
// select the last used playlist
102-
viewModel.lastSelectedPlaylistId?.let { id ->
103-
binding.playlistsSpinner.selectedItemPosition = playlists
104-
.indexOfFirst { it.id == id }
105-
.takeIf { it >= 0 } ?: 0
106-
}
107-
}
108-
}
109-
}
110-
111-
@SuppressLint("StringFormatInvalid")
112-
private suspend fun addToPlaylist(playlistId: String, playlistName: String) {
113-
val appContext = context?.applicationContext ?: return
114-
val streams = videoInfo?.let { listOf(it) } ?: PlayingQueue.getStreams()
115-
116-
val success = try {
117-
if (streams.isEmpty()) throw IllegalArgumentException()
118-
PlaylistsHelper.addToPlaylist(playlistId, *streams.toTypedArray())
119-
} catch (e: Exception) {
120-
Log.e(TAG(), e.toString())
121-
appContext.toastFromMainDispatcher(R.string.unknown_error)
122-
return
123-
}
124-
if (success) {
125-
appContext.toastFromMainDispatcher(
126-
appContext.getString(R.string.added_to_playlist, playlistName)
127-
)
128-
} else {
129-
appContext.toastFromMainDispatcher(R.string.fail)
130-
}
131-
}
132-
13388
override fun onDismiss(dialog: DialogInterface) {
13489
super.onDismiss(dialog)
13590

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,109 @@
11
package com.github.libretube.ui.models
22

3+
import android.os.Parcelable
4+
import androidx.annotation.StringRes
5+
import androidx.lifecycle.SavedStateHandle
36
import androidx.lifecycle.ViewModel
7+
import androidx.lifecycle.asLiveData
8+
import androidx.lifecycle.createSavedStateHandle
9+
import androidx.lifecycle.viewModelScope
10+
import androidx.lifecycle.viewmodel.initializer
11+
import androidx.lifecycle.viewmodel.viewModelFactory
12+
import com.github.libretube.R
13+
import com.github.libretube.api.PlaylistsHelper
14+
import com.github.libretube.api.obj.Playlists
15+
import com.github.libretube.api.obj.StreamItem
16+
import com.github.libretube.constants.IntentData
17+
import com.github.libretube.util.PlayingQueue
18+
import kotlinx.coroutines.launch
19+
import kotlinx.parcelize.Parcelize
20+
import kotlinx.parcelize.RawValue
421

5-
class PlaylistViewModel : ViewModel() {
6-
var lastSelectedPlaylistId: String? = null
22+
class PlaylistViewModel(
23+
private val savedStateHandle: SavedStateHandle,
24+
) : ViewModel() {
25+
26+
private val _uiState = savedStateHandle.getStateFlow(UI_STATE, UiState())
27+
val uiState = _uiState.asLiveData()
28+
29+
init {
30+
fetchPlaylists()
31+
}
32+
33+
fun fetchPlaylists() {
34+
viewModelScope.launch {
35+
kotlin.runCatching {
36+
PlaylistsHelper.getPlaylists()
37+
}.onSuccess { playlists ->
38+
savedStateHandle[UI_STATE] = _uiState.value.copy(
39+
playlists = playlists.filterNot { list -> list.name.isNullOrEmpty() }
40+
)
41+
}.onFailure {
42+
savedStateHandle[UI_STATE] = _uiState.value.copy(
43+
message = UiState.Message(R.string.unknown_error)
44+
)
45+
}
46+
}
47+
}
48+
49+
fun onAddToPlaylist(playlistIndex: Int) {
50+
val playlist = _uiState.value.playlists.getOrElse(playlistIndex) { return }
51+
savedStateHandle[UI_STATE] = _uiState.value.copy(lastSelectedPlaylistId = playlist.id)
52+
53+
val videoInfo = savedStateHandle.get<StreamItem>(IntentData.videoInfo)
54+
val streams = videoInfo?.let { listOf(it) } ?: PlayingQueue.getStreams()
55+
56+
viewModelScope.launch {
57+
kotlin.runCatching {
58+
if (streams.isEmpty()) {
59+
throw IllegalArgumentException()
60+
}
61+
PlaylistsHelper.addToPlaylist(playlist.id!!, *streams.toTypedArray())
62+
}.onSuccess {
63+
savedStateHandle[UI_STATE] = _uiState.value.copy(
64+
message = UiState.Message(R.string.added_to_playlist, listOf(playlist.name!!)),
65+
saved = Unit,
66+
)
67+
}
68+
.onFailure {
69+
savedStateHandle[UI_STATE] = _uiState.value.copy(
70+
message = UiState.Message(R.string.unknown_error)
71+
)
72+
}
73+
}
74+
}
75+
76+
fun onMessageShown() {
77+
savedStateHandle[UI_STATE] = _uiState.value.copy(message = null)
78+
}
79+
80+
fun onDismissed() {
81+
savedStateHandle[UI_STATE] = _uiState.value.copy(saved = null)
82+
}
83+
84+
@Parcelize
85+
data class UiState(
86+
val lastSelectedPlaylistId: String? = null,
87+
val playlists: List<Playlists> = emptyList(),
88+
val message: Message? = null,
89+
val saved: Unit? = null,
90+
) : Parcelable {
91+
@Parcelize
92+
data class Message(
93+
@StringRes val resId: Int,
94+
val formatArgs: List<@RawValue Any>? = null,
95+
) : Parcelable
96+
}
97+
98+
companion object {
99+
private const val UI_STATE = "ui_state"
100+
101+
val Factory = viewModelFactory {
102+
initializer {
103+
PlaylistViewModel(
104+
savedStateHandle = createSavedStateHandle(),
105+
)
106+
}
107+
}
108+
}
7109
}

0 commit comments

Comments
 (0)