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: Disable check item when no edits have been made
|
||||||
|
|
||||||
// TODO: Massively improve how this UI is indicated:
|
// 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
|
// - Add an additional toolbar to indicate editing
|
||||||
// - Header should flip to re-sort button eventually
|
// - Header should flip to re-sort button eventually
|
||||||
|
|
||||||
playlistListAdapter.setEditing(editedPlaylist != null)
|
playlistListAdapter.setEditing(editedPlaylist != null)
|
||||||
|
playlistHeaderAdapter.setEditedPlaylist(editedPlaylist)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
|
|
|
@ -48,6 +48,13 @@ abstract class DetailHeaderAdapter<T : MusicParent, VH : RecyclerView.ViewHolder
|
||||||
*/
|
*/
|
||||||
fun setParent(parent: T) {
|
fun setParent(parent: T) {
|
||||||
currentParent = parent
|
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)
|
notifyItemChanged(0, PAYLOAD_UPDATE_HEADER)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding
|
import org.oxycblt.auxio.databinding.ItemDetailHeaderBinding
|
||||||
import org.oxycblt.auxio.music.Playlist
|
import org.oxycblt.auxio.music.Playlist
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.playback.formatDurationMs
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.getPlural
|
import org.oxycblt.auxio.util.getPlural
|
||||||
|
@ -38,11 +39,27 @@ import org.oxycblt.auxio.util.inflater
|
||||||
*/
|
*/
|
||||||
class PlaylistDetailHeaderAdapter(private val listener: Listener) :
|
class PlaylistDetailHeaderAdapter(private val listener: Listener) :
|
||||||
DetailHeaderAdapter<Playlist, PlaylistDetailHeaderViewHolder>() {
|
DetailHeaderAdapter<Playlist, PlaylistDetailHeaderViewHolder>() {
|
||||||
|
private var editedPlaylist: List<Song>? = null
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
PlaylistDetailHeaderViewHolder.from(parent)
|
PlaylistDetailHeaderViewHolder.from(parent)
|
||||||
|
|
||||||
override fun onBindHeader(holder: PlaylistDetailHeaderViewHolder, parent: Playlist) =
|
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.
|
* Bind new data to this instance.
|
||||||
*
|
*
|
||||||
* @param playlist The new [Playlist] to bind.
|
* @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.
|
* @param listener A [DetailHeaderAdapter.Listener] to bind interactions to.
|
||||||
*/
|
*/
|
||||||
fun bind(playlist: Playlist, listener: DetailHeaderAdapter.Listener) {
|
fun bind(
|
||||||
binding.detailCover.bind(playlist)
|
playlist: Playlist,
|
||||||
|
editedPlaylist: List<Song>?,
|
||||||
|
listener: DetailHeaderAdapter.Listener
|
||||||
|
) {
|
||||||
|
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)
|
||||||
// Nothing about a playlist is applicable to the sub-head text.
|
// Nothing about a playlist is applicable to the sub-head text.
|
||||||
binding.detailSubhead.isVisible = false
|
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.
|
// The song count of the playlist maps to the info text.
|
||||||
binding.detailInfo.apply {
|
binding.detailInfo.text =
|
||||||
isVisible = true
|
if (songs.isNotEmpty()) {
|
||||||
text =
|
binding.context.getString(
|
||||||
if (playlist.songs.isNotEmpty()) {
|
R.string.fmt_two,
|
||||||
binding.context.getString(
|
binding.context.getPlural(R.plurals.fmt_song_count, songs.size),
|
||||||
R.string.fmt_two,
|
durationMs.formatDurationMs(true))
|
||||||
binding.context.getPlural(R.plurals.fmt_song_count, playlist.songs.size),
|
} else {
|
||||||
playlist.durationMs.formatDurationMs(true))
|
binding.context.getString(R.string.def_song_count)
|
||||||
} else {
|
}
|
||||||
binding.context.getString(R.string.def_song_count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.detailPlayButton.apply {
|
binding.detailPlayButton.apply {
|
||||||
isEnabled = playlist.songs.isNotEmpty()
|
isEnabled = playlist.songs.isNotEmpty() && editedPlaylist == null
|
||||||
setOnClickListener { listener.onPlay() }
|
setOnClickListener { listener.onPlay() }
|
||||||
}
|
}
|
||||||
binding.detailShuffleButton.apply {
|
binding.detailShuffleButton.apply {
|
||||||
isEnabled = playlist.songs.isNotEmpty()
|
isEnabled = playlist.songs.isNotEmpty() && editedPlaylist == null
|
||||||
setOnClickListener { listener.onShuffle() }
|
setOnClickListener { listener.onShuffle() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -278,10 +278,7 @@ private constructor(private val binding: ItemEditableSongBinding) :
|
||||||
override fun updateEditing(editing: Boolean) {
|
override fun updateEditing(editing: Boolean) {
|
||||||
binding.songDragHandle.isInvisible = !editing
|
binding.songDragHandle.isInvisible = !editing
|
||||||
binding.songMenu.isInvisible = editing
|
binding.songMenu.isInvisible = editing
|
||||||
binding.interactBody.apply {
|
binding.interactBody.isEnabled = !editing
|
||||||
isClickable = !editing
|
|
||||||
isFocusable = !editing
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -103,42 +103,47 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
*
|
*
|
||||||
* @param album the [Album] to bind.
|
* @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.
|
* Bind an [Artist]'s image to this view, also updating the content description.
|
||||||
*
|
*
|
||||||
* @param artist the [Artist] to bind.
|
* @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.
|
* Bind an [Genre]'s image to this view, also updating the content description.
|
||||||
*
|
*
|
||||||
* @param genre the [Genre] to bind.
|
* @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.
|
* 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) =
|
fun bind(playlist: Playlist, songs: List<Song>? = null) =
|
||||||
bindImpl(playlist, R.drawable.ic_playlist_24, R.string.desc_playlist_image)
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
private fun bind(parent: MusicParent, @DrawableRes errorRes: Int, @StringRes descRes: Int) {
|
||||||
* Internally bind a [Music]'s image to this view.
|
bind(parent.songs, context.getString(descRes, parent.name.resolve(context)), errorRes)
|
||||||
*
|
}
|
||||||
* @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.
|
private fun bind(songs: List<Song>, desc: String, @DrawableRes errorRes: Int) {
|
||||||
* @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) {
|
|
||||||
val request =
|
val request =
|
||||||
ImageRequest.Builder(context)
|
ImageRequest.Builder(context)
|
||||||
.data(parent.songs)
|
.data(songs)
|
||||||
.error(StyledDrawable(context, context.getDrawableCompat(errorRes)))
|
.error(StyledDrawable(context, context.getDrawableCompat(errorRes)))
|
||||||
.transformations(SquareFrameTransform.INSTANCE)
|
.transformations(SquareFrameTransform.INSTANCE)
|
||||||
.target(this)
|
.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.
|
// Dispose of any previous image request and load a new image.
|
||||||
CoilUtils.dispose(this)
|
CoilUtils.dispose(this)
|
||||||
imageLoader.enqueue(request)
|
imageLoader.enqueue(request)
|
||||||
// Update the content description to the specified resource.
|
contentDescription = desc
|
||||||
contentDescription = context.getString(descRes, parent.name.resolve(context))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,5 +7,5 @@
|
||||||
android:tint="?attr/colorControlNormal">
|
android:tint="?attr/colorControlNormal">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
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>
|
</vector>
|
||||||
|
|
Loading…
Reference in a new issue