From cee92c80876a19ab194d50416bffce8d6dbcb280 Mon Sep 17 00:00:00 2001 From: Alexander Capehart Date: Fri, 19 May 2023 14:33:49 -0600 Subject: [PATCH] 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. --- .../auxio/detail/PlaylistDetailFragment.kt | 3 +- .../detail/header/DetailHeaderAdapter.kt | 7 +++ .../header/PlaylistDetailHeaderAdapter.kt | 55 +++++++++++++------ .../detail/list/PlaylistDetailListAdapter.kt | 5 +- .../oxycblt/auxio/image/StyledImageView.kt | 40 ++++++++------ app/src/main/res/drawable/ic_edit_24.xml | 2 +- 6 files changed, 70 insertions(+), 42 deletions(-) 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 0d3bc3d9d..18ecf7956 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/PlaylistDetailFragment.kt @@ -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) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt index 36a30fe24..06317f5e2 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/header/DetailHeaderAdapter.kt @@ -48,6 +48,13 @@ abstract class DetailHeaderAdapter() { + private var editedPlaylist: List? = 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?) { + 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?, + 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.context.getString( - R.string.fmt_two, - binding.context.getPlural(R.plurals.fmt_song_count, playlist.songs.size), - playlist.durationMs.formatDurationMs(true)) - } else { - binding.context.getString(R.string.def_song_count) - } - } + binding.detailInfo.text = + if (songs.isNotEmpty()) { + binding.context.getString( + R.string.fmt_two, + 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() } } } 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 c24683c0e..75306da26 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 @@ -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 { diff --git a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt index 9c6b137d3..2e04617e5 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/StyledImageView.kt @@ -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? = 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, 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 } /** diff --git a/app/src/main/res/drawable/ic_edit_24.xml b/app/src/main/res/drawable/ic_edit_24.xml index 7d2e3e617..9ce54759b 100644 --- a/app/src/main/res/drawable/ic_edit_24.xml +++ b/app/src/main/res/drawable/ic_edit_24.xml @@ -7,5 +7,5 @@ android:tint="?attr/colorControlNormal"> + 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"/>