music: clean up playlist experience
Add a variety of mild fixes and qol improvements regarding playlists.
This commit is contained in:
parent
1fd6795b0d
commit
572b0e52f8
21 changed files with 89 additions and 37 deletions
|
@ -348,7 +348,6 @@ constructor(
|
||||||
* @param at The position of the item to remove, in the list adapter data.
|
* @param at The position of the item to remove, in the list adapter data.
|
||||||
*/
|
*/
|
||||||
fun removePlaylistSong(at: Int) {
|
fun removePlaylistSong(at: Int) {
|
||||||
// TODO: Remove header when empty
|
|
||||||
val playlist = _currentPlaylist.value ?: return
|
val playlist = _currentPlaylist.value ?: return
|
||||||
val editedPlaylist = (_editedPlaylist.value ?: return).toMutableList()
|
val editedPlaylist = (_editedPlaylist.value ?: return).toMutableList()
|
||||||
val realAt = at - 2
|
val realAt = at - 2
|
||||||
|
@ -357,7 +356,13 @@ constructor(
|
||||||
}
|
}
|
||||||
editedPlaylist.removeAt(realAt)
|
editedPlaylist.removeAt(realAt)
|
||||||
_editedPlaylist.value = editedPlaylist
|
_editedPlaylist.value = editedPlaylist
|
||||||
refreshPlaylistList(playlist, UpdateInstructions.Remove(at))
|
refreshPlaylistList(
|
||||||
|
playlist,
|
||||||
|
if (editedPlaylist.isNotEmpty()) {
|
||||||
|
UpdateInstructions.Remove(at, 1)
|
||||||
|
} else {
|
||||||
|
UpdateInstructions.Remove(at - 2, 3)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refreshAudioInfo(song: Song) {
|
private fun refreshAudioInfo(song: Song) {
|
||||||
|
@ -490,18 +495,15 @@ constructor(
|
||||||
logD("Refreshing playlist list")
|
logD("Refreshing playlist list")
|
||||||
val list = mutableListOf<Item>()
|
val list = mutableListOf<Item>()
|
||||||
|
|
||||||
val newInstructions =
|
val songs = editedPlaylist.value ?: playlist.songs
|
||||||
if (playlist.songs.isNotEmpty()) {
|
if (songs.isNotEmpty()) {
|
||||||
val header = EditHeader(R.string.lbl_songs)
|
val header = EditHeader(R.string.lbl_songs)
|
||||||
list.add(Divider(header))
|
list.add(Divider(header))
|
||||||
list.add(header)
|
list.add(header)
|
||||||
list.addAll(_editedPlaylist.value ?: playlist.songs)
|
list.addAll(songs)
|
||||||
instructions
|
}
|
||||||
} else {
|
|
||||||
UpdateInstructions.Diff
|
|
||||||
}
|
|
||||||
|
|
||||||
_playlistInstructions.put(newInstructions)
|
_playlistInstructions.put(instructions)
|
||||||
_playlistList.value = list
|
_playlistList.value = list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -211,8 +211,7 @@ class PlaylistDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOpenMenu(item: Song, anchor: View) {
|
override fun onOpenMenu(item: Song, anchor: View) {
|
||||||
// TODO: Remove "Add to playlist" option, makes no sense
|
openMusicMenu(anchor, R.menu.menu_playlist_song_actions, item)
|
||||||
openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlay() {
|
override fun onPlay() {
|
||||||
|
@ -284,9 +283,11 @@ class PlaylistDetailFragment :
|
||||||
playlistHeaderAdapter.setEditedPlaylist(editedPlaylist)
|
playlistHeaderAdapter.setEditedPlaylist(editedPlaylist)
|
||||||
selectionModel.drop()
|
selectionModel.drop()
|
||||||
|
|
||||||
logD(editedPlaylist == detailModel.currentPlaylist.value?.songs)
|
if (editedPlaylist != null) {
|
||||||
requireBinding().detailEditToolbar.menu.findItem(R.id.action_save).isEnabled =
|
requireBinding().detailEditToolbar.menu.findItem(R.id.action_save).apply {
|
||||||
editedPlaylist != detailModel.currentPlaylist.value?.songs
|
isEnabled = editedPlaylist != detailModel.currentPlaylist.value?.songs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateMultiToolbar()
|
updateMultiToolbar()
|
||||||
}
|
}
|
||||||
|
|
|
@ -83,6 +83,7 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
|
||||||
editedPlaylist: List<Song>?,
|
editedPlaylist: List<Song>?,
|
||||||
listener: DetailHeaderAdapter.Listener
|
listener: DetailHeaderAdapter.Listener
|
||||||
) {
|
) {
|
||||||
|
// TODO: Debug perpetually re-binding images
|
||||||
binding.detailCover.bind(playlist, editedPlaylist)
|
binding.detailCover.bind(playlist, editedPlaylist)
|
||||||
binding.detailType.text = binding.context.getString(R.string.lbl_playlist)
|
binding.detailType.text = binding.context.getString(R.string.lbl_playlist)
|
||||||
binding.detailName.text = playlist.name.resolve(binding.context)
|
binding.detailName.text = playlist.name.resolve(binding.context)
|
||||||
|
|
|
@ -99,7 +99,7 @@ class PlaylistDetailListAdapter(private val listener: Listener) :
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.isEditing = editing
|
this.isEditing = editing
|
||||||
notifyItemRangeChanged(1, currentList.size - 2, PAYLOAD_EDITING_CHANGED)
|
notifyItemRangeChanged(1, currentList.size - 1, PAYLOAD_EDITING_CHANGED)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** An extended [DetailListAdapter.Listener] for [PlaylistDetailListAdapter]. */
|
/** An extended [DetailListAdapter.Listener] for [PlaylistDetailListAdapter]. */
|
||||||
|
@ -256,6 +256,7 @@ private constructor(private val binding: ItemEditableSongBinding) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateSelectionIndicator(isSelected: Boolean) {
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
binding.interactBody.isActivated = isSelected
|
||||||
binding.songAlbumCover.isActivated = isSelected
|
binding.songAlbumCover.isActivated = isSelected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,13 @@ import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [ListFragment] that shows a list of [Playlist]s.
|
||||||
|
*
|
||||||
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*
|
||||||
|
* TODO: Show a placeholder when there are no playlists.
|
||||||
|
*/
|
||||||
class PlaylistListFragment :
|
class PlaylistListFragment :
|
||||||
ListFragment<Playlist, FragmentHomeListBinding>(),
|
ListFragment<Playlist, FragmentHomeListBinding>(),
|
||||||
FastScrollRecyclerView.PopupProvider,
|
FastScrollRecyclerView.PopupProvider,
|
||||||
|
|
|
@ -93,8 +93,9 @@ sealed interface UpdateInstructions {
|
||||||
* Remove an item.
|
* Remove an item.
|
||||||
*
|
*
|
||||||
* @param at The location that the item should be removed from.
|
* @param at The location that the item should be removed from.
|
||||||
|
* @param size The amount of items to add.
|
||||||
*/
|
*/
|
||||||
data class Remove(val at: Int) : UpdateInstructions
|
data class Remove(val at: Int, val size: Int) : UpdateInstructions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -147,7 +148,7 @@ private class FlexibleListDiffer<T>(
|
||||||
}
|
}
|
||||||
is UpdateInstructions.Remove -> {
|
is UpdateInstructions.Remove -> {
|
||||||
currentList = newList
|
currentList = newList
|
||||||
updateCallback.onRemoved(instructions.at, 1)
|
updateCallback.onRemoved(instructions.at, instructions.size)
|
||||||
callback?.invoke()
|
callback?.invoke()
|
||||||
}
|
}
|
||||||
is UpdateInstructions.Diff,
|
is UpdateInstructions.Diff,
|
||||||
|
|
|
@ -131,7 +131,6 @@ class IndexerService :
|
||||||
override val scope = indexScope
|
override val scope = indexScope
|
||||||
|
|
||||||
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
override fun onMusicChanges(changes: MusicRepository.Changes) {
|
||||||
// TODO: Do not pause when playlist changes
|
|
||||||
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
val deviceLibrary = musicRepository.deviceLibrary ?: return
|
||||||
// Wipe possibly-invalidated outdated covers
|
// Wipe possibly-invalidated outdated covers
|
||||||
imageLoader.memoryCache?.clear()
|
imageLoader.memoryCache?.clear()
|
||||||
|
|
|
@ -34,6 +34,7 @@ import org.oxycblt.auxio.MainFragmentDirections
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
||||||
import org.oxycblt.auxio.music.MusicParent
|
import org.oxycblt.auxio.music.MusicParent
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.music.resolveNames
|
import org.oxycblt.auxio.music.resolveNames
|
||||||
import org.oxycblt.auxio.navigation.MainNavigationAction
|
import org.oxycblt.auxio.navigation.MainNavigationAction
|
||||||
|
@ -50,6 +51,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
* available controls.
|
* available controls.
|
||||||
*
|
*
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @author Alexander Capehart (OxygenCobalt)
|
||||||
|
*
|
||||||
|
* TODO: Improve flickering situation on play button
|
||||||
*/
|
*/
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class PlaybackPanelFragment :
|
class PlaybackPanelFragment :
|
||||||
|
@ -57,6 +60,7 @@ class PlaybackPanelFragment :
|
||||||
Toolbar.OnMenuItemClickListener,
|
Toolbar.OnMenuItemClickListener,
|
||||||
StyledSeekBar.Listener {
|
StyledSeekBar.Listener {
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
private val musicModel: MusicViewModel by activityViewModels()
|
||||||
private val navModel: NavigationViewModel by activityViewModels()
|
private val navModel: NavigationViewModel by activityViewModels()
|
||||||
private var equalizerLauncher: ActivityResultLauncher<Intent>? = null
|
private var equalizerLauncher: ActivityResultLauncher<Intent>? = null
|
||||||
|
|
||||||
|
@ -164,6 +168,10 @@ class PlaybackPanelFragment :
|
||||||
navigateToCurrentAlbum()
|
navigateToCurrentAlbum()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_playlist_add -> {
|
||||||
|
playbackModel.song.value?.let(musicModel::addToPlaylist)
|
||||||
|
true
|
||||||
|
}
|
||||||
R.id.action_song_detail -> {
|
R.id.action_song_detail -> {
|
||||||
playbackModel.song.value?.let { song ->
|
playbackModel.song.value?.let { song ->
|
||||||
navModel.mainNavigateTo(
|
navModel.mainNavigateTo(
|
||||||
|
|
|
@ -306,7 +306,7 @@ class EditableQueue : Queue {
|
||||||
else -> Queue.Change.Type.MAPPING
|
else -> Queue.Change.Type.MAPPING
|
||||||
}
|
}
|
||||||
check()
|
check()
|
||||||
return Queue.Change(type, UpdateInstructions.Remove(at))
|
return Queue.Change(type, UpdateInstructions.Remove(at, 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -534,17 +534,24 @@ class PlaybackStateManagerImpl @Inject constructor() : PlaybackStateManager {
|
||||||
val internalPlayer = internalPlayer ?: return
|
val internalPlayer = internalPlayer ?: return
|
||||||
logD("Restoring state $savedState")
|
logD("Restoring state $savedState")
|
||||||
|
|
||||||
|
val lastSong = queue.currentSong
|
||||||
parent = savedState.parent
|
parent = savedState.parent
|
||||||
queue.applySavedState(savedState.queueState)
|
queue.applySavedState(savedState.queueState)
|
||||||
repeatMode = savedState.repeatMode
|
repeatMode = savedState.repeatMode
|
||||||
notifyNewPlayback()
|
notifyNewPlayback()
|
||||||
|
|
||||||
// Continuing playback while also possibly doing drastic state updates is
|
// Check if we need to reload the player with a new music file, or if we can just leave
|
||||||
// a bad idea, so pause.
|
// it be. Specifically done so we don't pause on music updates that don't really change
|
||||||
internalPlayer.loadSong(queue.currentSong, false)
|
// what's playing (ex. playlist editing)
|
||||||
if (queue.currentSong != null) {
|
if (lastSong != queue.currentSong) {
|
||||||
// Internal player may have reloaded the media item, re-seek to the previous position
|
// Continuing playback while also possibly doing drastic state updates is
|
||||||
seekTo(savedState.positionMs)
|
// a bad idea, so pause.
|
||||||
|
internalPlayer.loadSong(queue.currentSong, false)
|
||||||
|
if (queue.currentSong != null) {
|
||||||
|
// Internal player may have reloaded the media item, re-seek to the previous
|
||||||
|
// position
|
||||||
|
seekTo(savedState.positionMs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
isInitialized = true
|
isInitialized = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="?attr/colorSurface" />
|
<item android:drawable="@drawable/sel_selection_bg" />
|
||||||
<item android:drawable="@drawable/sel_item_ripple_bg" />
|
|
||||||
<item android:drawable="?attr/selectableItemBackground" />
|
<item android:drawable="?attr/selectableItemBackground" />
|
||||||
</layer-list>
|
</layer-list>
|
|
@ -4,8 +4,9 @@
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="960"
|
android:viewportWidth="960"
|
||||||
android:viewportHeight="960"
|
android:viewportHeight="960"
|
||||||
android:tint="?attr/colorControlNormal">
|
android:tint="@color/sel_activatable_icon">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M840,280L840,840L120,840L120,120L680,120L840,280ZM760,314L646,200L200,200L200,760L760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM240,400L600,400L600,240L240,240L240,400ZM200,314L200,760L200,760L200,200L200,200L200,314Z"/>
|
android:pathData="M840,280L840,840L120,840L120,120L680,120L840,280ZM760,314L646,200L200,200L200,760L760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM240,400L600,400L600,240L240,240L240,400ZM200,314L200,760L200,760L200,200L200,200L200,314Z"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|
||||||
|
|
5
app/src/main/res/drawable/ui_item_bg.xml
Normal file
5
app/src/main/res/drawable/ui_item_bg.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?attr/colorSurface" />
|
||||||
|
<item android:drawable="@drawable/ui_item_ripple" />
|
||||||
|
</layer-list>
|
|
@ -1,5 +1,4 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:drawable="?attr/colorSurface" />
|
|
||||||
<item android:drawable="?attr/selectableItemBackground" />
|
<item android:drawable="?attr/selectableItemBackground" />
|
||||||
</layer-list>
|
</layer-list>
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/ui_item_ripple"
|
android:background="@drawable/ui_item_bg"
|
||||||
android:paddingStart="@dimen/spacing_medium"
|
android:paddingStart="@dimen/spacing_medium"
|
||||||
android:paddingTop="@dimen/spacing_mid_medium"
|
android:paddingTop="@dimen/spacing_mid_medium"
|
||||||
android:paddingEnd="@dimen/spacing_mid_medium"
|
android:paddingEnd="@dimen/spacing_mid_medium"
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
android:id="@+id/interact_body"
|
android:id="@+id/interact_body"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectableItemBackground">
|
android:background="@drawable/ui_item_ripple">
|
||||||
|
|
||||||
<org.oxycblt.auxio.image.ImageGroup
|
<org.oxycblt.auxio.image.ImageGroup
|
||||||
android:id="@+id/song_album_cover"
|
android:id="@+id/song_album_cover"
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/ui_item_ripple"
|
android:background="@drawable/ui_item_bg"
|
||||||
android:paddingStart="@dimen/spacing_medium"
|
android:paddingStart="@dimen/spacing_medium"
|
||||||
android:paddingTop="@dimen/spacing_mid_medium"
|
android:paddingTop="@dimen/spacing_mid_medium"
|
||||||
android:paddingEnd="@dimen/spacing_mid_medium"
|
android:paddingEnd="@dimen/spacing_mid_medium"
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="@drawable/ui_item_ripple"
|
android:background="@drawable/ui_item_bg"
|
||||||
android:paddingStart="@dimen/spacing_medium"
|
android:paddingStart="@dimen/spacing_medium"
|
||||||
android:paddingTop="@dimen/spacing_mid_medium"
|
android:paddingTop="@dimen/spacing_mid_medium"
|
||||||
android:paddingEnd="@dimen/spacing_mid_medium"
|
android:paddingEnd="@dimen/spacing_mid_medium"
|
||||||
|
|
|
@ -14,6 +14,9 @@
|
||||||
android:id="@+id/action_go_album"
|
android:id="@+id/action_go_album"
|
||||||
android:title="@string/lbl_go_album"
|
android:title="@string/lbl_go_album"
|
||||||
app:showAsAction="never" />
|
app:showAsAction="never" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_playlist_add"
|
||||||
|
android:title="@string/lbl_playlist_add" />
|
||||||
<item
|
<item
|
||||||
android:id="@+id/action_song_detail"
|
android:id="@+id/action_song_detail"
|
||||||
android:title="@string/lbl_song_detail"
|
android:title="@string/lbl_song_detail"
|
||||||
|
|
18
app/src/main/res/menu/menu_playlist_song_actions.xml
Normal file
18
app/src/main/res/menu/menu_playlist_song_actions.xml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_play_next"
|
||||||
|
android:title="@string/lbl_play_next" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_queue_add"
|
||||||
|
android:title="@string/lbl_queue_add" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_go_artist"
|
||||||
|
android:title="@string/lbl_go_artist" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_go_album"
|
||||||
|
android:title="@string/lbl_go_album" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_song_detail"
|
||||||
|
android:title="@string/lbl_song_detail" />
|
||||||
|
</menu>
|
Loading…
Reference in a new issue