diff --git a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt index 93b8b239a..db14d5882 100644 --- a/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt +++ b/app/src/main/java/org/oxycblt/auxio/IntegerTable.kt @@ -49,6 +49,12 @@ object IntegerTable { const val VIEW_TYPE_ARTIST_SONG = 0xA00A /** DiscHeaderViewHolder */ const val VIEW_TYPE_DISC_HEADER = 0xA00B + /** EditHeaderViewHolder */ + const val VIEW_TYPE_EDIT_HEADER = 0xA00C + /** ConfirmHeaderViewHolder */ + const val VIEW_TYPE_CONFIRM_HEADER = 0xA00D + /** EditableSongViewHolder */ + const val VIEW_TYPE_EDITABLE_SONG = 0xA00E /** "Music playback" notification code */ const val PLAYBACK_NOTIFICATION_CODE = 0xA0A0 /** "Music loading" notification code */ diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index b676745cd..ccaf2c9e1 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -22,6 +22,7 @@ import androidx.annotation.StringRes import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import java.lang.Exception import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -30,6 +31,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.yield import org.oxycblt.auxio.R +import org.oxycblt.auxio.detail.list.EditHeader import org.oxycblt.auxio.detail.list.SortHeader import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Divider @@ -145,6 +147,7 @@ constructor( } // --- PLAYLIST --- + private val _currentPlaylist = MutableStateFlow(null) /** The current [Playlist] to display. Null if there is nothing to do. */ val currentPlaylist: StateFlow @@ -158,16 +161,13 @@ constructor( val playlistInstructions: Event get() = _playlistInstructions - private var isEditingPlaylist = false - - /** The current [Sort] used for [Song]s in [playlistList]. */ - var playlistSongSort: Sort - get() = musicSettings.playlistSongSort - set(value) { - musicSettings.playlistSongSort = value - // Refresh the playlist list to reflect the new sort. - currentPlaylist.value?.let { refreshPlaylistList(it, true) } - } + private val _editedPlaylist = MutableStateFlow?>(null) + /** + * The new playlist songs created during the current editing session. Null if no editing session + * is occurring. + */ + val editedPlaylist: StateFlow?> + get() = _editedPlaylist /** * The [MusicMode] to use when playing a [Song] from the UI, or null to play from the currently @@ -220,6 +220,7 @@ constructor( if (changes.userLibrary && userLibrary != null) { val playlist = currentPlaylist.value if (playlist != null) { + logD("Updated playlist to ${currentPlaylist.value}") _currentPlaylist.value = userLibrary.findPlaylist(playlist.uid)?.also(::refreshPlaylistList) } @@ -285,6 +286,71 @@ constructor( musicRepository.userLibrary?.findPlaylist(uid)?.also(::refreshPlaylistList) } + /** Start a playlist editing session. Does nothing if a playlist is not being shown. */ + fun startPlaylistEdit() { + val playlist = _currentPlaylist.value ?: return + logD("Starting playlist edit") + _editedPlaylist.value = playlist.songs + refreshPlaylistList(playlist) + } + + /** + * End a playlist editing session and commits it to the database. Does nothing if there was no + * prior editing session. + */ + fun confirmPlaylistEdit() { + val playlist = _currentPlaylist.value ?: return + val editedPlaylist = (_editedPlaylist.value ?: return).also { _editedPlaylist.value = null } + musicRepository.rewritePlaylist(playlist, editedPlaylist) + } + + /** + * End a playlist editing session and keep the prior state. Does nothing if there was no prior + * editing session. + */ + fun dropPlaylistEdit() { + val playlist = _currentPlaylist.value ?: return + _editedPlaylist.value = null + refreshPlaylistList(playlist) + } + + /** + * (Visually) move a song in the current playlist. Does nothing if not in an editing session. + * + * @param from The start position, in the list adapter data. + * @param to The destination position, in the list adapter data. + */ + fun movePlaylistSongs(from: Int, to: Int): Boolean { + val playlist = _currentPlaylist.value ?: return false + val editedPlaylist = (_editedPlaylist.value ?: return false).toMutableList() + val realFrom = from - 2 + val realTo = to - 2 + if (realFrom !in editedPlaylist.indices || realTo !in editedPlaylist.indices) { + return false + } + editedPlaylist.add(realFrom, editedPlaylist.removeAt(realTo)) + _editedPlaylist.value = editedPlaylist + refreshPlaylistList(playlist, UpdateInstructions.Move(from, to)) + return true + } + + /** + * (Visually) remove a song in the current playlist. Does nothing if not in an editing session. + * + * @param at The position of the item to remove, in the list adapter data. + */ + fun removePlaylistSong(at: Int) { + val playlist = _currentPlaylist.value ?: return + val editedPlaylist = (_editedPlaylist.value ?: return).toMutableList() + val realAt = at - 2 + if (realAt !in editedPlaylist.indices) { + return + } + editedPlaylist.removeAt(realAt) + _editedPlaylist.value = editedPlaylist + refreshPlaylistList(playlist, UpdateInstructions.Remove(at)) + } + private fun refreshAudioInfo(song: Song) { // Clear any previous job in order to avoid stale data from appearing in the UI. currentSongJob?.cancel() @@ -408,21 +474,26 @@ constructor( _genreList.value = list } - private fun refreshPlaylistList(playlist: Playlist, replace: Boolean = false) { + private fun refreshPlaylistList( + playlist: Playlist, + instructions: UpdateInstructions = UpdateInstructions.Diff + ) { + logD(Exception().stackTraceToString()) logD("Refreshing playlist list") - var instructions: UpdateInstructions = UpdateInstructions.Diff val list = mutableListOf() - if (playlist.songs.isNotEmpty()) { - val header = BasicHeader(R.string.lbl_songs) - list.add(Divider(header)) - list.add(header) - if (replace) { - instructions = UpdateInstructions.Replace(list.size) + val newInstructions = + if (playlist.songs.isNotEmpty()) { + val header = EditHeader(R.string.lbl_songs) + list.add(Divider(header)) + list.add(header) + list.addAll(_editedPlaylist.value ?: playlist.songs) + instructions + } else { + UpdateInstructions.Diff } - list.addAll(playlistSongSort.songs(playlist.songs)) - } - _playlistInstructions.put(instructions) + + _playlistInstructions.put(newInstructions) _playlistList.value = list } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt index 9b7d78b5c..42f1d2e91 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -23,23 +23,26 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import androidx.fragment.app.activityViewModels +import androidx.navigation.NavController +import androidx.navigation.NavDestination import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.transition.MaterialSharedAxis import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.header.DetailHeaderAdapter import org.oxycblt.auxio.detail.header.PlaylistDetailHeaderAdapter -import org.oxycblt.auxio.detail.list.DetailListAdapter import org.oxycblt.auxio.detail.list.PlaylistDetailListAdapter +import org.oxycblt.auxio.detail.list.PlaylistDragCallback import org.oxycblt.auxio.list.Divider import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ListFragment -import org.oxycblt.auxio.list.Sort import org.oxycblt.auxio.list.selection.SelectionViewModel import org.oxycblt.auxio.music.* import org.oxycblt.auxio.navigation.NavigationViewModel @@ -55,7 +58,8 @@ import org.oxycblt.auxio.util.* class PlaylistDetailFragment : ListFragment(), DetailHeaderAdapter.Listener, - DetailListAdapter.Listener { + PlaylistDetailListAdapter.Listener, + NavController.OnDestinationChangedListener { private val detailModel: DetailViewModel by activityViewModels() override val navModel: NavigationViewModel by activityViewModels() override val playbackModel: PlaybackViewModel by activityViewModels() @@ -66,6 +70,8 @@ class PlaylistDetailFragment : private val args: PlaylistDetailFragmentArgs by navArgs() private val playlistHeaderAdapter = PlaylistDetailHeaderAdapter(this) private val playlistListAdapter = PlaylistDetailListAdapter(this) + private var touchHelper: ItemTouchHelper? = null + private var initialNavDestinationChange = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -92,6 +98,10 @@ class PlaylistDetailFragment : binding.detailRecycler.apply { adapter = ConcatAdapter(playlistHeaderAdapter, playlistListAdapter) + touchHelper = + ItemTouchHelper(PlaylistDragCallback(detailModel)).also { + it.attachToRecyclerView(this) + } (layoutManager as GridLayoutManager).setFullWidthLookup { if (it != 0) { val item = detailModel.playlistList.value[it - 1] @@ -107,21 +117,52 @@ class PlaylistDetailFragment : detailModel.setPlaylistUid(args.playlistUid) collectImmediately(detailModel.currentPlaylist, ::updatePlaylist) collectImmediately(detailModel.playlistList, ::updateList) + collectImmediately(detailModel.editedPlaylist, ::updateEditedPlaylist) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) collect(navModel.exploreNavigationItem.flow, ::handleNavigation) collectImmediately(selectionModel.selected, ::updateSelection) } + override fun onStart() { + super.onStart() + // Once we add the destination change callback, we will receive another initialization call, + // so handle that by resetting the flag. + initialNavDestinationChange = false + findNavController().addOnDestinationChangedListener(this) + } + + override fun onStop() { + super.onStop() + findNavController().removeOnDestinationChangedListener(this) + } + override fun onDestroyBinding(binding: FragmentDetailBinding) { super.onDestroyBinding(binding) binding.detailToolbar.setOnMenuItemClickListener(null) + touchHelper = null binding.detailRecycler.adapter = null // Avoid possible race conditions that could cause a bad replace instruction to be consumed // during list initialization and crash the app. Could happen if the user is fast enough. detailModel.playlistInstructions.consume() } + override fun onDestinationChanged( + controller: NavController, + destination: NavDestination, + arguments: Bundle? + ) { + // Drop the initial call by NavController that simply provides us with the current + // destination. This would cause the selection state to be lost every time the device + // rotates. + if (!initialNavDestinationChange) { + initialNavDestinationChange = true + return + } + // Drop any pending playlist edits when navigating away. + detailModel.dropPlaylistEdit() + } + override fun onMenuItemClick(item: MenuItem): Boolean { if (super.onMenuItemClick(item)) { return true @@ -155,7 +196,12 @@ class PlaylistDetailFragment : playbackModel.playFromPlaylist(item, unlikelyToBeNull(detailModel.currentPlaylist.value)) } + override fun onPickUp(viewHolder: RecyclerView.ViewHolder) { + requireNotNull(touchHelper) { "ItemTouchHelper was not available" }.startDrag(viewHolder) + } + override fun onOpenMenu(item: Song, anchor: View) { + // TODO: Remove "Add to playlist" option, makes no sense openMusicMenu(anchor, R.menu.menu_song_actions, item) } @@ -167,39 +213,21 @@ class PlaylistDetailFragment : playbackModel.shuffle(unlikelyToBeNull(detailModel.currentPlaylist.value)) } - override fun onOpenSortMenu(anchor: View) { - openMenu(anchor, R.menu.menu_playlist_sort) { - // Select the corresponding sort mode option - val sort = detailModel.playlistSongSort - unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true - // Select the corresponding sort direction option - val directionItemId = - when (sort.direction) { - Sort.Direction.ASCENDING -> R.id.option_sort_asc - Sort.Direction.DESCENDING -> R.id.option_sort_dec - } - unlikelyToBeNull(menu.findItem(directionItemId)).isChecked = true - // If there is no sort specified, disable the ascending/descending options, as - // they make no sense. We still do want to indicate the state however, in the case - // that the user wants to switch to a sort mode where they do make sense. - if (sort.mode is Sort.Mode.ByNone) { - menu.findItem(R.id.option_sort_dec).isEnabled = false - menu.findItem(R.id.option_sort_asc).isEnabled = false - } + override fun onStartEdit() { + selectionModel.drop() + detailModel.startPlaylistEdit() + } - setOnMenuItemClickListener { item -> - item.isChecked = !item.isChecked - detailModel.playlistSongSort = - when (item.itemId) { - // Sort direction options - R.id.option_sort_asc -> sort.withDirection(Sort.Direction.ASCENDING) - R.id.option_sort_dec -> sort.withDirection(Sort.Direction.DESCENDING) - // Any other option is a sort mode - else -> sort.withMode(unlikelyToBeNull(Sort.Mode.fromItemId(item.itemId))) - } - true - } - } + override fun onConfirmEdit() { + detailModel.confirmPlaylistEdit() + } + + override fun onDropEdit() { + detailModel.dropPlaylistEdit() + } + + override fun onOpenSortMenu(anchor: View) { + throw IllegalStateException() } private fun updatePlaylist(playlist: Playlist?) { @@ -250,6 +278,12 @@ class PlaylistDetailFragment : playlistListAdapter.update(list, detailModel.playlistInstructions.consume()) } + private fun updateEditedPlaylist(editedPlaylist: List?) { + // TODO: Disable check item when no edits have been made + // TODO: Improve how this state change looks + playlistListAdapter.setEditing(editedPlaylist != null) + } + private fun updateSelection(selected: List) { playlistListAdapter.setSelected(selected.toSet()) requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt index cd23751be..9c43dc875 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/DetailListAdapter.kt @@ -111,8 +111,8 @@ abstract class DetailListAdapter( data class SortHeader(@StringRes override val titleRes: Int) : Header /** - * A [RecyclerView.ViewHolder] that displays a [SortHeader], a variation on [BasicHeader] that adds - * a button opening a menu for sorting. Use [from] to create an instance. + * A [RecyclerView.ViewHolder] that displays a [SortHeader] and it's actions. Use [from] to create + * an instance. * * @author Alexander Capehart (OxygenCobalt) */ @@ -126,7 +126,7 @@ private class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : */ fun bind(sortHeader: SortHeader, listener: DetailListAdapter.Listener<*>) { binding.headerTitle.text = binding.context.getString(sortHeader.titleRes) - binding.headerButton.apply { + binding.headerSort.apply { // Add a Tooltip based on the content description so that the purpose of this // button can be clear. TooltipCompat.setTooltipText(this, contentDescription) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt index 5a33e511f..c24683c0e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDetailListAdapter.kt @@ -18,53 +18,286 @@ package org.oxycblt.auxio.detail.list +import android.annotation.SuppressLint +import android.graphics.drawable.LayerDrawable +import android.view.View import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.appcompat.widget.TooltipCompat +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.shape.MaterialShapeDrawable +import org.oxycblt.auxio.IntegerTable +import org.oxycblt.auxio.R +import org.oxycblt.auxio.databinding.ItemEditHeaderBinding +import org.oxycblt.auxio.databinding.ItemEditableSongBinding +import org.oxycblt.auxio.list.EditableListListener +import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item +import org.oxycblt.auxio.list.adapter.PlayingIndicatorAdapter +import org.oxycblt.auxio.list.adapter.SelectionIndicatorAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback +import org.oxycblt.auxio.list.recycler.MaterialDragCallback import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.music.Playlist import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.music.resolveNames +import org.oxycblt.auxio.util.context +import org.oxycblt.auxio.util.getAttrColorCompat +import org.oxycblt.auxio.util.getDimen +import org.oxycblt.auxio.util.inflater /** - * A [DetailListAdapter] implementing the header and sub-items for the [Playlist] detail view. + * A [DetailListAdapter] implementing the header, sub-items, and editing state for the [Playlist] + * detail view. * * @param listener A [DetailListAdapter.Listener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ -class PlaylistDetailListAdapter(private val listener: Listener) : +class PlaylistDetailListAdapter(private val listener: Listener) : DetailListAdapter(listener, DIFF_CALLBACK) { + private var isEditing = false + override fun getItemViewType(position: Int) = when (getItem(position)) { - // Support generic song items. - is Song -> SongViewHolder.VIEW_TYPE + is EditHeader -> EditHeaderViewHolder.VIEW_TYPE + is Song -> PlaylistSongViewHolder.VIEW_TYPE else -> super.getItemViewType(position) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = - if (viewType == SongViewHolder.VIEW_TYPE) { - SongViewHolder.from(parent) - } else { - super.onCreateViewHolder(parent, viewType) + when (viewType) { + EditHeaderViewHolder.VIEW_TYPE -> EditHeaderViewHolder.from(parent) + PlaylistSongViewHolder.VIEW_TYPE -> PlaylistSongViewHolder.from(parent) + else -> super.onCreateViewHolder(parent, viewType) } - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - super.onBindViewHolder(holder, position) - val item = getItem(position) - if (item is Song) { - (holder as SongViewHolder).bind(item, listener) + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + payloads: List + ) { + super.onBindViewHolder(holder, position, payloads) + + if (payloads.isEmpty()) { + when (val item = getItem(position)) { + is EditHeader -> (holder as EditHeaderViewHolder).bind(item, listener) + is Song -> (holder as PlaylistSongViewHolder).bind(item, listener) + } + } + + if (holder is ViewHolder) { + holder.updateEditing(isEditing) } } - companion object { + fun setEditing(editing: Boolean) { + if (editing == isEditing) { + // Nothing to do. + return + } + this.isEditing = editing + notifyItemRangeChanged(1, currentList.size - 2, PAYLOAD_EDITING_CHANGED) + } + + /** An extended [DetailListAdapter.Listener] for [PlaylistDetailListAdapter]. */ + interface Listener : DetailListAdapter.Listener, EditableListListener { + /** Called when the "edit" option is selected in the edit header. */ + fun onStartEdit() + /** Called when the "confirm" option is selected in the edit header. */ + fun onConfirmEdit() + /** Called when the "cancel" option is selected in the edit header. */ + fun onDropEdit() + } + + /** + * A [RecyclerView.ViewHolder] extension required to respond to changes in the editing state. + */ + interface ViewHolder { + /** + * Called when the editing state changes. Implementations should update UI options as needed + * to reflect the new state. + * + * @param editing Whether the data is currently being edited or not. + */ + fun updateEditing(editing: Boolean) + } + + private companion object { + val PAYLOAD_EDITING_CHANGED = Any() + val DIFF_CALLBACK = object : SimpleDiffCallback() { override fun areContentsTheSame(oldItem: Item, newItem: Item) = when { oldItem is Song && newItem is Song -> - SongViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) + PlaylistSongViewHolder.DIFF_CALLBACK.areContentsTheSame( + oldItem, newItem) + oldItem is EditHeader && newItem is EditHeader -> + EditHeaderViewHolder.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) else -> DetailListAdapter.DIFF_CALLBACK.areContentsTheSame(oldItem, newItem) } } } } + +/** + * A [RecyclerView.ViewHolder] that displays a [SortHeader] and it's actions. Use [from] to create + * an instance. + * + * @param titleRes The string resource to use as the header title + * @author Alexander Capehart (OxygenCobalt) + */ +data class EditHeader(@StringRes override val titleRes: Int) : Header + +/** Displays an [EditHeader] and it's actions. Use [from] to create an instance. */ +private class EditHeaderViewHolder private constructor(private val binding: ItemEditHeaderBinding) : + RecyclerView.ViewHolder(binding.root), PlaylistDetailListAdapter.ViewHolder { + /** + * Bind new data to this instance. + * + * @param editHeader The new [EditHeader] to bind. + * @param listener An [PlaylistDetailListAdapter.Listener] to bind interactions to. + */ + fun bind(editHeader: EditHeader, listener: PlaylistDetailListAdapter.Listener) { + binding.headerTitle.text = binding.context.getString(editHeader.titleRes) + // Add a Tooltip based on the content description so that the purpose of this + // button can be clear. + binding.headerEdit.apply { + TooltipCompat.setTooltipText(this, contentDescription) + setOnClickListener { listener.onStartEdit() } + } + binding.headerConfirm.apply { + TooltipCompat.setTooltipText(this, contentDescription) + setOnClickListener { listener.onConfirmEdit() } + } + binding.headerCancel.apply { + TooltipCompat.setTooltipText(this, contentDescription) + setOnClickListener { listener.onDropEdit() } + } + } + + override fun updateEditing(editing: Boolean) { + binding.headerEdit.apply { + isGone = editing + jumpDrawablesToCurrentState() + } + binding.headerConfirm.apply { + isVisible = editing + jumpDrawablesToCurrentState() + } + binding.headerCancel.apply { + isVisible = editing + jumpDrawablesToCurrentState() + } + } + + companion object { + /** A unique ID for this [RecyclerView.ViewHolder] type. */ + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_EDIT_HEADER + + /** + * Create a new instance. + * + * @param parent The parent to inflate this instance from. + * @return A new instance. + */ + fun from(parent: View) = + EditHeaderViewHolder(ItemEditHeaderBinding.inflate(parent.context.inflater)) + + /** A comparator that can be used with DiffUtil. */ + val DIFF_CALLBACK = + object : SimpleDiffCallback() { + override fun areContentsTheSame(oldItem: EditHeader, newItem: EditHeader) = + oldItem.titleRes == newItem.titleRes + } + } +} + +/** + * A [PlayingIndicatorAdapter.ViewHolder] that displays a queue [Song] which can be re-ordered and + * removed. Use [from] to create an instance. + * + * @author Alexander Capehart (OxygenCobalt) + */ +private class PlaylistSongViewHolder +private constructor(private val binding: ItemEditableSongBinding) : + SelectionIndicatorAdapter.ViewHolder(binding.root), + MaterialDragCallback.ViewHolder, + PlaylistDetailListAdapter.ViewHolder { + override val enabled: Boolean + get() = binding.songDragHandle.isVisible + override val root = binding.root + override val body = binding.body + override val delete = binding.background + override val background = + MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply { + fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface) + elevation = binding.context.getDimen(R.dimen.elevation_normal) + alpha = 0 + } + init { + binding.body.background = + LayerDrawable( + arrayOf( + MaterialShapeDrawable.createWithElevationOverlay(binding.context).apply { + fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface) + }, + background)) + } + + /** + * Bind new data to this instance. + * + * @param song The new [Song] to bind. + * @param listener A [PlaylistDetailListAdapter.Listener] to bind interactions to. + */ + @SuppressLint("ClickableViewAccessibility") + fun bind(song: Song, listener: PlaylistDetailListAdapter.Listener) { + listener.bind(song, this, binding.interactBody, menuButton = binding.songMenu) + listener.bind(this, binding.songDragHandle) + binding.songAlbumCover.bind(song) + binding.songName.text = song.name.resolve(binding.context) + binding.songInfo.text = song.artists.resolveNames(binding.context) + // Not swiping this ViewHolder if it's being re-bound, ensure that the background is + // not visible. See MaterialDragCallback for why this is done. + binding.background.isInvisible = true + } + + override fun updateSelectionIndicator(isSelected: Boolean) { + binding.songAlbumCover.isActivated = isSelected + } + + override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) { + binding.interactBody.isSelected = isActive + binding.songAlbumCover.isPlaying = isPlaying + } + + override fun updateEditing(editing: Boolean) { + binding.songDragHandle.isInvisible = !editing + binding.songMenu.isInvisible = editing + binding.interactBody.apply { + isClickable = !editing + isFocusable = !editing + } + } + + companion object { + /** A unique ID for this [RecyclerView.ViewHolder] type. */ + const val VIEW_TYPE = IntegerTable.VIEW_TYPE_EDITABLE_SONG + + /** + * Create a new instance. + * + * @param parent The parent to inflate this instance from. + * @return A new instance. + */ + fun from(parent: View) = + PlaylistSongViewHolder(ItemEditableSongBinding.inflate(parent.context.inflater)) + + /** A comparator that can be used with DiffUtil. */ + val DIFF_CALLBACK = SongViewHolder.DIFF_CALLBACK + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDragCallback.kt new file mode 100644 index 000000000..c93514e14 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/PlaylistDragCallback.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Auxio Project + * PlaylistDragCallback.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 . + */ + +package org.oxycblt.auxio.detail.list + +import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.detail.DetailViewModel +import org.oxycblt.auxio.list.recycler.MaterialDragCallback + +/** + * A [MaterialDragCallback] extension for playlist-specific item editing. + * + * @author Alexander Capehart (OxygenCobalt) + */ +class PlaylistDragCallback(private val detailModel: DetailViewModel) : MaterialDragCallback() { + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ) = + detailModel.movePlaylistSongs( + viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + detailModel.removePlaylistSong(viewHolder.bindingAdapterPosition) + } +} diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt index a1b9db7fe..9e778cca1 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabAdapter.kt @@ -24,7 +24,7 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemTabBinding -import org.oxycblt.auxio.list.EditableListListener +import org.oxycblt.auxio.list.EditClickListListener import org.oxycblt.auxio.list.recycler.DialogRecyclerView import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.util.inflater @@ -32,9 +32,9 @@ import org.oxycblt.auxio.util.inflater /** * A [RecyclerView.Adapter] that displays an array of [Tab]s open for configuration. * - * @param listener A [EditableListListener] for tab interactions. + * @param listener A [EditClickListListener] for tab interactions. */ -class TabAdapter(private val listener: EditableListListener) : +class TabAdapter(private val listener: EditClickListListener) : RecyclerView.Adapter() { /** The current array of [Tab]s. */ var tabs = arrayOf() @@ -97,10 +97,10 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) : * Bind new data to this instance. * * @param tab The new [Tab] to bind. - * @param listener A [EditableListListener] to bind interactions to. + * @param listener A [EditClickListListener] to bind interactions to. */ @SuppressLint("ClickableViewAccessibility") - fun bind(tab: Tab, listener: EditableListListener) { + fun bind(tab: Tab, listener: EditClickListListener) { listener.bind(tab, this, dragHandle = binding.tabDragHandle) binding.tabCheckBox.apply { // Update the CheckBox name to align with the mode diff --git a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt index 536a205bb..dae73e93e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/tabs/TabCustomizeDialog.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.home.HomeSettings -import org.oxycblt.auxio.list.EditableListListener +import org.oxycblt.auxio.list.EditClickListListener import org.oxycblt.auxio.ui.ViewBindingDialogFragment import org.oxycblt.auxio.util.logD @@ -40,7 +40,7 @@ import org.oxycblt.auxio.util.logD */ @AndroidEntryPoint class TabCustomizeDialog : - ViewBindingDialogFragment(), EditableListListener { + ViewBindingDialogFragment(), EditClickListListener { private val tabAdapter = TabAdapter(this) private var touchHelper: ItemTouchHelper? = null @Inject lateinit var homeSettings: HomeSettings diff --git a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt index 12ef10a50..f557c6946 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/extractor/Components.kt @@ -39,6 +39,7 @@ class SongKeyer @Inject constructor() : Keyer { override fun key(data: Song, options: Options) = "${data.album.uid}${data.album.hashCode()}" } +// TODO: Key on the actual mosaic items used class ParentKeyer @Inject constructor() : Keyer { override fun key(data: MusicParent, options: Options) = "${data.uid}${data.hashCode()}" } diff --git a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt index c102fcfef..d728a6142 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Listeners.kt @@ -50,11 +50,11 @@ interface ClickableListListener { } /** - * An extension of [ClickableListListener] that enables list editing functionality. + * A listener for lists that can be edited. * * @author Alexander Capehart (OxygenCobalt) */ -interface EditableListListener : ClickableListListener { +interface EditableListListener { /** * Called when a [RecyclerView.ViewHolder] requests that it should be dragged. * @@ -62,6 +62,29 @@ interface EditableListListener : ClickableListListener { */ fun onPickUp(viewHolder: RecyclerView.ViewHolder) + /** + * Binds this instance to a list item. + * + * @param viewHolder The [RecyclerView.ViewHolder] to bind. + * @param dragHandle A touchable [View]. Any drag on this view will start a drag event. + */ + fun bind(viewHolder: RecyclerView.ViewHolder, dragHandle: View) { + dragHandle.setOnTouchListener { _, motionEvent -> + dragHandle.performClick() + if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { + onPickUp(viewHolder) + true + } else false + } + } +} + +/** + * A listener for lists that can be clicked and edited at the same time. + * + * @author Alexander Capehart (OxygenCobalt) + */ +interface EditClickListListener : ClickableListListener, EditableListListener { /** * Binds this instance to a list item. * @@ -78,13 +101,7 @@ interface EditableListListener : ClickableListListener { dragHandle: View ) { bind(item, viewHolder, bodyView) - dragHandle.setOnTouchListener { _, motionEvent -> - dragHandle.performClick() - if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { - onPickUp(viewHolder) - true - } else false - } + bind(viewHolder, dragHandle) } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt index 808a8d150..5002e60cf 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/Sort.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/Sort.kt @@ -206,19 +206,6 @@ data class Sort(val mode: Mode, val direction: Direction) { */ fun getPlaylistComparator(direction: Direction): Comparator? = null - /** - * Sort by the item's natural order. - * - * @see Music.name - */ - object ByNone : Mode { - override val intCode: Int - get() = IntegerTable.SORT_BY_NONE - - override val itemId: Int - get() = R.id.option_sort_none - } - /** * Sort by the item's name. * @@ -455,7 +442,6 @@ data class Sort(val mode: Mode, val direction: Direction) { */ fun fromIntCode(intCode: Int) = when (intCode) { - ByNone.intCode -> ByNone ByName.intCode -> ByName ByArtist.intCode -> ByArtist ByAlbum.intCode -> ByAlbum @@ -477,7 +463,6 @@ data class Sort(val mode: Mode, val direction: Direction) { */ fun fromItemId(@IdRes itemId: Int) = when (itemId) { - ByNone.itemId -> ByNone ByName.itemId -> ByName ByAlbum.itemId -> ByAlbum ByArtist.itemId -> ByArtist diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt index a2983755d..ea5629e78 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/MaterialDragCallback.kt @@ -1,6 +1,6 @@ /* * Copyright (c) 2021 Auxio Project - * ExtendedDragCallback.kt is part of Auxio. + * MaterialDragCallback.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 @@ -44,7 +44,7 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() { recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder ) = - if (viewHolder is ViewHolder) { + if (viewHolder is ViewHolder && viewHolder.enabled) { makeFlag( ItemTouchHelper.ACTION_STATE_DRAG, ItemTouchHelper.UP or ItemTouchHelper.DOWN) or makeFlag(ItemTouchHelper.ACTION_STATE_SWIPE, ItemTouchHelper.START) @@ -138,6 +138,8 @@ abstract class MaterialDragCallback : ItemTouchHelper.Callback() { /** Required [RecyclerView.ViewHolder] implementation that exposes the following. */ interface ViewHolder { + /** Whether this [ViewHolder] can be moved right now. */ + val enabled: Boolean /** The root view containing the delete scrim and information. */ val root: View /** The body view containing music information. */ diff --git a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index 5c772f519..5329151ad 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -96,7 +96,7 @@ constructor( is Album -> musicSettings.albumSongSort.songs(it.songs) is Artist -> musicSettings.artistSongSort.songs(it.songs) is Genre -> musicSettings.genreSongSort.songs(it.songs) - is Playlist -> musicSettings.playlistSongSort.songs(it.songs) + is Playlist -> it.songs } } .also { drop() } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt index 91ad069fb..8d7c64f7b 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicRepository.kt @@ -141,6 +141,14 @@ interface MusicRepository { */ fun addToPlaylist(songs: List, playlist: Playlist) + /** + * Update the [Song]s of a [Playlist]. + * + * @param playlist The [Playlist] to update. + * @param songs The new [Song]s to be contained in the [Playlist]. + */ + fun rewritePlaylist(playlist: Playlist, songs: List) + /** * Request that a music loading operation is started by the current [IndexingWorker]. Does * nothing if one is not available. @@ -304,6 +312,15 @@ constructor( } } + override fun rewritePlaylist(playlist: Playlist, songs: List) { + val userLibrary = userLibrary ?: return + userLibrary.rewritePlaylist(playlist, songs) + for (listener in updateListeners) { + listener.onMusicChanges( + MusicRepository.Changes(deviceLibrary = false, userLibrary = true)) + } + } + override fun requestIndex(withCache: Boolean) { indexingWorker?.requestIndex(withCache) } diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt index adcf337c0..48b180388 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt @@ -63,8 +63,6 @@ interface MusicSettings : Settings { var artistSongSort: Sort /** The [Sort] mode used in a [Genre]'s [Song] list. */ var genreSongSort: Sort - /** The [Sort] mode used in a [Playlist]'s [Song] list. */ - var playlistSongSort: Sort interface Listener { /** Called when a setting controlling how music is loaded has changed. */ @@ -225,19 +223,6 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext context: Context } } - override var playlistSongSort: Sort - get() = - Sort.fromIntCode( - sharedPreferences.getInt( - getString(R.string.set_key_playlist_songs_sort), Int.MIN_VALUE)) - ?: Sort(Sort.Mode.ByNone, Sort.Direction.ASCENDING) - set(value) { - sharedPreferences.edit { - putInt(getString(R.string.set_key_playlist_songs_sort), value.intCode) - apply() - } - } - override fun onSettingChanged(key: String, listener: MusicSettings.Listener) { // TODO: Differentiate "hard reloads" (Need the cache) and "Soft reloads" // (just need to manipulate data) diff --git a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt index 56514f7dd..557dda7c4 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/system/IndexerService.kt @@ -131,7 +131,7 @@ class IndexerService : override val scope = indexScope override fun onMusicChanges(changes: MusicRepository.Changes) { - if (!changes.deviceLibrary) return + // TODO: Do not pause when playlist changes val deviceLibrary = musicRepository.deviceLibrary ?: return // Wipe possibly-invalidated outdated covers imageLoader.memoryCache?.clear() diff --git a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt index 563f99316..0760a6f4a 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/user/UserLibrary.kt @@ -101,6 +101,14 @@ interface MutableUserLibrary : UserLibrary { * @param playlist The [Playlist] to add to. Must currently exist. */ fun addToPlaylist(playlist: Playlist, songs: List) + + /** + * Update the [Song]s of a [Playlist]. + * + * @param playlist The [Playlist] to update. + * @param songs The new [Song]s to be contained in the [Playlist]. + */ + fun rewritePlaylist(playlist: Playlist, songs: List) } class UserLibraryFactoryImpl @@ -148,4 +156,11 @@ private class UserLibraryImpl( requireNotNull(playlistMap[playlist.uid]) { "Cannot add to invalid playlist" } playlistMap[playlist.uid] = playlistImpl.edit { addAll(songs) } } + + @Synchronized + override fun rewritePlaylist(playlist: Playlist, songs: List) { + val playlistImpl = + requireNotNull(playlistMap[playlist.uid]) { "Cannot rewrite invalid playlist" } + playlistMap[playlist.uid] = playlistImpl.edit(songs) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt index 81ea0d121..68f2cac16 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/PlaybackViewModel.kt @@ -306,16 +306,14 @@ constructor( "Song to play not in parent" } val deviceLibrary = musicRepository.deviceLibrary ?: return - val sort = + val queue = when (parent) { - is Genre -> musicSettings.genreSongSort - is Artist -> musicSettings.artistSongSort - is Album -> musicSettings.albumSongSort - is Playlist -> musicSettings.playlistSongSort - null -> musicSettings.songSort + is Genre -> musicSettings.genreSongSort.songs(parent.songs) + is Artist -> musicSettings.artistSongSort.songs(parent.songs) + is Album -> musicSettings.albumSongSort.songs(parent.songs) + is Playlist -> parent.songs + null -> musicSettings.songSort.songs(deviceLibrary.songs) } - val songs = parent?.songs ?: deviceLibrary.songs - val queue = sort.songs(songs) playbackManager.play(song, parent, queue, shuffled) } @@ -394,7 +392,7 @@ constructor( * @param playlist The [Playlist] to add. */ fun playNext(playlist: Playlist) { - playbackManager.playNext(musicSettings.playlistSongSort.songs(playlist.songs)) + playbackManager.playNext(playlist.songs) } /** @@ -448,7 +446,7 @@ constructor( * @param playlist The [Playlist] to add. */ fun addToQueue(playlist: Playlist) { - playbackManager.addToQueue(musicSettings.playlistSongSort.songs(playlist.songs)) + playbackManager.addToQueue(playlist.songs) } /** diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt index de1edf36c..76625a038 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueAdapter.kt @@ -27,7 +27,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.shape.MaterialShapeDrawable import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemEditableSongBinding -import org.oxycblt.auxio.list.EditableListListener +import org.oxycblt.auxio.list.EditClickListListener import org.oxycblt.auxio.list.adapter.* import org.oxycblt.auxio.list.recycler.MaterialDragCallback import org.oxycblt.auxio.list.recycler.SongViewHolder @@ -38,10 +38,10 @@ import org.oxycblt.auxio.util.* /** * A [RecyclerView.Adapter] that shows an editable list of queue items. * - * @param listener A [EditableListListener] to bind interactions to. + * @param listener A [EditClickListListener] to bind interactions to. * @author Alexander Capehart (OxygenCobalt) */ -class QueueAdapter(private val listener: EditableListListener) : +class QueueAdapter(private val listener: EditClickListListener) : FlexibleListAdapter(QueueSongViewHolder.DIFF_CALLBACK) { // Since PlayingIndicator adapter relies on an item value, we cannot use it for this // adapter, as one item can appear at several points in the UI. Use a similar implementation @@ -97,22 +97,17 @@ class QueueAdapter(private val listener: EditableListListener) : } /** - * A [PlayingIndicatorAdapter.ViewHolder] that displays an editable [Song] which can be re-ordered - * and removed. Use [from] to create an instance. + * A [PlayingIndicatorAdapter.ViewHolder] that displays an queue [Song] which can be re-ordered and + * removed. Use [from] to create an instance. * * @author Alexander Capehart (OxygenCobalt) */ class QueueSongViewHolder private constructor(private val binding: ItemEditableSongBinding) : PlayingIndicatorAdapter.ViewHolder(binding.root), MaterialDragCallback.ViewHolder { - override val root: View - get() = binding.root - - override val body: View - get() = binding.body - - override val delete: View - get() = binding.background - + override val enabled = true + override val root = binding.root + override val body = binding.body + override val delete = binding.background override val background = MaterialShapeDrawable.createWithElevationOverlay(binding.root.context).apply { fillColor = binding.context.getAttrColorCompat(R.attr.colorSurface) @@ -143,10 +138,10 @@ class QueueSongViewHolder private constructor(private val binding: ItemEditableS * Bind new data to this instance. * * @param song The new [Song] to bind. - * @param listener A [EditableListListener] to bind interactions to. + * @param listener A [EditClickListListener] to bind interactions to. */ @SuppressLint("ClickableViewAccessibility") - fun bind(song: Song, listener: EditableListListener) { + fun bind(song: Song, listener: EditClickListListener) { listener.bind(song, this, body, binding.songDragHandle) binding.songAlbumCover.bind(song) binding.songName.text = song.name.resolve(binding.context) diff --git a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt index e39348451..414ab0eeb 100644 --- a/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/playback/queue/QueueFragment.kt @@ -28,7 +28,7 @@ import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint import kotlin.math.min import org.oxycblt.auxio.databinding.FragmentQueueBinding -import org.oxycblt.auxio.list.EditableListListener +import org.oxycblt.auxio.list.EditClickListListener import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.ViewBindingFragment @@ -40,7 +40,7 @@ import org.oxycblt.auxio.util.collectImmediately * @author Alexander Capehart (OxygenCobalt) */ @AndroidEntryPoint -class QueueFragment : ViewBindingFragment(), EditableListListener { +class QueueFragment : ViewBindingFragment(), EditClickListListener { private val queueModel: QueueViewModel by activityViewModels() private val playbackModel: PlaybackViewModel by activityViewModels() private val queueAdapter = QueueAdapter(this) diff --git a/app/src/main/res/drawable/ic_edit_24.xml b/app/src/main/res/drawable/ic_edit_24.xml new file mode 100644 index 000000000..7d2e3e617 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_24.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/layout/item_edit_header.xml b/app/src/main/res/layout/item_edit_header.xml new file mode 100644 index 000000000..3b999323e --- /dev/null +++ b/app/src/main/res/layout/item_edit_header.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_editable_song.xml b/app/src/main/res/layout/item_editable_song.xml index c8b8dd5a6..9cfa194eb 100644 --- a/app/src/main/res/layout/item_editable_song.xml +++ b/app/src/main/res/layout/item_editable_song.xml @@ -85,6 +85,19 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="@+id/song_album_cover" /> + + diff --git a/app/src/main/res/layout/item_sort_header.xml b/app/src/main/res/layout/item_sort_header.xml index 7f2deab47..ef24e6d6b 100644 --- a/app/src/main/res/layout/item_sort_header.xml +++ b/app/src/main/res/layout/item_sort_header.xml @@ -1,5 +1,4 @@ - Rename playlist Delete Delete playlist? + Edit Search diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicRepository.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicRepository.kt index 600a316d1..735f2fc02 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicRepository.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicRepository.kt @@ -62,6 +62,10 @@ open class FakeMusicRepository : MusicRepository { throw NotImplementedError() } + override fun renamePlaylist(playlist: Playlist, name: String) { + throw NotImplementedError() + } + override fun deletePlaylist(playlist: Playlist) { throw NotImplementedError() } @@ -70,7 +74,7 @@ open class FakeMusicRepository : MusicRepository { throw NotImplementedError() } - override fun renamePlaylist(playlist: Playlist, name: String) { + override fun rewritePlaylist(playlist: Playlist, songs: List) { throw NotImplementedError() } diff --git a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt index 7ad814fc7..66cd8e880 100644 --- a/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt +++ b/app/src/test/java/org/oxycblt/auxio/music/FakeMusicSettings.kt @@ -60,7 +60,4 @@ open class FakeMusicSettings : MusicSettings { override var genreSongSort: Sort get() = throw NotImplementedError() set(_) = throw NotImplementedError() - override var playlistSongSort: Sort - get() = throw NotImplementedError() - set(_) = throw NotImplementedError() }