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:
Alexander Capehart 2023-05-12 10:39:56 -06:00
parent 7ba2b1bb41
commit e01ea25d0b
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
18 changed files with 302 additions and 68 deletions

View file

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

View file

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

View file

@ -78,11 +78,15 @@ 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)
// 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. // We will re-show the FAB later, assuming that there was not a prior flip operation.
super.hide(FlipVisibilityListener()) super.hide(FlipVisibilityListener())
} }
}
private data class PendingConfig( private data class PendingConfig(
@DrawableRes val iconRes: Int, @DrawableRes val iconRes: Int,

View file

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

View file

@ -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 }))
} }
/** /**

View file

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

View file

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

View file

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

View file

@ -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 = '&'
}

View file

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

View file

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

View file

@ -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. */

View file

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

View file

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

View file

@ -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. */

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

View file

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

View file

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