detail: update playlist header to reflect edits
Make the header information reflect changes in playlist composition as the playlist is edited. This should improve the editing experience to some extent.
This commit is contained in:
parent
5fff1bd0b3
commit
cee92c8087
6 changed files with 70 additions and 42 deletions
|
@ -282,12 +282,11 @@ class PlaylistDetailFragment :
|
|||
// TODO: Disable check item when no edits have been made
|
||||
|
||||
// TODO: Massively improve how this UI is indicated:
|
||||
// - Make playlist header dynamically respond to song changes
|
||||
// - Disable play and pause buttons
|
||||
// - Add an additional toolbar to indicate editing
|
||||
// - Header should flip to re-sort button eventually
|
||||
|
||||
playlistListAdapter.setEditing(editedPlaylist != null)
|
||||
playlistHeaderAdapter.setEditedPlaylist(editedPlaylist)
|
||||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
|
|
|
@ -48,6 +48,13 @@ abstract class DetailHeaderAdapter<T : MusicParent, VH : RecyclerView.ViewHolder
|
|||
*/
|
||||
fun setParent(parent: T) {
|
||||
currentParent = parent
|
||||
rebindParent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces the parent [RecyclerView.ViewHolder] to rebind as soon as possible, with no animation.
|
||||
*/
|
||||
protected fun rebindParent() {
|
||||
notifyItemChanged(0, PAYLOAD_UPDATE_HEADER)
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding
|
||||
import org.oxycblt.auxio.music.Playlist
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
|
@ -38,11 +39,27 @@ import org.oxycblt.auxio.util.inflater
|
|||
*/
|
||||
class PlaylistDetailHeaderAdapter(private val listener: Listener) :
|
||||
DetailHeaderAdapter<Playlist, PlaylistDetailHeaderViewHolder>() {
|
||||
private var editedPlaylist: List<Song>? = null
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||
PlaylistDetailHeaderViewHolder.from(parent)
|
||||
|
||||
override fun onBindHeader(holder: PlaylistDetailHeaderViewHolder, parent: Playlist) =
|
||||
holder.bind(parent, listener)
|
||||
holder.bind(parent, editedPlaylist, listener)
|
||||
|
||||
/**
|
||||
* Indicate to this adapter that editing is ongoing with the current state of the editing
|
||||
* process. This will make the header immediately update to reflect information about the edited
|
||||
* playlist.
|
||||
*/
|
||||
fun setEditedPlaylist(songs: List<Song>?) {
|
||||
if (editedPlaylist == songs) {
|
||||
// Nothing to do.
|
||||
return
|
||||
}
|
||||
editedPlaylist = songs
|
||||
rebindParent()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -58,35 +75,39 @@ private constructor(private val binding: ItemDetailHeaderBinding) :
|
|||
* Bind new data to this instance.
|
||||
*
|
||||
* @param playlist The new [Playlist] to bind.
|
||||
* @param editedPlaylist The current edited state of the playlist, if it exists.
|
||||
* @param listener A [DetailHeaderAdapter.Listener] to bind interactions to.
|
||||
*/
|
||||
fun bind(playlist: Playlist, listener: DetailHeaderAdapter.Listener) {
|
||||
binding.detailCover.bind(playlist)
|
||||
fun bind(
|
||||
playlist: Playlist,
|
||||
editedPlaylist: List<Song>?,
|
||||
listener: DetailHeaderAdapter.Listener
|
||||
) {
|
||||
binding.detailCover.bind(playlist, editedPlaylist)
|
||||
binding.detailType.text = binding.context.getString(R.string.lbl_playlist)
|
||||
binding.detailName.text = playlist.name.resolve(binding.context)
|
||||
// Nothing about a playlist is applicable to the sub-head text.
|
||||
binding.detailSubhead.isVisible = false
|
||||
|
||||
val songs = editedPlaylist ?: playlist.songs
|
||||
val durationMs = editedPlaylist?.sumOf { it.durationMs } ?: playlist.durationMs
|
||||
// The song count of the playlist maps to the info text.
|
||||
binding.detailInfo.apply {
|
||||
isVisible = true
|
||||
text =
|
||||
if (playlist.songs.isNotEmpty()) {
|
||||
binding.detailInfo.text =
|
||||
if (songs.isNotEmpty()) {
|
||||
binding.context.getString(
|
||||
R.string.fmt_two,
|
||||
binding.context.getPlural(R.plurals.fmt_song_count, playlist.songs.size),
|
||||
playlist.durationMs.formatDurationMs(true))
|
||||
binding.context.getPlural(R.plurals.fmt_song_count, songs.size),
|
||||
durationMs.formatDurationMs(true))
|
||||
} else {
|
||||
binding.context.getString(R.string.def_song_count)
|
||||
}
|
||||
}
|
||||
|
||||
binding.detailPlayButton.apply {
|
||||
isEnabled = playlist.songs.isNotEmpty()
|
||||
isEnabled = playlist.songs.isNotEmpty() && editedPlaylist == null
|
||||
setOnClickListener { listener.onPlay() }
|
||||
}
|
||||
binding.detailShuffleButton.apply {
|
||||
isEnabled = playlist.songs.isNotEmpty()
|
||||
isEnabled = playlist.songs.isNotEmpty() && editedPlaylist == null
|
||||
setOnClickListener { listener.onShuffle() }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -278,10 +278,7 @@ private constructor(private val binding: ItemEditableSongBinding) :
|
|||
override fun updateEditing(editing: Boolean) {
|
||||
binding.songDragHandle.isInvisible = !editing
|
||||
binding.songMenu.isInvisible = editing
|
||||
binding.interactBody.apply {
|
||||
isClickable = !editing
|
||||
isFocusable = !editing
|
||||
}
|
||||
binding.interactBody.isEnabled = !editing
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -103,42 +103,47 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
*
|
||||
* @param album the [Album] to bind.
|
||||
*/
|
||||
fun bind(album: Album) = bindImpl(album, R.drawable.ic_album_24, R.string.desc_album_cover)
|
||||
fun bind(album: Album) = bind(album, R.drawable.ic_album_24, R.string.desc_album_cover)
|
||||
|
||||
/**
|
||||
* Bind an [Artist]'s image to this view, also updating the content description.
|
||||
*
|
||||
* @param artist the [Artist] to bind.
|
||||
*/
|
||||
fun bind(artist: Artist) = bindImpl(artist, R.drawable.ic_artist_24, R.string.desc_artist_image)
|
||||
fun bind(artist: Artist) = bind(artist, R.drawable.ic_artist_24, R.string.desc_artist_image)
|
||||
|
||||
/**
|
||||
* Bind an [Genre]'s image to this view, also updating the content description.
|
||||
*
|
||||
* @param genre the [Genre] to bind.
|
||||
*/
|
||||
fun bind(genre: Genre) = bindImpl(genre, R.drawable.ic_genre_24, R.string.desc_genre_image)
|
||||
fun bind(genre: Genre) = bind(genre, R.drawable.ic_genre_24, R.string.desc_genre_image)
|
||||
|
||||
/**
|
||||
* Bind a [Playlist]'s image to this view, also updating the content description.
|
||||
*
|
||||
* @param playlist the [Playlist] to bind.
|
||||
* @param playlist The [Playlist] to bind.
|
||||
* @param songs [Song]s that can override the playlist image if it needs to differ for any
|
||||
* reason.
|
||||
*/
|
||||
fun bind(playlist: Playlist) =
|
||||
bindImpl(playlist, R.drawable.ic_playlist_24, R.string.desc_playlist_image)
|
||||
fun bind(playlist: Playlist, songs: List<Song>? = null) =
|
||||
if (songs != null) {
|
||||
bind(
|
||||
songs,
|
||||
context.getString(R.string.desc_playlist_image, playlist.name.resolve(context)),
|
||||
R.drawable.ic_playlist_24)
|
||||
} else {
|
||||
bind(playlist, R.drawable.ic_playlist_24, R.string.desc_playlist_image)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internally bind a [Music]'s image to this view.
|
||||
*
|
||||
* @param parent The music to bind, in the form of it's [MusicParent]s.
|
||||
* @param errorRes The error drawable resource to use if the music cannot be loaded.
|
||||
* @param descRes The content description string resource to use. The resource must have one
|
||||
* field for the name of the [Music].
|
||||
*/
|
||||
private fun bindImpl(parent: MusicParent, @DrawableRes errorRes: Int, @StringRes descRes: Int) {
|
||||
private fun bind(parent: MusicParent, @DrawableRes errorRes: Int, @StringRes descRes: Int) {
|
||||
bind(parent.songs, context.getString(descRes, parent.name.resolve(context)), errorRes)
|
||||
}
|
||||
|
||||
private fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) {
|
||||
val request =
|
||||
ImageRequest.Builder(context)
|
||||
.data(parent.songs)
|
||||
.data(songs)
|
||||
.error(StyledDrawable(context, context.getDrawableCompat(errorRes)))
|
||||
.transformations(SquareFrameTransform.INSTANCE)
|
||||
.target(this)
|
||||
|
@ -146,8 +151,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
// Dispose of any previous image request and load a new image.
|
||||
CoilUtils.dispose(this)
|
||||
imageLoader.enqueue(request)
|
||||
// Update the content description to the specified resource.
|
||||
contentDescription = context.getString(descRes, parent.name.resolve(context))
|
||||
contentDescription = desc
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,5 +7,5 @@
|
|||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M200,760L256,760L601,415L545,359L200,704L200,760ZM772,357L602,189L658,133Q681,110 714.5,110Q748,110 771,133L827,189Q850,212 851,244.5Q852,277 829,300L772,357ZM714,416L290,840L120,840L120,670L544,246L714,416ZM573,387L545,359L545,359L601,415L601,415L573,387Z"/>
|
||||
android:pathData="M200,760L256,760L601,415L545,359L200,704L200,760ZM772,357L602,189L715,76L884,245L772,357ZM120,840L120,670L544,246L714,416L290,840L120,840ZM573,387L545,359L545,359L601,415L601,415L573,387Z"/>
|
||||
</vector>
|
||||
|
|
Loading…
Reference in a new issue