Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 967f2abd authored by Sebastiano Barezzi's avatar Sebastiano Barezzi
Browse files

Twelve: New create playlist fragment

Change-Id: Iba45cdd4f73af7ffb8af69516641866c14c57e58
parent 23e5e41f
Loading
Loading
Loading
Loading
+15 −18
Original line number Diff line number Diff line
/*
 * SPDX-FileCopyrightText: 2024 The LineageOS Project
 * SPDX-FileCopyrightText: 2024-2025 The LineageOS Project
 * SPDX-License-Identifier: Apache-2.0
 */

@@ -28,10 +28,10 @@ import kotlinx.coroutines.launch
import org.lineageos.twelve.R
import org.lineageos.twelve.ext.getParcelable
import org.lineageos.twelve.ext.getViewProperty
import org.lineageos.twelve.ext.navigateSafe
import org.lineageos.twelve.ext.setProgressCompat
import org.lineageos.twelve.models.Playlist
import org.lineageos.twelve.models.RequestStatus
import org.lineageos.twelve.ui.dialogs.EditTextMaterialAlertDialogBuilder
import org.lineageos.twelve.ui.recyclerview.SimpleListAdapter
import org.lineageos.twelve.ui.views.FullscreenLoadingProgressBar
import org.lineageos.twelve.ui.views.ListItem
@@ -65,7 +65,13 @@ class AddOrRemoveFromPlaylistsFragment : Fragment(R.layout.fragment_add_or_remov
                view.setOnClickListener {
                    item?.let {
                        when (it === addNewPlaylistItem) {
                            true -> openCreateNewPlaylistDialog()
                            true -> findNavController().navigateSafe(
                                R.id.action_addOrRemoveFromPlaylistsFragment_to_fragment_create_playlist_dialog,
                                CreatePlaylistDialogFragment.createBundle(
                                    providerIdentifier = viewModel.providerOfAudio.value,
                                )
                            )

                            false -> viewLifecycleOwner.lifecycleScope.launch {
                                fullscreenLoadingProgressBar.withProgress {
                                    when (it.second) {
@@ -120,7 +126,12 @@ class AddOrRemoveFromPlaylistsFragment : Fragment(R.layout.fragment_add_or_remov
        recyclerView.adapter = adapter

        createNewPlaylistButton.setOnClickListener {
            openCreateNewPlaylistDialog()
            findNavController().navigateSafe(
                R.id.action_addOrRemoveFromPlaylistsFragment_to_fragment_create_playlist_dialog,
                CreatePlaylistDialogFragment.createBundle(
                    providerIdentifier = viewModel.providerOfAudio.value,
                )
            )
        }

        viewModel.loadAudio(audioUri)
@@ -178,20 +189,6 @@ class AddOrRemoveFromPlaylistsFragment : Fragment(R.layout.fragment_add_or_remov
        }
    }

    private fun openCreateNewPlaylistDialog() {
        EditTextMaterialAlertDialogBuilder(requireContext())
            .setPositiveButton(R.string.create_playlist_confirm) { text ->
                viewLifecycleOwner.lifecycleScope.launch {
                    fullscreenLoadingProgressBar.withProgress {
                        viewModel.createPlaylist(text)
                    }
                }
            }
            .setTitle(R.string.create_playlist)
            .setNegativeButton(android.R.string.cancel, null)
            .show()
    }

    companion object {
        private val LOG_TAG = AddOrRemoveFromPlaylistsFragment::class.simpleName!!

+139 −0
Original line number Diff line number Diff line
/*
 * SPDX-FileCopyrightText: 2025 The LineageOS Project
 * SPDX-License-Identifier: Apache-2.0
 */

package org.lineageos.twelve.fragments

import android.os.Bundle
import android.view.View
import androidx.core.os.bundleOf
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import com.google.android.material.button.MaterialButton
import com.google.android.material.textfield.MaterialAutoCompleteTextView
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.lineageos.twelve.R
import org.lineageos.twelve.ext.getParcelable
import org.lineageos.twelve.ext.getViewProperty
import org.lineageos.twelve.ext.selectItem
import org.lineageos.twelve.models.ProviderIdentifier
import org.lineageos.twelve.ui.views.FullscreenLoadingProgressBar
import org.lineageos.twelve.viewmodels.CreatePlaylistViewModel

class CreatePlaylistDialogFragment : MaterialDialogFragment(
    R.layout.fragment_create_playlist_dialog
) {
    // View models
    private val viewModel by viewModels<CreatePlaylistViewModel>()

    // Views
    private val cancelMaterialButton by getViewProperty<MaterialButton>(R.id.cancelMaterialButton)
    private val createMaterialButton by getViewProperty<MaterialButton>(R.id.createMaterialButton)
    private val fullscreenLoadingProgressBar by getViewProperty<FullscreenLoadingProgressBar>(R.id.fullscreenLoadingProgressBar)
    private val playlistNameTextInputLayout by getViewProperty<TextInputLayout>(R.id.playlistNameTextInputLayout)
    private val providerAutoCompleteTextView by getViewProperty<MaterialAutoCompleteTextView>(R.id.providerAutoCompleteTextView)
    private val providerTextInputLayout by getViewProperty<TextInputLayout>(R.id.providerTextInputLayout)

    // Arguments
    private val providerIdentifier: ProviderIdentifier?
        get() = arguments?.getParcelable(ARG_PROVIDER_IDENTIFIER, ProviderIdentifier::class)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        viewModel.setProviderIdentifier(providerIdentifier)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        providerAutoCompleteTextView.setOnItemClickListener { _, _, position, _ ->
            viewModel.setProviderPosition(position)
        }

        playlistNameTextInputLayout.editText!!.apply {
            setText(viewModel.getPlaylistName())
            doOnTextChanged { text, _, _, _ ->
                playlistNameTextInputLayout.error = null
                viewModel.setPlaylistName(text?.toString() ?: "")
            }
        }

        cancelMaterialButton.setOnClickListener {
            findNavController().navigateUp()
        }

        createMaterialButton.setOnClickListener {
            if (viewModel.isPlaylistNameEmpty()) {
                playlistNameTextInputLayout.error = getString(
                    R.string.create_playlist_error_empty_name
                )
                return@setOnClickListener
            }

            playlistNameTextInputLayout.error = null

            viewLifecycleOwner.lifecycleScope.launch {
                fullscreenLoadingProgressBar.withProgress {
                    viewModel.createPlaylist()
                    findNavController().navigateUp()
                }
            }
        }

        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                loadData()
            }
        }
    }

    private fun CoroutineScope.loadData() {
        launch {
            viewModel.providersWithSelection.collectLatest { providersWithSelection ->
                val (providers, position) = providersWithSelection

                providerAutoCompleteTextView.setSimpleItems(
                    providers.map { provider ->
                        getString(
                            R.string.provider_format,
                            provider.name,
                            getString(provider.type.nameStringResId),
                        )
                    }.toTypedArray()
                )

                position?.also {
                    val provider = providers[it]

                    providerAutoCompleteTextView.selectItem(it)
                    providerTextInputLayout.setStartIconDrawable(
                        provider.type.iconDrawableResId
                    )
                }
            }
        }
    }

    companion object {
        private const val ARG_PROVIDER_IDENTIFIER = "provider_identifier"

        /**
         * Create a [Bundle] to use as the arguments for this fragment.
         * @param providerIdentifier A [ProviderIdentifier] to pre-fill the provider field
         */
        fun createBundle(
            providerIdentifier: ProviderIdentifier? = null,
        ) = bundleOf(
            ARG_PROVIDER_IDENTIFIER to providerIdentifier,
        )
    }
}
+14 −20
Original line number Diff line number Diff line
/*
 * SPDX-FileCopyrightText: 2024 The LineageOS Project
 * SPDX-FileCopyrightText: 2024-2025 The LineageOS Project
 * SPDX-License-Identifier: Apache-2.0
 */

@@ -30,10 +30,8 @@ import org.lineageos.twelve.ext.setProgressCompat
import org.lineageos.twelve.models.Playlist
import org.lineageos.twelve.models.RequestStatus
import org.lineageos.twelve.models.SortingStrategy
import org.lineageos.twelve.ui.dialogs.EditTextMaterialAlertDialogBuilder
import org.lineageos.twelve.ui.recyclerview.SimpleListAdapter
import org.lineageos.twelve.ui.recyclerview.UniqueItemDiffCallback
import org.lineageos.twelve.ui.views.FullscreenLoadingProgressBar
import org.lineageos.twelve.ui.views.ListItem
import org.lineageos.twelve.ui.views.SortingChip
import org.lineageos.twelve.utils.PermissionsChecker
@@ -49,7 +47,6 @@ class PlaylistsFragment : Fragment(R.layout.fragment_playlists) {

    // Views
    private val createNewPlaylistButton by getViewProperty<Button>(R.id.createNewPlaylistButton)
    private val fullscreenLoadingProgressBar by getViewProperty<FullscreenLoadingProgressBar>(R.id.fullscreenLoadingProgressBar)
    private val linearProgressIndicator by getViewProperty<LinearProgressIndicator>(R.id.linearProgressIndicator)
    private val noElementsLinearLayout by getViewProperty<LinearLayout>(R.id.noElementsLinearLayout)
    private val recyclerView by getViewProperty<RecyclerView>(R.id.recyclerView)
@@ -65,7 +62,13 @@ class PlaylistsFragment : Fragment(R.layout.fragment_playlists) {
            view.setOnClickListener {
                item?.let {
                    when (it === addNewPlaylistItem) {
                        true -> openCreateNewPlaylistDialog()
                        true -> findNavController().navigateSafe(
                            R.id.action_mainFragment_to_fragment_create_playlist_dialog,
                            CreatePlaylistDialogFragment.createBundle(
                                providerIdentifier = viewModel.navigationProvider.value
                            )
                        )

                        false -> findNavController().navigateSafe(
                            R.id.action_mainFragment_to_fragment_playlist,
                            PlaylistFragment.createBundle(it.uri)
@@ -124,7 +127,12 @@ class PlaylistsFragment : Fragment(R.layout.fragment_playlists) {
        recyclerView.adapter = adapter

        createNewPlaylistButton.setOnClickListener {
            openCreateNewPlaylistDialog()
            findNavController().navigateSafe(
                R.id.action_mainFragment_to_fragment_create_playlist_dialog,
                CreatePlaylistDialogFragment.createBundle(
                    providerIdentifier = viewModel.navigationProvider.value
                )
            )
        }

        viewLifecycleOwner.lifecycleScope.launch {
@@ -194,20 +202,6 @@ class PlaylistsFragment : Fragment(R.layout.fragment_playlists) {
        }
    }

    private fun openCreateNewPlaylistDialog() {
        EditTextMaterialAlertDialogBuilder(requireContext())
            .setPositiveButton(R.string.create_playlist_confirm) { text ->
                viewLifecycleOwner.lifecycleScope.launch {
                    fullscreenLoadingProgressBar.withProgress {
                        viewModel.createPlaylist(text)
                    }
                }
            }
            .setTitle(R.string.create_playlist)
            .setNegativeButton(android.R.string.cancel, null)
            .show()
    }

    companion object {
        private val LOG_TAG = PlaylistsFragment::class.simpleName!!
    }
+12 −1
Original line number Diff line number Diff line
/*
 * SPDX-FileCopyrightText: 2024 The LineageOS Project
 * SPDX-FileCopyrightText: 2024-2025 The LineageOS Project
 * SPDX-License-Identifier: Apache-2.0
 */

@@ -48,6 +48,17 @@ class AddOrRemoveFromPlaylistsViewModel(application: Application) : TwelveViewMo
            RequestStatus.Loading()
        )

    @OptIn(ExperimentalCoroutinesApi::class)
    val providerOfAudio = audioUri
        .filterNotNull()
        .flatMapLatest { mediaRepository.providerOfMediaItems(it) }
        .flowOn(Dispatchers.IO)
        .stateIn(
            viewModelScope,
            SharingStarted.Eagerly,
            null
        )

    fun loadAudio(audioUri: Uri) {
        this.audioUri.value = audioUri
    }
+65 −0
Original line number Diff line number Diff line
/*
 * SPDX-FileCopyrightText: 2025 The LineageOS Project
 * SPDX-License-Identifier: Apache-2.0
 */

package org.lineageos.twelve.viewmodels

import android.app.Application
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
import org.lineageos.twelve.datasources.MediaError
import org.lineageos.twelve.models.Provider
import org.lineageos.twelve.models.ProviderIdentifier
import org.lineageos.twelve.models.RequestStatus

class CreatePlaylistViewModel(application: Application) : TwelveViewModel(application) {
    private val providerIdentifier = MutableStateFlow<ProviderIdentifier?>(null)

    private val playlistName = MutableStateFlow("")

    val providersWithSelection = combine(
        mediaRepository.allVisibleProviders,
        providerIdentifier
    ) { allVisibleProviders, providerIdentifier ->
        allVisibleProviders to providerIdentifier?.let {
            allVisibleProviders.indexOfFirst { provider ->
                provider.type == it.type && provider.typeId == it.typeId
            }.takeIf { it != -1 }
        }
    }
        .flowOn(Dispatchers.IO)
        .stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(),
            listOf<Provider>() to null
        )

    fun setProviderIdentifier(providerIdentifier: ProviderIdentifier?) {
        this.providerIdentifier.value = providerIdentifier
    }

    fun setProviderPosition(position: Int) = setProviderIdentifier(
        providersWithSelection.value.first[position]
    )

    fun getPlaylistName() = playlistName.value

    fun setPlaylistName(playlistName: String) {
        this.playlistName.value = playlistName
    }

    fun isPlaylistNameEmpty() = playlistName.value.isEmpty()

    suspend fun createPlaylist() = providerIdentifier.value?.let {
        withContext(Dispatchers.IO) {
            mediaRepository.createPlaylist(it, playlistName.value)
        }
    } ?: RequestStatus.Error(MediaError.IO)
}
Loading