diff --git a/CHANGELOG.md b/CHANGELOG.md index 68edcef2b..ef1c85f38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,9 @@ ## dev #### What's New +- Improved playing indicators [#218] + - Search and library now show playing indicators + - Playing indicators are now animated when playback is ongoing - Added smooth seeking - Queue now has a fast scroller diff --git a/app/build.gradle b/app/build.gradle index 9454a8aef..a97337868 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -69,6 +69,8 @@ dependencies { // --- SUPPORT --- // General + // 1.4.0 is used in order to avoid a ripple bug in material components + implementation "androidx.appcompat:appcompat:1.4.0" implementation "androidx.core:core-ktx:1.8.0" implementation "androidx.activity:activity-ktx:1.6.0-rc01" implementation "androidx.fragment:fragment-ktx:1.5.2" diff --git a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt index 6a09ec491..185702206 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -96,7 +96,8 @@ class AlbumDetailFragment : collectImmediately(detailModel.currentAlbum, ::handleItemChange) collectImmediately(detailModel.albumData, detailAdapter::submitList) - collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback) + collectImmediately( + playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) collect(navModel.exploreNavigationItem, ::handleNavigation) } @@ -135,6 +136,7 @@ class AlbumDetailFragment : override fun onOpenMenu(item: Item, anchor: View) { if (item is Song) { musicMenu(anchor, R.menu.menu_album_song_actions, item) + return } error("Unexpected datatype when opening menu: ${item::class.java}") @@ -244,7 +246,7 @@ class AlbumDetailFragment : } } - private fun updatePlayback(song: Song?, parent: MusicParent?) { + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { val binding = requireBinding() for (item in binding.detailToolbar.menu.children) { @@ -257,10 +259,10 @@ class AlbumDetailFragment : } if (parent is Album && parent.id == unlikelyToBeNull(detailModel.currentAlbum.value).id) { - detailAdapter.activateSong(song) + detailAdapter.updateIndicator(song, isPlaying) } else { // Clear the ViewHolders if the mode isn't ALL_SONGS - detailAdapter.activateSong(null) + detailAdapter.updateIndicator(null, isPlaying) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt index 5038b7974..6e90e0526 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -91,7 +91,8 @@ class ArtistDetailFragment : collectImmediately(detailModel.currentArtist, ::handleItemChange) collectImmediately(detailModel.artistData, detailAdapter::submitList) - collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback) + collectImmediately( + playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) collect(navModel.exploreNavigationItem, ::handleNavigation) } @@ -200,20 +201,17 @@ class ArtistDetailFragment : } } - private fun updatePlayback(song: Song?, parent: MusicParent?) { - if (parent is Artist && parent.id == unlikelyToBeNull(detailModel.currentArtist.value).id) { - detailAdapter.activateSong(song) - } else { - // Ignore song playback not from the artist - detailAdapter.activateSong(null) + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { + var item: Item? = null + + if (parent is Album) { + item = parent } - if (parent is Album && - parent.artist.id == unlikelyToBeNull(detailModel.currentArtist.value).id) { - detailAdapter.activateAlbum(parent) - } else { - // Ignore album playback not from the artist - detailAdapter.activateAlbum(null) + if (parent is Artist && parent.id == unlikelyToBeNull(detailModel.currentArtist.value).id) { + item = song } + + detailAdapter.updateIndicator(item, isPlaying) } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt index 8f8554346..74227d047 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -92,7 +92,8 @@ class GenreDetailFragment : collectImmediately(detailModel.currentGenre, ::handleItemChange) collectImmediately(detailModel.genreData, detailAdapter::submitList) - collectImmediately(playbackModel.song, playbackModel.parent, ::updatePlayback) + collectImmediately( + playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) collect(navModel.exploreNavigationItem, ::handleNavigation) } @@ -129,10 +130,11 @@ class GenreDetailFragment : } override fun onOpenMenu(item: Item, anchor: View) { - when (item) { - is Song -> musicMenu(anchor, R.menu.menu_song_actions, item) - else -> error("Unexpected datatype when opening menu: ${item::class.java}") + if (item is Song) { + musicMenu(anchor, R.menu.menu_song_actions, item) } + + error("Unexpected datatype when opening menu: ${item::class.java}") } override fun onPlayParent() { @@ -193,12 +195,12 @@ class GenreDetailFragment : } } - private fun updatePlayback(song: Song?, parent: MusicParent?) { + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { if (parent is Genre && parent.id == unlikelyToBeNull(detailModel.currentGenre.value).id) { - detailAdapter.activateSong(song) + detailAdapter.updateIndicator(song, isPlaying) } else { // Ignore song playback not from the genre - detailAdapter.activateSong(null) + detailAdapter.updateIndicator(null, isPlaying) } } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt index 3e01dd638..48af998cb 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/AlbumDetailAdapter.kt @@ -29,6 +29,7 @@ import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding import org.oxycblt.auxio.detail.DiscHeader import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.ui.recycler.IndicatorViewHolder import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.SimpleItemCallback @@ -43,7 +44,6 @@ import org.oxycblt.auxio.util.inflater */ class AlbumDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFFER) { - private var currentSong: Song? = null override fun getItemViewType(position: Int) = when (differ.currentList[position]) { @@ -77,18 +77,6 @@ class AlbumDetailAdapter(private val listener: Listener) : } } - override fun shouldActivateViewHolder(position: Int): Boolean { - val item = differ.currentList[position] - return item is Song && item.id == currentSong?.id - } - - /** Update the [song] that this adapter should indicate playback */ - fun activateSong(song: Song?) { - if (song == currentSong) return - activateImpl(differ.currentList, currentSong, song) - currentSong = song - } - companion object { private val DIFFER = object : SimpleItemCallback() { @@ -182,7 +170,7 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : } private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) : - RecyclerView.ViewHolder(binding.root) { + IndicatorViewHolder(binding.root) { fun bind(item: Song, listener: MenuItemListener) { // Hide the track number view if the song does not have a track. if (item.track != null) { @@ -210,6 +198,11 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA binding.root.setOnClickListener { listener.onItemClick(item) } } + override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) { + binding.root.isActivated = isActive + binding.songTrackBg.isPlaying = isPlaying + } + companion object { const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_SONG diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt index c12dafa93..8f1868664 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/ArtistDetailAdapter.kt @@ -30,6 +30,7 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.resolveYear import org.oxycblt.auxio.ui.recycler.ArtistViewHolder +import org.oxycblt.auxio.ui.recycler.IndicatorViewHolder import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.SimpleItemCallback @@ -44,8 +45,6 @@ import org.oxycblt.auxio.util.inflater */ class ArtistDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFFER) { - private var currentAlbum: Album? = null - private var currentSong: Song? = null override fun getItemViewType(position: Int) = when (differ.currentList[position]) { @@ -79,26 +78,6 @@ class ArtistDetailAdapter(private val listener: Listener) : } } - override fun shouldActivateViewHolder(position: Int): Boolean { - val item = differ.currentList[position] - return (item is Album && item.id == currentAlbum?.id) || - (item is Song && item.id == currentSong?.id) - } - - /** Update the [album] that this adapter should indicate playback */ - fun activateAlbum(album: Album?) { - if (album == currentAlbum) return - activateImpl(differ.currentList, currentAlbum, album) - currentAlbum = album - } - - /** Update the [song] that this adapter should indicate playback */ - fun activateSong(song: Song?) { - if (song == currentSong) return - activateImpl(differ.currentList, currentSong, song) - currentSong = song - } - companion object { private val DIFFER = object : SimpleItemCallback() { @@ -158,7 +137,7 @@ private class ArtistDetailViewHolder private constructor(private val binding: It private class ArtistAlbumViewHolder private constructor( private val binding: ItemParentBinding, -) : RecyclerView.ViewHolder(binding.root) { +) : IndicatorViewHolder(binding.root) { fun bind(item: Album, listener: MenuItemListener) { binding.parentImage.bind(item) binding.parentName.text = item.resolveName(binding.context) @@ -171,6 +150,11 @@ private constructor( binding.root.setOnClickListener { listener.onItemClick(item) } } + override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) { + binding.root.isActivated = isActive + binding.parentImage.isPlaying = isPlaying + } + companion object { const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_ALBUM @@ -188,7 +172,7 @@ private constructor( private class ArtistSongViewHolder private constructor( private val binding: ItemSongBinding, -) : RecyclerView.ViewHolder(binding.root) { +) : IndicatorViewHolder(binding.root) { fun bind(item: Song, listener: MenuItemListener) { binding.songAlbumCover.bind(item) binding.songName.text = item.resolveName(binding.context) @@ -201,6 +185,11 @@ private constructor( binding.root.setOnClickListener { listener.onItemClick(item) } } + override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) { + binding.root.isActivated = isActive + binding.songAlbumCover.isPlaying = isPlaying + } + companion object { const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_SONG diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt index 1d656e4aa..54e27f118 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/DetailAdapter.kt @@ -26,9 +26,9 @@ import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.databinding.ItemSortHeaderBinding import org.oxycblt.auxio.detail.SortHeader -import org.oxycblt.auxio.ui.recycler.ActivationAdapter import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.HeaderViewHolder +import org.oxycblt.auxio.ui.recycler.IndicatorAdapter import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.SimpleItemCallback @@ -38,7 +38,9 @@ import org.oxycblt.auxio.util.inflater abstract class DetailAdapter( private val listener: L, diffCallback: DiffUtil.ItemCallback -) : ActivationAdapter() { +) : IndicatorAdapter() { + private var isPlaying = false + @Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size override fun getItemViewType(position: Int) = @@ -77,7 +79,7 @@ abstract class DetailAdapter( protected val differ = AsyncListDiffer(this, diffCallback) - val currentList: List + override val currentList: List get() = differ.currentList fun submitList(list: List) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt index 462368c82..9802f71c6 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/recycler/GenreDetailAdapter.kt @@ -40,6 +40,7 @@ import org.oxycblt.auxio.util.inflater class GenreDetailAdapter(private val listener: Listener) : DetailAdapter(listener, DIFFER) { private var currentSong: Song? = null + private var isPlaying = false override fun getItemViewType(position: Int) = when (differ.currentList[position]) { @@ -70,18 +71,6 @@ class GenreDetailAdapter(private val listener: Listener) : } } - override fun shouldActivateViewHolder(position: Int): Boolean { - val item = differ.currentList[position] - return item is Song && item.id == currentSong?.id - } - - /** Update the [song] that this adapter should indicate playback */ - fun activateSong(song: Song?) { - if (song == currentSong) return - activateImpl(differ.currentList, currentSong, song) - currentSong = song - } - companion object { val DIFFER = object : SimpleItemCallback() { diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt index f9e030a53..44dd683c0 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/AlbumListFragment.kt @@ -29,8 +29,8 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort -import org.oxycblt.auxio.ui.recycler.ActivationAdapter import org.oxycblt.auxio.ui.recycler.AlbumViewHolder +import org.oxycblt.auxio.ui.recycler.IndicatorAdapter import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.SyncListDiffer @@ -56,7 +56,7 @@ class AlbumListFragment : HomeListFragment() { } collectImmediately(homeModel.albums, homeAdapter::replaceList) - collectImmediately(playbackModel.parent, ::handleParent) + collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handleParent) } override fun getPopup(pos: Int): String? { @@ -109,19 +109,21 @@ class AlbumListFragment : HomeListFragment() { } } - private fun handleParent(parent: MusicParent?) { + private fun handleParent(parent: MusicParent?, isPlaying: Boolean) { if (parent is Album) { - homeAdapter.activateAlbum(parent) + homeAdapter.updateIndicator(parent, isPlaying) } else { // Ignore playback not from albums - homeAdapter.activateAlbum(null) + homeAdapter.updateIndicator(null, isPlaying) } } private class AlbumAdapter(private val listener: MenuItemListener) : - ActivationAdapter() { + IndicatorAdapter() { private val differ = SyncListDiffer(this, AlbumViewHolder.DIFFER) - private var currentAlbum: Album? = null + + override val currentList: List + get() = differ.currentList override fun getItemCount() = differ.currentList.size @@ -136,20 +138,8 @@ class AlbumListFragment : HomeListFragment() { } } - override fun shouldActivateViewHolder(position: Int): Boolean { - val item = differ.currentList[position] - return item.id == currentAlbum?.id - } - fun replaceList(newList: List) { differ.replaceList(newList) } - - /** Update the [album] that this adapter should indicate playback */ - fun activateAlbum(album: Album?) { - if (album == currentAlbum) return - activateImpl(differ.currentList, currentAlbum, album) - currentAlbum = album - } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt index 27600d816..0085cc40e 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/ArtistListFragment.kt @@ -27,8 +27,8 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort -import org.oxycblt.auxio.ui.recycler.ActivationAdapter import org.oxycblt.auxio.ui.recycler.ArtistViewHolder +import org.oxycblt.auxio.ui.recycler.IndicatorAdapter import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.SyncListDiffer @@ -51,7 +51,7 @@ class ArtistListFragment : HomeListFragment() { } collectImmediately(homeModel.artists, homeAdapter::replaceList) - collectImmediately(playbackModel.parent, ::handleParent) + collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handleParent) } override fun getPopup(pos: Int): String? { @@ -85,19 +85,21 @@ class ArtistListFragment : HomeListFragment() { } } - private fun handleParent(parent: MusicParent?) { + private fun handleParent(parent: MusicParent?, isPlaying: Boolean) { if (parent is Artist) { - homeAdapter.activateArtist(parent) + homeAdapter.updateIndicator(parent, isPlaying) } else { // Ignore playback not from artists - homeAdapter.activateArtist(null) + homeAdapter.updateIndicator(null, isPlaying) } } private class ArtistAdapter(private val listener: MenuItemListener) : - ActivationAdapter() { + IndicatorAdapter() { private val differ = SyncListDiffer(this, ArtistViewHolder.DIFFER) - private var currentArtist: Artist? = null + + override val currentList: List + get() = differ.currentList override fun getItemCount() = differ.currentList.size @@ -116,20 +118,8 @@ class ArtistListFragment : HomeListFragment() { } } - override fun shouldActivateViewHolder(position: Int): Boolean { - val item = differ.currentList[position] - return item.id == currentArtist?.id - } - fun replaceList(newList: List) { differ.replaceList(newList) } - - /** Update the [artist] that this adapter should indicate playback */ - fun activateArtist(artist: Artist?) { - if (artist == currentArtist) return - activateImpl(differ.currentList, currentArtist, artist) - currentArtist = artist - } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt index 3ff08fd65..a296d12c9 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/GenreListFragment.kt @@ -27,8 +27,8 @@ import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort -import org.oxycblt.auxio.ui.recycler.ActivationAdapter import org.oxycblt.auxio.ui.recycler.GenreViewHolder +import org.oxycblt.auxio.ui.recycler.IndicatorAdapter import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.SyncListDiffer @@ -51,7 +51,7 @@ class GenreListFragment : HomeListFragment() { } collectImmediately(homeModel.genres, homeAdapter::replaceList) - collectImmediately(playbackModel.parent, ::handlePlayback) + collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handlePlayback) } override fun getPopup(pos: Int): String? { @@ -85,19 +85,21 @@ class GenreListFragment : HomeListFragment() { } } - private fun handlePlayback(parent: MusicParent?) { + private fun handlePlayback(parent: MusicParent?, isPlaying: Boolean) { if (parent is Genre) { - homeAdapter.activateGenre(parent) + homeAdapter.updateIndicator(parent, isPlaying) } else { // Ignore playback not from genres - homeAdapter.activateGenre(null) + homeAdapter.updateIndicator(null, isPlaying) } } private class GenreAdapter(private val listener: MenuItemListener) : - ActivationAdapter() { + IndicatorAdapter() { private val differ = SyncListDiffer(this, GenreViewHolder.DIFFER) - private var currentGenre: Genre? = null + + override val currentList: List + get() = differ.currentList override fun getItemCount() = differ.currentList.size @@ -112,20 +114,8 @@ class GenreListFragment : HomeListFragment() { } } - override fun shouldActivateViewHolder(position: Int): Boolean { - val item = differ.currentList[position] - return item.id == currentGenre?.id - } - fun replaceList(newList: List) { differ.replaceList(newList) } - - /** Update the [genre] that this adapter should indicate playback */ - fun activateGenre(genre: Genre?) { - if (genre == currentGenre) return - activateImpl(differ.currentList, currentGenre, genre) - currentGenre = genre - } } } diff --git a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt index a01b47ee9..f2e44e73c 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/list/SongListFragment.kt @@ -29,7 +29,7 @@ import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.ui.DisplayMode import org.oxycblt.auxio.ui.Sort -import org.oxycblt.auxio.ui.recycler.ActivationAdapter +import org.oxycblt.auxio.ui.recycler.IndicatorAdapter import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.SongViewHolder @@ -58,7 +58,8 @@ class SongListFragment : HomeListFragment() { } collectImmediately(homeModel.songs, homeAdapter::replaceList) - collectImmediately(playbackModel.song, playbackModel.parent, ::handlePlayback) + collectImmediately( + playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback) } override fun getPopup(pos: Int): String? { @@ -113,19 +114,21 @@ class SongListFragment : HomeListFragment() { } } - private fun handlePlayback(song: Song?, parent: MusicParent?) { + private fun handlePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { if (parent == null) { - homeAdapter.activateSong(song) + homeAdapter.updateIndicator(song, isPlaying) } else { // Ignore playback that is not from all songs - homeAdapter.activateSong(null) + homeAdapter.updateIndicator(null, isPlaying) } } private class SongAdapter(private val listener: MenuItemListener) : - ActivationAdapter() { + IndicatorAdapter() { private val differ = SyncListDiffer(this, SongViewHolder.DIFFER) - private var currentSong: Song? = null + + override val currentList: List + get() = differ.currentList override fun getItemCount() = differ.currentList.size @@ -140,20 +143,8 @@ class SongListFragment : HomeListFragment() { } } - override fun shouldActivateViewHolder(position: Int): Boolean { - val item = differ.currentList[position] - return item.id == currentSong?.id - } - fun replaceList(newList: List) { differ.replaceList(newList) } - - /** Update the [song] that this adapter should indicate playback */ - fun activateSong(song: Song?) { - if (song == currentSong) return - activateImpl(differ.currentList, currentSong, song) - currentSong = song - } } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt index 702057f75..01f9f2a66 100644 --- a/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt +++ b/app/src/main/java/org/oxycblt/auxio/image/ImageGroup.kt @@ -30,7 +30,6 @@ import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.util.getColorCompat -import org.oxycblt.auxio.util.getDrawableCompat /** * Effectively a super-charged [StyledImageView]. @@ -52,7 +51,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private val cornerRadius: Float private val inner: StyledImageView private var customView: View? = null - private val indicator: StyledImageView + private val indicator: IndicatorView init { // Android wants you to make separate attributes for each view type, but will @@ -63,11 +62,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr styledAttrs.recycle() inner = StyledImageView(context, attrs) - indicator = - StyledImageView(context).apply { - cornerRadius = this@ImageGroup.cornerRadius - staticIcon = context.getDrawableCompat(R.drawable.ic_currently_playing_24) - } + indicator = IndicatorView(context).apply { cornerRadius = this@ImageGroup.cornerRadius } addView(inner) } @@ -101,6 +96,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr invalidateIndicator() } + fun updateIndicator(isActive: Boolean, isPlaying: Boolean) {} + + var isPlaying: Boolean + get() = indicator.isPlaying + set(value) { + indicator.isPlaying = value + } + override fun setEnabled(enabled: Boolean) { super.setEnabled(enabled) invalidateIndicator() @@ -109,14 +112,14 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr private fun invalidateIndicator() { if (isActivated) { alpha = 1f - indicator.alpha = 1f customView?.alpha = 0f inner.alpha = 0f + indicator.alpha = 1f } else { alpha = if (isEnabled) 1f else 0.5f - indicator.alpha = 0f customView?.alpha = 1f inner.alpha = 1f + indicator.alpha = 0f } } diff --git a/app/src/main/java/org/oxycblt/auxio/image/IndicatorView.kt b/app/src/main/java/org/oxycblt/auxio/image/IndicatorView.kt new file mode 100644 index 000000000..2827afbe6 --- /dev/null +++ b/app/src/main/java/org/oxycblt/auxio/image/IndicatorView.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2022 Auxio Project + * + * 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.image + +import android.content.Context +import android.graphics.Matrix +import android.graphics.RectF +import android.graphics.drawable.AnimationDrawable +import android.util.AttributeSet +import androidx.annotation.AttrRes +import androidx.appcompat.widget.AppCompatImageView +import androidx.core.widget.ImageViewCompat +import com.google.android.material.shape.MaterialShapeDrawable +import kotlin.math.max +import org.oxycblt.auxio.R +import org.oxycblt.auxio.settings.Settings +import org.oxycblt.auxio.util.getColorCompat +import org.oxycblt.auxio.util.getDrawableCompat + +/** + * View that displays the playback indicator. Nominally emulates [StyledImageView], but is + * much different internally as an animated icon can't be wrapped within StyledDrawable without + * causing insane issues. + * @author OxygenCobalt + */ +class IndicatorView +@JvmOverloads +constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : + AppCompatImageView(context, attrs, defStyleAttr) { + private val playingIndicatorDrawable = + context.getDrawableCompat(R.drawable.ic_playing_indicator_24) as AnimationDrawable + + private val pausedIndicatorDrawable = context.getDrawableCompat(R.drawable.ic_paused_indicator_24) + + private val indicatorMatrix = Matrix() + private val indicatorMatrixSrc = RectF() + private val indicatorMatrixDst = RectF() + + private val settings = Settings(context) + + var cornerRadius = 0f + set(value) { + field = value + (background as? MaterialShapeDrawable)?.let { bg -> + if (settings.roundMode) { + bg.setCornerSize(value) + } else { + bg.setCornerSize(0f) + } + } + } + + init { + // Use clipToOutline and a background drawable to crop images. While Coil's transformation + // could theoretically be used to round corners, the corner radius is dependent on the + // dimensions of the image, which will result in inconsistent corners across different + // album covers unless we resize all covers to be the same size. clipToOutline is both + // cheaper and more elegant. As a side-note, this also allows us to re-use the same + // background for both the tonal background color and the corner rounding. + clipToOutline = true + background = + MaterialShapeDrawable().apply { + fillColor = context.getColorCompat(R.color.sel_cover_bg) + setCornerSize(cornerRadius) + } + + scaleType = ScaleType.MATRIX + ImageViewCompat.setImageTintList(this, context.getColorCompat(R.color.sel_on_cover_bg)) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val iconSize = max(measuredWidth, measuredHeight) / 2 + + imageMatrix = + indicatorMatrix.apply { + reset() + drawable?.let { drawable -> + // Android is too good to allow us to set a fixed image size, so we instead need + // to define a matrix to scale an image directly. + + // First scale the icon up to the desired size. + indicatorMatrixSrc.set( + 0f, + 0f, + drawable.intrinsicWidth.toFloat(), + drawable.intrinsicHeight.toFloat()) + indicatorMatrixDst.set(0f, 0f, iconSize.toFloat(), iconSize.toFloat()) + indicatorMatrix.setRectToRect( + indicatorMatrixSrc, indicatorMatrixDst, Matrix.ScaleToFit.CENTER) + + // Then actually center it into the icon, which the previous call does not + // actually do. + indicatorMatrix.postTranslate( + (measuredWidth - iconSize) / 2f, (measuredHeight - iconSize) / 2f) + } + } + } + + var isPlaying: Boolean + get() = drawable == playingIndicatorDrawable + set(value) { + if (value) { + playingIndicatorDrawable.start() + setImageDrawable(playingIndicatorDrawable) + } else { + playingIndicatorDrawable.stop() + setImageDrawable(pausedIndicatorDrawable) + } + } +} 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 aeeb95990..fd410855f 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 @@ -35,6 +35,7 @@ class QueueAdapter(private val listener: QueueItemListener) : RecyclerView.Adapter() { private var differ = SyncListDiffer(this, QueueSongViewHolder.DIFFER) private var currentIndex = 0 + private var isPlaying = false override fun getItemCount() = differ.currentList.size @@ -54,7 +55,7 @@ class QueueAdapter(private val listener: QueueItemListener) : } viewHolder.isEnabled = position > currentIndex - viewHolder.isActivated = position == currentIndex + viewHolder.updateIndicator(position == currentIndex, isPlaying) } fun submitList(newList: List) { @@ -65,16 +66,30 @@ class QueueAdapter(private val listener: QueueItemListener) : differ.replaceList(newList) } - fun updateIndex(index: Int) { - when { - index < currentIndex -> { - val lastIndex = currentIndex - currentIndex = index - notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_INDEX) + fun updateIndicator(index: Int, isPlaying: Boolean) { + var updatedIndex = false + + if (index != currentIndex) { + when { + index < currentIndex -> { + val lastIndex = currentIndex + currentIndex = index + notifyItemRangeChanged(0, lastIndex + 1, PAYLOAD_UPDATE_INDEX) + } + else -> { + currentIndex = index + notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_INDEX) + } } - index > currentIndex -> { - currentIndex = index - notifyItemRangeChanged(0, currentIndex + 1, PAYLOAD_UPDATE_INDEX) + + updatedIndex = true + } + + if (this.isPlaying != isPlaying) { + this.isPlaying = isPlaying + + if (!updatedIndex) { + notifyItemChanged(index, PAYLOAD_UPDATE_INDEX) } } } @@ -92,7 +107,7 @@ interface QueueItemListener { class QueueSongViewHolder private constructor( private val binding: ItemQueueSongBinding, -) : RecyclerView.ViewHolder(binding.root) { +) : IndicatorViewHolder(binding.root) { val bodyView: View get() = binding.body val backgroundView: View @@ -146,12 +161,10 @@ private constructor( binding.songDragHandle.isEnabled = value } - var isActivated: Boolean - get() = binding.interactBody.isActivated - set(value) { - // Activation does not affect clicking, make everything activated. - binding.interactBody.isActivated = value - } + override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) { + binding.interactBody.isActivated = isActive + binding.songAlbumCover.isPlaying = isPlaying + } companion object { fun new(parent: View) = 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 285292d5c..b79b1852c 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 @@ -27,7 +27,9 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.fragment.ViewBindingFragment +import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.logD @@ -38,6 +40,7 @@ import org.oxycblt.auxio.util.logD */ class QueueFragment : ViewBindingFragment(), QueueItemListener { private val queueModel: QueueViewModel by activityViewModels() + private val playbackModel: PlaybackViewModel by androidActivityViewModels() private val queueAdapter = QueueAdapter(this) private val touchHelper: ItemTouchHelper by lifecycleObject { ItemTouchHelper(QueueDragCallback(queueModel)) @@ -63,7 +66,8 @@ class QueueFragment : ViewBindingFragment(), QueueItemList // --- VIEWMODEL SETUP ---- - collectImmediately(queueModel.queue, queueModel.index, ::updateQueue) + collectImmediately( + queueModel.queue, queueModel.index, playbackModel.isPlaying, ::updateQueue) } override fun onDestroyBinding(binding: FragmentQueueBinding) { @@ -79,7 +83,7 @@ class QueueFragment : ViewBindingFragment(), QueueItemList touchHelper.startDrag(viewHolder) } - private fun updateQueue(queue: List, index: Int) { + private fun updateQueue(queue: List, index: Int, isPlaying: Boolean) { val binding = requireBinding() val replaceQueue = queueModel.replaceQueue @@ -111,7 +115,7 @@ class QueueFragment : ViewBindingFragment(), QueueItemList queueModel.finishScrollTo() - queueAdapter.updateIndex(index) + queueAdapter.updateIndicator(index, isPlaying) } private fun invalidateDivider() { diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt index 65f104448..facb7fdfc 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchAdapter.kt @@ -24,24 +24,20 @@ import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song -import org.oxycblt.auxio.ui.recycler.ActivationAdapter import org.oxycblt.auxio.ui.recycler.AlbumViewHolder import org.oxycblt.auxio.ui.recycler.ArtistViewHolder import org.oxycblt.auxio.ui.recycler.GenreViewHolder import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.ui.recycler.HeaderViewHolder +import org.oxycblt.auxio.ui.recycler.IndicatorAdapter import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.ui.recycler.SimpleItemCallback import org.oxycblt.auxio.ui.recycler.SongViewHolder class SearchAdapter(private val listener: MenuItemListener) : - ActivationAdapter() { + IndicatorAdapter() { private val differ = AsyncListDiffer(this, DIFFER) - private var currentSong: Song? = null - private var currentAlbum: Album? = null - private var currentArtist: Artist? = null - private var currentGenre: Genre? = null override fun getItemCount() = differ.currentList.size @@ -83,44 +79,11 @@ class SearchAdapter(private val listener: MenuItemListener) : } } - override fun shouldActivateViewHolder(position: Int): Boolean { - val item = differ.currentList[position] - - return (item is Song && item.id == currentSong?.id) || - (item is Album && item.id == currentAlbum?.id) || - (item is Artist && item.id == currentArtist?.id) || - (item is Genre && item.id == currentGenre?.id) - } - - val currentList: List + override val currentList: List get() = differ.currentList fun submitList(list: List, callback: () -> Unit) = differ.submitList(list, callback) - fun activateSong(song: Song?) { - if (song == currentSong) return - activateImpl(differ.currentList, currentSong, song) - currentSong = song - } - - fun activateAlbum(album: Album?) { - if (album == currentAlbum) return - activateImpl(differ.currentList, currentAlbum, album) - currentAlbum = album - } - - fun activateArtist(artist: Artist?) { - if (artist == currentArtist) return - activateImpl(differ.currentList, currentArtist, artist) - currentArtist = artist - } - - fun activateGenre(genre: Genre?) { - if (genre == currentGenre) return - activateImpl(differ.currentList, currentGenre, genre) - currentGenre = genre - } - companion object { private val DIFFER = object : SimpleItemCallback() { diff --git a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt index 2861d3af9..0990b4a5e 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -112,7 +112,8 @@ class SearchFragment : // --- VIEWMODEL SETUP --- collectImmediately(searchModel.searchResults, ::handleResults) - collectImmediately(playbackModel.song, playbackModel.parent, ::handlePlayback) + collectImmediately( + playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback) collect(navModel.exploreNavigationItem, ::handleNavigation) } @@ -165,34 +166,8 @@ class SearchFragment : binding.searchRecycler.isInvisible = results.isEmpty() } - private fun handlePlayback(song: Song?, parent: MusicParent?) { - if (parent == null) { - searchAdapter.activateSong(song) - } else { - // Ignore playback not from all songs - searchAdapter.activateSong(null) - } - - if (parent is Album) { - searchAdapter.activateAlbum(parent) - } else { - // Ignore playback not from albums - searchAdapter.activateAlbum(null) - } - - if (parent is Artist) { - searchAdapter.activateArtist(parent) - } else { - // Ignore playback not from artists - searchAdapter.activateArtist(null) - } - - if (parent is Genre) { - searchAdapter.activateGenre(parent) - } else { - // Ignore playback not from artists - searchAdapter.activateGenre(null) - } + private fun handlePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { + searchAdapter.updateIndicator(parent ?: song, isPlaying) } private fun handleNavigation(item: Music?) { diff --git a/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt b/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt index 81067b726..8f1dfea74 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/recycler/RecyclerFramework.kt @@ -173,42 +173,84 @@ abstract class SimpleItemCallback : DiffUtil.ItemCallback() { } } -abstract class ActivationAdapter : RecyclerView.Adapter() { +// TODO: Base adapter that automates current list stuff for span size lookup +// TODO: Dialog view holder that automates the dumb sizing hack I have to do + +abstract class IndicatorAdapter : RecyclerView.Adapter() { + private var isPlaying = false + private var currentItem: Item? = null + override fun onBindViewHolder(holder: VH, position: Int) = throw UnsupportedOperationException() override fun onBindViewHolder(holder: VH, position: Int, payloads: List) { - holder.itemView.isActivated = shouldActivateViewHolder(position) + if (holder is IndicatorViewHolder) { + val item = currentList[position] + val currentItem = currentItem + holder.updateIndicator( + currentItem != null && + item.javaClass == currentItem.javaClass && + item.id == currentItem.id, + isPlaying) + } } - protected abstract fun shouldActivateViewHolder(position: Int): Boolean + abstract val currentList: List - protected inline fun activateImpl( - currentList: List, - oldItem: T?, - newItem: T? - ) { - if (oldItem != null) { - val pos = currentList.indexOfFirst { item -> item.id == oldItem.id && item is T } + fun updateIndicator(item: Item?, isPlaying: Boolean) { + var updatedItem = false - if (pos > -1) { - notifyItemChanged(pos, PAYLOAD_ACTIVATION_CHANGED) - } else { - logW("oldItem was not in adapter data") + if (currentItem != item) { + val oldItem = currentItem + currentItem = item + + if (oldItem != null) { + val pos = + currentList.indexOfFirst { + it.javaClass == oldItem.javaClass && it.id == oldItem.id + } + + if (pos > -1) { + notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED) + } else { + logW("oldItem was not in adapter data") + } } + + if (item != null) { + val pos = + currentList.indexOfFirst { it.javaClass == item.javaClass && it.id == item.id } + + if (pos > -1) { + notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED) + } else { + logW("newItem was not in adapter data") + } + } + + updatedItem = true } - if (newItem != null) { - val pos = currentList.indexOfFirst { item -> item is T && item.id == newItem.id } + if (this.isPlaying != isPlaying) { + this.isPlaying = isPlaying - if (pos > -1) { - notifyItemChanged(pos, PAYLOAD_ACTIVATION_CHANGED) - } else { - logW("newItem was not in adapter data") + if (!updatedItem && item != null) { + val pos = + currentList.indexOfFirst { it.javaClass == item.javaClass && it.id == item.id } + + if (pos > -1) { + notifyItemChanged(pos, PAYLOAD_INDICATOR_CHANGED) + } else { + logW("newItem was not in adapter data") + } } } } companion object { - val PAYLOAD_ACTIVATION_CHANGED = Any() + val PAYLOAD_INDICATOR_CHANGED = Any() } } + +abstract class IndicatorViewHolder(root: View) : RecyclerView.ViewHolder(root) { + abstract fun updateIndicator(isActive: Boolean, isPlaying: Boolean) +} diff --git a/app/src/main/java/org/oxycblt/auxio/ui/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/ui/recycler/ViewHolders.kt index 49feac7d1..393b0bb9a 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/recycler/ViewHolders.kt @@ -37,7 +37,7 @@ import org.oxycblt.auxio.util.inflater * @author OxygenCobalt */ class SongViewHolder private constructor(private val binding: ItemSongBinding) : - RecyclerView.ViewHolder(binding.root) { + IndicatorViewHolder(binding.root) { fun bind(item: Song, listener: MenuItemListener) { binding.songAlbumCover.bind(item) binding.songName.text = item.resolveName(binding.context) @@ -50,6 +50,11 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : binding.root.setOnClickListener { listener.onItemClick(item) } } + override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) { + binding.root.isActivated = isActive + binding.songAlbumCover.isPlaying = isPlaying + } + companion object { const val VIEW_TYPE = IntegerTable.VIEW_TYPE_SONG @@ -71,7 +76,7 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : class AlbumViewHolder private constructor( private val binding: ItemParentBinding, -) : RecyclerView.ViewHolder(binding.root) { +) : IndicatorViewHolder(binding.root) { fun bind(item: Album, listener: MenuItemListener) { binding.parentImage.bind(item) @@ -85,6 +90,11 @@ private constructor( binding.root.setOnClickListener { listener.onItemClick(item) } } + override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) { + binding.root.isActivated = isActive + binding.parentImage.isPlaying = isPlaying + } + companion object { const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM @@ -105,7 +115,7 @@ private constructor( * @author OxygenCobalt */ class ArtistViewHolder private constructor(private val binding: ItemParentBinding) : - RecyclerView.ViewHolder(binding.root) { + IndicatorViewHolder(binding.root) { fun bind(item: Artist, listener: MenuItemListener) { binding.parentImage.bind(item) @@ -123,6 +133,11 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin binding.root.setOnClickListener { listener.onItemClick(item) } } + override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) { + binding.root.isActivated = isActive + binding.parentImage.isPlaying = isPlaying + } + companion object { const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST @@ -145,7 +160,7 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin class GenreViewHolder private constructor( private val binding: ItemParentBinding, -) : RecyclerView.ViewHolder(binding.root) { +) : IndicatorViewHolder(binding.root) { fun bind(item: Genre, listener: MenuItemListener) { binding.parentImage.bind(item) @@ -160,6 +175,11 @@ private constructor( binding.root.setOnClickListener { listener.onItemClick(item) } } + override fun updateIndicator(isActive: Boolean, isPlaying: Boolean) { + binding.root.isActivated = isActive + binding.parentImage.isPlaying = isPlaying + } + companion object { const val VIEW_TYPE = IntegerTable.VIEW_TYPE_GENRE diff --git a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt index 71cc733ee..24ca26d1f 100644 --- a/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/util/FrameworkUtil.kt @@ -144,6 +144,18 @@ fun Fragment.collectImmediately( launch { combine.collect { block(it.first, it.second) } } } +/** Like [collectImmediately], but with three [StateFlow] values. */ +fun Fragment.collectImmediately( + a: StateFlow, + b: StateFlow, + c: StateFlow, + block: (T1, T2, T3) -> Unit +) { + block(a.value, b.value, c.value) + val combine = combine(a, b, c) { a1, b2, c3 -> Triple(a1, b2, c3) } + launch { combine.collect { block(it.first, it.second, it.third) } } +} + /** * Launches [block] in a lifecycle-aware coroutine once [state] is reached. This is primarily a * shortcut intended to correctly launch a co-routine on a fragment in a way that won't cause diff --git a/app/src/main/res/drawable/ic_currently_playing_24.xml b/app/src/main/res/drawable/ic_currently_playing_24.xml deleted file mode 100644 index 0e518ab3d..000000000 --- a/app/src/main/res/drawable/ic_currently_playing_24.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_paused_indicator_24.xml b/app/src/main/res/drawable/ic_paused_indicator_24.xml new file mode 100644 index 000000000..714e83b9c --- /dev/null +++ b/app/src/main/res/drawable/ic_paused_indicator_24.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/drawable/ic_playing_indicator_24.xml b/app/src/main/res/drawable/ic_playing_indicator_24.xml new file mode 100644 index 000000000..fe6877d05 --- /dev/null +++ b/app/src/main/res/drawable/ic_playing_indicator_24.xml @@ -0,0 +1,492 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +