music: add playlist naming flow
Add a real playlist naming dialog and UX flow. This is a bit rough at the moment since theres a good amount of nuance here. Should improve as the playlist implementation continues to grow more fleshed out.
This commit is contained in:
parent
7ba2b1bb41
commit
e01ea25d0b
18 changed files with 302 additions and 68 deletions
|
@ -11,6 +11,7 @@
|
||||||
be parsed as images
|
be parsed as images
|
||||||
- Fixed issue where searches would match song file names case-sensitively
|
- Fixed issue where searches would match song file names case-sensitively
|
||||||
- Fixed issue where the notification would not respond to changes in the album cover setting
|
- Fixed issue where the notification would not respond to changes in the album cover setting
|
||||||
|
- Fixed issue where short names starting with an article would not be correctly sorted (ex. "the 1")
|
||||||
|
|
||||||
## 3.0.5
|
## 3.0.5
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,9 @@ import kotlin.math.min
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.music.dialog.PendingName
|
||||||
import org.oxycblt.auxio.navigation.MainNavigationAction
|
import org.oxycblt.auxio.navigation.MainNavigationAction
|
||||||
import org.oxycblt.auxio.navigation.NavigationViewModel
|
import org.oxycblt.auxio.navigation.NavigationViewModel
|
||||||
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
|
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
|
||||||
|
@ -60,8 +62,9 @@ class MainFragment :
|
||||||
ViewBindingFragment<FragmentMainBinding>(),
|
ViewBindingFragment<FragmentMainBinding>(),
|
||||||
ViewTreeObserver.OnPreDrawListener,
|
ViewTreeObserver.OnPreDrawListener,
|
||||||
NavController.OnDestinationChangedListener {
|
NavController.OnDestinationChangedListener {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
|
private val musicModel: MusicViewModel by activityViewModels()
|
||||||
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val selectionModel: SelectionViewModel by activityViewModels()
|
private val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
private val callback = DynamicBackPressedCallback()
|
private val callback = DynamicBackPressedCallback()
|
||||||
private var lastInsets: WindowInsets? = null
|
private var lastInsets: WindowInsets? = null
|
||||||
|
@ -132,6 +135,7 @@ class MainFragment :
|
||||||
collect(navModel.mainNavigationAction.flow, ::handleMainNavigation)
|
collect(navModel.mainNavigationAction.flow, ::handleMainNavigation)
|
||||||
collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
|
collect(navModel.exploreNavigationItem.flow, ::handleExploreNavigation)
|
||||||
collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
|
collect(navModel.exploreArtistNavigationItem.flow, ::handleArtistNavigationPicker)
|
||||||
|
collect(musicModel.pendingPlaylistNaming.flow, ::handlePlaylistNaming)
|
||||||
collectImmediately(playbackModel.song, ::updateSong)
|
collectImmediately(playbackModel.song, ::updateSong)
|
||||||
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
|
collect(playbackModel.artistPickerSong.flow, ::handlePlaybackArtistPicker)
|
||||||
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
|
collect(playbackModel.genrePickerSong.flow, ::handlePlaybackGenrePicker)
|
||||||
|
@ -300,6 +304,13 @@ class MainFragment :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handlePlaylistNaming(args: PendingName.Args?) {
|
||||||
|
if (args != null) {
|
||||||
|
findNavController().navigateSafe(MainFragmentDirections.actionNamePlaylist(args))
|
||||||
|
musicModel.pendingPlaylistNaming.consume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handlePlaybackArtistPicker(song: Song?) {
|
private fun handlePlaybackArtistPicker(song: Song?) {
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
navModel.mainNavigateTo(
|
navModel.mainNavigateTo(
|
||||||
|
|
|
@ -78,10 +78,14 @@ constructor(
|
||||||
// Avoid doing a flip if the given config is already being applied.
|
// Avoid doing a flip if the given config is already being applied.
|
||||||
if (tag == iconRes) return
|
if (tag == iconRes) return
|
||||||
tag = iconRes
|
tag = iconRes
|
||||||
flipping = true
|
|
||||||
pendingConfig = PendingConfig(iconRes, contentDescriptionRes, clickListener)
|
pendingConfig = PendingConfig(iconRes, contentDescriptionRes, clickListener)
|
||||||
// We will re-show the FAB later, assuming that there was not a prior flip operation.
|
|
||||||
super.hide(FlipVisibilityListener())
|
// Already hiding for whatever reason, apply the configuration when the FAB is shown again.
|
||||||
|
if (!isOrWillBeHidden) {
|
||||||
|
flipping = true
|
||||||
|
// We will re-show the FAB later, assuming that there was not a prior flip operation.
|
||||||
|
super.hide(FlipVisibilityListener())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class PendingConfig(
|
private data class PendingConfig(
|
||||||
|
|
|
@ -321,7 +321,7 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
binding.homeFab.flipTo(R.drawable.ic_add_24, R.string.desc_new_playlist) {
|
binding.homeFab.flipTo(R.drawable.ic_add_24, R.string.desc_new_playlist) {
|
||||||
musicModel.createPlaylist()
|
musicModel.createPlaylist("New playlist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,9 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import org.oxycblt.auxio.music.dialog.PendingName
|
||||||
|
import org.oxycblt.auxio.util.Event
|
||||||
|
import org.oxycblt.auxio.util.MutableEvent
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ViewModel] providing data specific to the music loading process.
|
* A [ViewModel] providing data specific to the music loading process.
|
||||||
|
@ -42,6 +45,9 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
|
||||||
val statistics: StateFlow<Statistics?>
|
val statistics: StateFlow<Statistics?>
|
||||||
get() = _statistics
|
get() = _statistics
|
||||||
|
|
||||||
|
private val _pendingPlaylistNaming = MutableEvent<PendingName.Args?>()
|
||||||
|
val pendingPlaylistNaming: Event<PendingName.Args?> = _pendingPlaylistNaming
|
||||||
|
|
||||||
init {
|
init {
|
||||||
musicRepository.addUpdateListener(this)
|
musicRepository.addUpdateListener(this)
|
||||||
musicRepository.addIndexingListener(this)
|
musicRepository.addIndexingListener(this)
|
||||||
|
@ -79,12 +85,15 @@ class MusicViewModel @Inject constructor(private val musicRepository: MusicRepos
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new generic playlist.
|
* Create a new generic playlist. This will prompt the user to edit the name before the creation
|
||||||
|
* finishes.
|
||||||
*
|
*
|
||||||
* @param name The name of the new playlist. If null, the user will be prompted for a name.
|
* @param name The preferred name of the new playlist.
|
||||||
*/
|
*/
|
||||||
fun createPlaylist(name: String? = null) {
|
fun createPlaylist(name: String, songs: List<Song> = listOf()) {
|
||||||
musicRepository.createPlaylist(name ?: "New playlist", listOf())
|
// TODO: Default to something like "Playlist 1", "Playlist 2", etc.
|
||||||
|
// TODO: Attempt to unify playlist creation flow with dialog model
|
||||||
|
_pendingPlaylistNaming.put(PendingName.Args(name, songs.map { it.uid }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* PlaylistDialogViewModel.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.music.dialog
|
||||||
|
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import org.oxycblt.auxio.music.Music
|
||||||
|
import org.oxycblt.auxio.music.MusicRepository
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [ViewModel] managing the state of the playlist editing dialogs.
|
||||||
|
* @author Alexander Capehart
|
||||||
|
*/
|
||||||
|
@HiltViewModel
|
||||||
|
class PlaylistDialogViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
||||||
|
ViewModel(), MusicRepository.UpdateListener {
|
||||||
|
private val _currentPendingName = MutableStateFlow<PendingName?>(null)
|
||||||
|
val currentPendingName: StateFlow<PendingName?> = _currentPendingName
|
||||||
|
|
||||||
|
init {
|
||||||
|
musicRepository.addUpdateListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||||
|
if (!changes.deviceLibrary) return
|
||||||
|
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||||
|
// Update the pending name to reflect new information in the music library.
|
||||||
|
_currentPendingName.value =
|
||||||
|
_currentPendingName.value?.let { pendingName ->
|
||||||
|
PendingName(
|
||||||
|
pendingName.name,
|
||||||
|
pendingName.songs.mapNotNull { deviceLibrary.findSong(it.uid) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
musicRepository.removeUpdateListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current [PendingName] based on the given [PendingName.Args].
|
||||||
|
* @param args The [PendingName.Args] to update with.
|
||||||
|
*/
|
||||||
|
fun setPendingName(args: PendingName.Args) {
|
||||||
|
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||||
|
val name =
|
||||||
|
PendingName(args.preferredName, args.songUids.mapNotNull(deviceLibrary::findSong))
|
||||||
|
_currentPendingName.value = name
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the current [PendingName] based on new user input.
|
||||||
|
* @param name The new user-inputted name, directly from the UI.
|
||||||
|
*/
|
||||||
|
fun updatePendingName(name: String?) {
|
||||||
|
// Remove any additional whitespace from the string to be consistent with all other
|
||||||
|
// music items.
|
||||||
|
val normalized = (name ?: return).trim()
|
||||||
|
_currentPendingName.value =
|
||||||
|
_currentPendingName.value?.run { PendingName(normalized, songs) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm the current [PendingName] operation and write it to the database.
|
||||||
|
*/
|
||||||
|
fun confirmPendingName() {
|
||||||
|
val pendingName = _currentPendingName.value ?: return
|
||||||
|
musicRepository.createPlaylist(pendingName.name, pendingName.songs)
|
||||||
|
_currentPendingName.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a name operation
|
||||||
|
*/
|
||||||
|
data class PendingName(val name: String, val songs: List<Song>) {
|
||||||
|
/**
|
||||||
|
* A [Parcelable] version of [PendingName], to be used as a dialog argument.
|
||||||
|
* @param preferredName The name to be used initially by the dialog.
|
||||||
|
* @param songUids The [Music.UID] of any pending [Song]s that will be put in the playlist.
|
||||||
|
*/
|
||||||
|
@Parcelize
|
||||||
|
data class Args(val preferredName: String, val songUids: List<Music.UID>) : Parcelable
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* PlaylistNamingDialog.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.oxycblt.auxio.music.dialog
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.navigation.fragment.findNavController
|
||||||
|
import androidx.navigation.fragment.navArgs
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.databinding.DialogPlaylistNamingBinding
|
||||||
|
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||||
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A dialog allowing the name of a new/existing playlist to be edited.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class PlaylistNamingDialog : ViewBindingDialogFragment<DialogPlaylistNamingBinding>() {
|
||||||
|
// activityViewModels is intentional here as the ViewModel will do work that we
|
||||||
|
// do not want to cancel after this dialog closes.
|
||||||
|
private val dialogModel: PlaylistDialogViewModel by activityViewModels()
|
||||||
|
// Information about what playlist to name for is initially within the navigation arguments
|
||||||
|
// as UIDs, as that is the only safe way to parcel playlist information.
|
||||||
|
private val args: PlaylistNamingDialogArgs by navArgs()
|
||||||
|
private var initializedInput = false
|
||||||
|
|
||||||
|
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
||||||
|
builder
|
||||||
|
.setTitle(R.string.lbl_new_playlist)
|
||||||
|
.setPositiveButton(R.string.lbl_ok) { _, _ -> dialogModel.confirmPendingName() }
|
||||||
|
.setNegativeButton(R.string.lbl_cancel, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||||
|
DialogPlaylistNamingBinding.inflate(inflater)
|
||||||
|
|
||||||
|
override fun onBindingCreated(
|
||||||
|
binding: DialogPlaylistNamingBinding,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) {
|
||||||
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
|
binding.playlistName.addTextChangedListener {
|
||||||
|
dialogModel.updatePendingName(it?.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogModel.setPendingName(args.pendingName)
|
||||||
|
collectImmediately(dialogModel.currentPendingName, ::updatePendingName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updatePendingName(pendingName: PendingName?) {
|
||||||
|
if (pendingName == null) {
|
||||||
|
findNavController().navigateUp()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Make sure we initialize the TextView with the preferred name if we haven't already.
|
||||||
|
if (!initializedInput) {
|
||||||
|
requireBinding().playlistName.setText(pendingName.name)
|
||||||
|
initializedInput = true
|
||||||
|
}
|
||||||
|
// Disable the OK button if the name is invalid (empty or whitespace)
|
||||||
|
(dialog as AlertDialog).getButton(AlertDialog.BUTTON_POSITIVE)?.isEnabled =
|
||||||
|
pendingName.name.isNotBlank()
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
package org.oxycblt.auxio.music.dialog
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
@ -99,6 +99,19 @@ class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
|
||||||
return separators
|
return separators
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the allowed separator characters that can be used to delimit multi-value tags.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*/
|
||||||
|
private object Separators {
|
||||||
|
const val COMMA = ','
|
||||||
|
const val SEMICOLON = ';'
|
||||||
|
const val SLASH = '/'
|
||||||
|
const val PLUS = '+'
|
||||||
|
const val AND = '&'
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val KEY_PENDING_SEPARATORS = BuildConfig.APPLICATION_ID + ".key.PENDING_SEPARATORS"
|
const val KEY_PENDING_SEPARATORS = BuildConfig.APPLICATION_ID + ".key.PENDING_SEPARATORS"
|
||||||
}
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2023 Auxio Project
|
|
||||||
* Separators.kt is part of Auxio.
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.oxycblt.auxio.music.metadata
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the allowed separator characters that can be used to delimit multi-value tags.
|
|
||||||
*
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
|
||||||
*/
|
|
||||||
object Separators {
|
|
||||||
const val COMMA = ','
|
|
||||||
const val SEMICOLON = ';'
|
|
||||||
const val SLASH = '/'
|
|
||||||
const val PLUS = '+'
|
|
||||||
const val AND = '&'
|
|
||||||
}
|
|
|
@ -169,7 +169,9 @@ private class TagWorkerImpl(
|
||||||
(textFrames["TCMP"]
|
(textFrames["TCMP"]
|
||||||
?: textFrames["TXXX:compilation"] ?: textFrames["TXXX:itunescompilation"])
|
?: textFrames["TXXX:compilation"] ?: textFrames["TXXX:itunescompilation"])
|
||||||
?.let {
|
?.let {
|
||||||
|
// Ignore invalid instances of this tag
|
||||||
if (it.size != 1 || it[0] != "1") return@let
|
if (it.size != 1 || it[0] != "1") return@let
|
||||||
|
// Change the metadata to be a compilation album made by "Various Artists"
|
||||||
rawSong.albumArtistNames =
|
rawSong.albumArtistNames =
|
||||||
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
||||||
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
||||||
|
@ -262,7 +264,9 @@ private class TagWorkerImpl(
|
||||||
|
|
||||||
// Compilation Flag
|
// Compilation Flag
|
||||||
(comments["compilation"] ?: comments["itunescompilation"])?.let {
|
(comments["compilation"] ?: comments["itunescompilation"])?.let {
|
||||||
|
// Ignore invalid instances of this tag
|
||||||
if (it.size != 1 || it[0] != "1") return@let
|
if (it.size != 1 || it[0] != "1") return@let
|
||||||
|
// Change the metadata to be a compilation album made by "Various Artists"
|
||||||
rawSong.albumArtistNames =
|
rawSong.albumArtistNames =
|
||||||
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
rawSong.albumArtistNames.ifEmpty { COMPILATION_ALBUM_ARTISTS }
|
||||||
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
rawSong.releaseTypes = rawSong.releaseTypes.ifEmpty { COMPILATION_RELEASE_TYPES }
|
||||||
|
|
|
@ -48,7 +48,7 @@ import org.oxycblt.auxio.util.collectImmediately
|
||||||
class ArtistNavigationPickerDialog :
|
class ArtistNavigationPickerDialog :
|
||||||
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> {
|
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> {
|
||||||
private val navigationModel: NavigationViewModel by activityViewModels()
|
private val navigationModel: NavigationViewModel by activityViewModels()
|
||||||
private val pickerModel: NavigationPickerViewModel by viewModels()
|
private val pickerModel: NavigationDialogViewModel by viewModels()
|
||||||
// Information about what artists to show choices for is initially within the navigation
|
// Information about what artists to show choices for is initially within the navigation
|
||||||
// arguments as UIDs, as that is the only safe way to parcel an artist.
|
// arguments as UIDs, as that is the only safe way to parcel an artist.
|
||||||
private val args: ArtistNavigationPickerDialogArgs by navArgs()
|
private val args: ArtistNavigationPickerDialogArgs by navArgs()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Auxio Project
|
* Copyright (c) 2023 Auxio Project
|
||||||
* NavigationPickerViewModel.kt is part of Auxio.
|
* NavigationDialogViewModel.kt is part of Auxio.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -26,12 +26,12 @@ import kotlinx.coroutines.flow.StateFlow
|
||||||
import org.oxycblt.auxio.music.*
|
import org.oxycblt.auxio.music.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [ViewModel] that stores the current information required for [ArtistNavigationPickerDialog].
|
* A [ViewModel] that stores the current information required for navigation dialogs
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class NavigationPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
class NavigationDialogViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
||||||
ViewModel(), MusicRepository.UpdateListener {
|
ViewModel(), MusicRepository.UpdateListener {
|
||||||
private val _currentArtistChoices = MutableStateFlow<ArtistNavigationChoices?>(null)
|
private val _currentArtistChoices = MutableStateFlow<ArtistNavigationChoices?>(null)
|
||||||
/** The current set of [Artist] choices to show in the picker, or null if to show nothing. */
|
/** The current set of [Artist] choices to show in the picker, or null if to show nothing. */
|
|
@ -49,7 +49,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
class ArtistPlaybackPickerDialog :
|
class ArtistPlaybackPickerDialog :
|
||||||
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> {
|
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Artist> {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val pickerModel: PlaybackPickerViewModel by viewModels()
|
private val pickerModel: PlaybackDialogViewModel by viewModels()
|
||||||
// Information about what Song to show choices for is initially within the navigation arguments
|
// Information about what Song to show choices for is initially within the navigation arguments
|
||||||
// as UIDs, as that is the only safe way to parcel a Song.
|
// as UIDs, as that is the only safe way to parcel a Song.
|
||||||
private val args: ArtistPlaybackPickerDialogArgs by navArgs()
|
private val args: ArtistPlaybackPickerDialogArgs by navArgs()
|
||||||
|
|
|
@ -49,7 +49,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
class GenrePlaybackPickerDialog :
|
class GenrePlaybackPickerDialog :
|
||||||
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Genre> {
|
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ClickableListListener<Genre> {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val pickerModel: PlaybackPickerViewModel by viewModels()
|
private val pickerModel: PlaybackDialogViewModel by viewModels()
|
||||||
// Information about what Song to show choices for is initially within the navigation arguments
|
// Information about what Song to show choices for is initially within the navigation arguments
|
||||||
// as UIDs, as that is the only safe way to parcel a Song.
|
// as UIDs, as that is the only safe way to parcel a Song.
|
||||||
private val args: GenrePlaybackPickerDialogArgs by navArgs()
|
private val args: GenrePlaybackPickerDialogArgs by navArgs()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Copyright (c) 2023 Auxio Project
|
* Copyright (c) 2023 Auxio Project
|
||||||
* PlaybackPickerViewModel.kt is part of Auxio.
|
* PlaybackDialogViewModel.kt is part of Auxio.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
@ -31,7 +31,7 @@ import org.oxycblt.auxio.music.*
|
||||||
* @author OxygenCobalt (Alexander Capehart)
|
* @author OxygenCobalt (Alexander Capehart)
|
||||||
*/
|
*/
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class PlaybackPickerViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
class PlaybackDialogViewModel @Inject constructor(private val musicRepository: MusicRepository) :
|
||||||
ViewModel(), MusicRepository.UpdateListener {
|
ViewModel(), MusicRepository.UpdateListener {
|
||||||
private val _currentPickerSong = MutableStateFlow<Song?>(null)
|
private val _currentPickerSong = MutableStateFlow<Song?>(null)
|
||||||
/** The current set of [Artist] choices to show in the picker, or null if to show nothing. */
|
/** The current set of [Artist] choices to show in the picker, or null if to show nothing. */
|
17
app/src/main/res/layout/dialog_playlist_naming.xml
Normal file
17
app/src/main/res/layout/dialog_playlist_naming.xml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.textfield.TextInputLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="@dimen/spacing_medium"
|
||||||
|
android:paddingEnd="@dimen/spacing_mid_large"
|
||||||
|
android:paddingStart="@dimen/spacing_mid_large"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
app:hintEnabled="false">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:id="@+id/playlist_name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
|
@ -18,26 +18,39 @@
|
||||||
android:id="@+id/action_show_details"
|
android:id="@+id/action_show_details"
|
||||||
app:destination="@id/song_detail_dialog" />
|
app:destination="@id/song_detail_dialog" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_pick_playback_artist"
|
android:id="@+id/action_name_playlist"
|
||||||
app:destination="@id/artist_playback_picker_dialog" />
|
app:destination="@id/playlist_naming_dialog" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_pick_navigation_artist"
|
android:id="@+id/action_pick_navigation_artist"
|
||||||
app:destination="@id/artist_navigation_picker_dialog" />
|
app:destination="@id/artist_navigation_picker_dialog" />
|
||||||
|
<action
|
||||||
|
android:id="@+id/action_pick_playback_artist"
|
||||||
|
app:destination="@id/artist_playback_picker_dialog" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_pick_playback_genre"
|
android:id="@+id/action_pick_playback_genre"
|
||||||
app:destination="@id/genre_playback_picker_dialog" />
|
app:destination="@id/genre_playback_picker_dialog" />
|
||||||
</fragment>
|
</fragment>
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/artist_playback_picker_dialog"
|
android:id="@+id/song_detail_dialog"
|
||||||
android:name="org.oxycblt.auxio.playback.dialog.ArtistPlaybackPickerDialog"
|
android:name="org.oxycblt.auxio.detail.SongDetailDialog"
|
||||||
android:label="artist_playback_picker_dialog"
|
android:label="song_detail_dialog"
|
||||||
tools:layout="@layout/dialog_music_picker">
|
tools:layout="@layout/dialog_song_detail">
|
||||||
<argument
|
<argument
|
||||||
android:name="artistUid"
|
android:name="songUid"
|
||||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
android:id="@+id/playlist_naming_dialog"
|
||||||
|
android:name="org.oxycblt.auxio.music.dialog.PlaylistNamingDialog"
|
||||||
|
android:label="playlist_naming_dialog"
|
||||||
|
tools:layout="@layout/dialog_song_detail">
|
||||||
|
<argument
|
||||||
|
android:name="pendingName"
|
||||||
|
app:argType="org.oxycblt.auxio.music.dialog.PendingName$Args" />
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/artist_navigation_picker_dialog"
|
android:id="@+id/artist_navigation_picker_dialog"
|
||||||
android:name="org.oxycblt.auxio.navigation.dialog.ArtistNavigationPickerDialog"
|
android:name="org.oxycblt.auxio.navigation.dialog.ArtistNavigationPickerDialog"
|
||||||
|
@ -48,6 +61,16 @@
|
||||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
|
<dialog
|
||||||
|
android:id="@+id/artist_playback_picker_dialog"
|
||||||
|
android:name="org.oxycblt.auxio.playback.dialog.ArtistPlaybackPickerDialog"
|
||||||
|
android:label="artist_playback_picker_dialog"
|
||||||
|
tools:layout="@layout/dialog_music_picker">
|
||||||
|
<argument
|
||||||
|
android:name="artistUid"
|
||||||
|
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||||
|
</dialog>
|
||||||
|
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/genre_playback_picker_dialog"
|
android:id="@+id/genre_playback_picker_dialog"
|
||||||
android:name="org.oxycblt.auxio.playback.dialog.GenrePlaybackPickerDialog"
|
android:name="org.oxycblt.auxio.playback.dialog.GenrePlaybackPickerDialog"
|
||||||
|
@ -58,16 +81,6 @@
|
||||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
||||||
</dialog>
|
</dialog>
|
||||||
|
|
||||||
<dialog
|
|
||||||
android:id="@+id/song_detail_dialog"
|
|
||||||
android:name="org.oxycblt.auxio.detail.SongDetailDialog"
|
|
||||||
android:label="song_detail_dialog"
|
|
||||||
tools:layout="@layout/dialog_song_detail">
|
|
||||||
<argument
|
|
||||||
android:name="songUid"
|
|
||||||
app:argType="org.oxycblt.auxio.music.Music$UID" />
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/root_preferences_fragment"
|
android:id="@+id/root_preferences_fragment"
|
||||||
|
@ -148,7 +161,7 @@
|
||||||
tools:layout="@layout/dialog_music_dirs" />
|
tools:layout="@layout/dialog_music_dirs" />
|
||||||
<dialog
|
<dialog
|
||||||
android:id="@+id/separators_dialog"
|
android:id="@+id/separators_dialog"
|
||||||
android:name="org.oxycblt.auxio.music.metadata.SeparatorsDialog"
|
android:name="org.oxycblt.auxio.music.dialog.SeparatorsDialog"
|
||||||
android:label="music_dirs_dialog"
|
android:label="music_dirs_dialog"
|
||||||
tools:layout="@layout/dialog_separators" />
|
tools:layout="@layout/dialog_separators" />
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,7 @@
|
||||||
|
|
||||||
<string name="lbl_playlist">Playlist</string>
|
<string name="lbl_playlist">Playlist</string>
|
||||||
<string name="lbl_playlists">Playlists</string>
|
<string name="lbl_playlists">Playlists</string>
|
||||||
|
<string name="lbl_new_playlist">New playlist</string>
|
||||||
|
|
||||||
<!-- Search for music -->
|
<!-- Search for music -->
|
||||||
<string name="lbl_search">Search</string>
|
<string name="lbl_search">Search</string>
|
||||||
|
|
Loading…
Reference in a new issue