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 ad08dfdb7..d51887ab8 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -65,7 +65,9 @@ class AlbumDetailFragment : DetailFragment() { } } - setupRecycler(detailAdapter) + setupRecycler(detailAdapter) { pos -> + pos == 0 + } // -- DETAILVIEWMODEL SETUP --- 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 a5e1e49ce..f9cbdf794 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -6,19 +6,22 @@ import android.view.View import android.view.ViewGroup import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import org.oxycblt.auxio.R import org.oxycblt.auxio.detail.adapters.ArtistDetailAdapter import org.oxycblt.auxio.logD +import org.oxycblt.auxio.music.ActionHeader import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.Song +import org.oxycblt.auxio.playback.state.PlaybackMode +import org.oxycblt.auxio.recycler.SortMode import org.oxycblt.auxio.ui.ActionMenu import org.oxycblt.auxio.ui.newMenu /** * The [DetailFragment] for an artist. - * TODO: Show a list of songs? * @author OxygenCobalt */ class ArtistDetailFragment : DetailFragment() { @@ -42,14 +45,18 @@ class ArtistDetailFragment : DetailFragment() { } val detailAdapter = ArtistDetailAdapter( - detailModel, playbackModel, viewLifecycleOwner, - doOnClick = { album -> - if (!detailModel.isNavigating) { - detailModel.setNavigating(true) + playbackModel, + doOnClick = { data -> + if (data is Album) { + if (!detailModel.isNavigating) { + detailModel.setNavigating(true) - findNavController().navigate( - ArtistDetailFragmentDirections.actionShowAlbum(album.id) - ) + findNavController().navigate( + ArtistDetailFragmentDirections.actionShowAlbum(data.id) + ) + } + } else if (data is Song) { + playbackModel.playSong(data, PlaybackMode.IN_ARTIST) } }, doOnLongClick = { view, data -> @@ -57,24 +64,40 @@ class ArtistDetailFragment : DetailFragment() { } ) + // We build the action header here since it's both more efficent to keep one action header + // and it also prevents the header from being constantly refreshed when the sort is updated. + + val songsHeader = ActionHeader( + id = -2, + name = getString(R.string.label_songs), + icon = detailModel.artistSortMode.value!!.iconRes, + ) { btn -> + detailModel.incrementArtistSortMode() + + // We'll update the icon of this header object directly so that the state persists + // after the viewholder is recycled. + icon = detailModel.artistSortMode.value!!.iconRes + btn.setImageResource(icon) + } + // --- UI SETUP --- binding.lifecycleOwner = this setupToolbar() - setupRecycler(detailAdapter) + setupRecycler(detailAdapter) { pos -> + // If the item is an ActionHeader we need to also make the item full-width + pos == 0 || detailAdapter.currentList.getOrNull(pos) is ActionHeader + } + + detailAdapter.submitList(createData(songsHeader, detailModel.artistSortMode.value!!)) // --- VIEWMODEL SETUP --- detailModel.artistSortMode.observe(viewLifecycleOwner) { mode -> logD("Updating sort mode to $mode") - // Header detail data is always included - val data = mutableListOf(detailModel.currentArtist.value!!).also { - it.addAll(mode.getSortedAlbumList(detailModel.currentArtist.value!!.albums)) - } - - detailAdapter.submitList(data) + detailAdapter.submitList(createData(songsHeader, mode)) } detailModel.navToItem.observe(viewLifecycleOwner) { item -> @@ -105,9 +128,21 @@ class ArtistDetailFragment : DetailFragment() { // Highlight albums if they are being played playbackModel.parent.observe(viewLifecycleOwner) { parent -> if (parent is Album?) { - detailAdapter.setCurrentAlbum(parent, binding.detailRecycler) + detailAdapter.highlightAlbum(parent, binding.detailRecycler) } else { - detailAdapter.setCurrentAlbum(null, binding.detailRecycler) + detailAdapter.highlightAlbum(null, binding.detailRecycler) + } + } + + // Highlight songs if they are being played + playbackModel.song.observe(viewLifecycleOwner) { song -> + if (playbackModel.mode.value == PlaybackMode.IN_ARTIST && + playbackModel.parent.value?.id == detailModel.currentArtist.value!!.id + ) { + detailAdapter.highlightSong(song, binding.detailRecycler) + } else { + // Clear the viewholders if the mode isn't ALL_SONGS + detailAdapter.highlightSong(null, binding.detailRecycler) } } @@ -115,4 +150,15 @@ class ArtistDetailFragment : DetailFragment() { return binding.root } + + private fun createData(songHeader: ActionHeader, mode: SortMode): MutableList { + val artist = detailModel.currentArtist.value!! + + val data = mutableListOf(artist) + data.addAll(SortMode.NUMERIC_DOWN.getSortedAlbumList(artist.albums)) + data.add(songHeader) + data.addAll(mode.getSortedArtistSongList(artist.songs)) + + return data + } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt index 46b166085..cef8ec2e8 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailFragment.kt @@ -8,10 +8,8 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.FragmentDetailBinding -import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.ui.fixAnimInfoLeak import org.oxycblt.auxio.ui.isLandscape @@ -78,7 +76,10 @@ abstract class DetailFragment : Fragment() { /** * Shortcut method for recyclerview setup */ - protected fun setupRecycler(detailAdapter: ListAdapter) { + protected fun setupRecycler( + detailAdapter: RecyclerView.Adapter, + gridLookup: (Int) -> Boolean + ) { binding.detailRecycler.apply { adapter = detailAdapter setHasFixedSize(true) @@ -88,7 +89,7 @@ abstract class DetailFragment : Fragment() { layoutManager = GridLayoutManager(requireContext(), 2).also { it.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { - return if (position == 0) 2 else 1 + return if (gridLookup(position)) 2 else 1 } } } 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 ef85b5d2a..cf1ee9355 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -56,7 +56,9 @@ class GenreDetailFragment : DetailFragment() { binding.lifecycleOwner = this setupToolbar() - setupRecycler(detailAdapter) + setupRecycler(detailAdapter) { pos -> + pos == 0 + } // --- DETAILVIEWMODEL SETUP --- diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt index aa2f11fe0..d0f42ea7b 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/ArtistDetailAdapter.kt @@ -2,21 +2,23 @@ package org.oxycblt.auxio.detail.adapters import android.view.View import android.view.ViewGroup -import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import org.oxycblt.auxio.databinding.ItemArtistAlbumBinding import org.oxycblt.auxio.databinding.ItemArtistHeaderBinding -import org.oxycblt.auxio.detail.DetailViewModel +import org.oxycblt.auxio.databinding.ItemArtistSongBinding +import org.oxycblt.auxio.logD +import org.oxycblt.auxio.music.ActionHeader import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.BaseModel +import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.recycler.DiffCallback +import org.oxycblt.auxio.recycler.viewholders.ActionHeaderViewHolder import org.oxycblt.auxio.recycler.viewholders.BaseViewHolder import org.oxycblt.auxio.recycler.viewholders.Highlightable import org.oxycblt.auxio.ui.Accent -import org.oxycblt.auxio.ui.disable import org.oxycblt.auxio.ui.inflater import org.oxycblt.auxio.ui.setTextColorResource @@ -25,19 +27,22 @@ import org.oxycblt.auxio.ui.setTextColorResource * @author OxygenCobalt */ class ArtistDetailAdapter( - private val detailModel: DetailViewModel, private val playbackModel: PlaybackViewModel, - private val lifecycleOwner: LifecycleOwner, - private val doOnClick: (data: Album) -> Unit, - private val doOnLongClick: (view: View, data: Album) -> Unit, + private val doOnClick: (data: BaseModel) -> Unit, + private val doOnLongClick: (view: View, data: BaseModel) -> Unit, ) : ListAdapter(DiffCallback()) { private var currentAlbum: Album? = null - private var lastHolder: Highlightable? = null + private var currentAlbumHolder: Highlightable? = null + + private var currentSong: Song? = null + private var currentSongHolder: Highlightable? = null override fun getItemViewType(position: Int): Int { return when (getItem(position)) { is Artist -> ARTIST_HEADER_ITEM_TYPE is Album -> ARTIST_ALBUM_ITEM_TYPE + is ActionHeader -> ActionHeaderViewHolder.ITEM_TYPE + is Song -> ARTIST_SONG_ITEM_TYPE else -> -1 } @@ -53,24 +58,43 @@ class ArtistDetailAdapter( ItemArtistAlbumBinding.inflate(parent.context.inflater) ) + ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context) + + ARTIST_SONG_ITEM_TYPE -> ArtistSongViewHolder( + ItemArtistSongBinding.inflate(parent.context.inflater) + ) + else -> error("Invalid ViewHolder item type $viewType") } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (val item = getItem(position)) { + val item = getItem(position) + + when (item) { is Artist -> (holder as ArtistHeaderViewHolder).bind(item) is Album -> (holder as ArtistAlbumViewHolder).bind(item) + is ActionHeader -> (holder as ActionHeaderViewHolder).bind(item) + is Song -> (holder as ArtistSongViewHolder).bind(item) + + else -> {} } - if (currentAlbum != null && position > 0) { - if (getItem(position).id == currentAlbum?.id) { - // Reset the last ViewHolder before assigning the new, correct one to be highlighted - lastHolder?.setHighlighted(false) - lastHolder = (holder as Highlightable) - holder.setHighlighted(true) - } else { - (holder as Highlightable).setHighlighted(false) + if (holder is Highlightable) { + when (item.id) { + currentAlbum?.id -> { + currentAlbumHolder?.setHighlighted(false) + currentAlbumHolder = holder + holder.setHighlighted(true) + } + + currentSong?.id -> { + currentSongHolder?.setHighlighted(false) + currentSongHolder = holder + holder.setHighlighted(true) + } + + else -> holder.setHighlighted(false) } } } @@ -79,11 +103,11 @@ class ArtistDetailAdapter( * Update the current [album] that this adapter should highlight * @param recycler The recyclerview the highlighting should act on. */ - fun setCurrentAlbum(album: Album?, recycler: RecyclerView) { + fun highlightAlbum(album: Album?, recycler: RecyclerView) { // Clear out the last ViewHolder as a song update usually signifies that this current // ViewHolder is likely invalid. - lastHolder?.setHighlighted(false) - lastHolder = null + currentAlbumHolder?.setHighlighted(false) + currentAlbumHolder = null currentAlbum = album @@ -93,12 +117,46 @@ class ArtistDetailAdapter( item.name == album.name && item is Album } + logD(pos) + // Check if the ViewHolder if this album is visible, and highlight it if so. recycler.layoutManager?.findViewByPosition(pos)?.let { child -> recycler.getChildViewHolder(child)?.let { - lastHolder = it as Highlightable + currentAlbumHolder = it as Highlightable - lastHolder?.setHighlighted(true) + currentAlbumHolder?.setHighlighted(true) + } + } + } + } + + /** + * Update the [song] that this adapter should highlight + * @param recycler The recyclerview the highlighting should act on. + */ + fun highlightSong(song: Song?, recycler: RecyclerView) { + // Clear out the last ViewHolder as a song update usually signifies that this current + // ViewHolder is likely invalid. + currentSongHolder?.setHighlighted(false) + currentSongHolder = null + + currentSong = song + + if (song != null) { + // Use existing data instead of having to re-sort it. + // We also have to account for the album count when searching for the viewholder + val pos = currentList.indexOfFirst { item -> + item.name == song.name && item is Song + } + + // Check if the ViewHolder for this song is visible, if it is then highlight it. + // If the ViewHolder is not visible, then the adapter should take care of it if + // it does become visible. + recycler.layoutManager?.findViewByPosition(pos)?.let { child -> + recycler.getChildViewHolder(child)?.let { + currentSongHolder = it as Highlightable + + currentSongHolder?.setHighlighted(true) } } } @@ -110,13 +168,7 @@ class ArtistDetailAdapter( override fun onBind(data: Artist) { binding.artist = data - binding.detailModel = detailModel binding.playbackModel = playbackModel - binding.lifecycleOwner = lifecycleOwner - - if (data.albums.size < 2) { - binding.artistSortButton.disable() - } } } @@ -133,6 +185,8 @@ class ArtistDetailAdapter( } override fun setHighlighted(isHighlighted: Boolean) { + logD(isHighlighted) + if (isHighlighted) { binding.albumName.setTextColorResource(Accent.get().color) } else { @@ -141,8 +195,29 @@ class ArtistDetailAdapter( } } + inner class ArtistSongViewHolder( + private val binding: ItemArtistSongBinding, + ) : BaseViewHolder(binding, doOnClick, doOnLongClick), Highlightable { + private val normalTextColor = binding.songName.currentTextColor + + override fun onBind(data: Song) { + binding.song = data + + binding.songName.requestLayout() + } + + override fun setHighlighted(isHighlighted: Boolean) { + if (isHighlighted) { + binding.songName.setTextColorResource(Accent.get().color) + } else { + binding.songName.setTextColor(normalTextColor) + } + } + } + companion object { const val ARTIST_HEADER_ITEM_TYPE = 0xA009 const val ARTIST_ALBUM_ITEM_TYPE = 0xA00A + const val ARTIST_SONG_ITEM_TYPE = 0xA00B } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt index 4a605e614..690228237 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/adapters/GenreDetailAdapter.kt @@ -143,7 +143,7 @@ class GenreDetailAdapter( } companion object { - const val GENRE_HEADER_ITEM_TYPE = 0xA00B - const val GENRE_SONG_ITEM_TYPE = 0xA00C + const val GENRE_HEADER_ITEM_TYPE = 0xA00C + const val GENRE_SONG_ITEM_TYPE = 0xA00D } } diff --git a/app/src/main/java/org/oxycblt/auxio/music/Models.kt b/app/src/main/java/org/oxycblt/auxio/music/Models.kt index 69fbfd84e..53710f5df 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/Models.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/Models.kt @@ -199,12 +199,12 @@ data class Header( /** * A data object for a header with an action button. Inherits [BaseModel]. - * @property icon The icon ot apply for this header + * @property icon The icon ot apply for this header. This can be changed to reflect any change. * @property action The callback that will be called when the action button is clicked. */ data class ActionHeader( override val id: Long = -1, override val name: String = "", - @DrawableRes val icon: Int, - val action: (button: ImageButton) -> Unit, + @DrawableRes var icon: Int, + val action: ActionHeader.(button: ImageButton) -> Unit, ) : BaseModel() 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 387a6fa09..ec52ccbcb 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 @@ -6,7 +6,6 @@ import android.view.ViewGroup import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.RecyclerView -import org.oxycblt.auxio.databinding.ItemActionHeaderBinding import org.oxycblt.auxio.databinding.ItemQueueSongBinding import org.oxycblt.auxio.logE import org.oxycblt.auxio.music.ActionHeader @@ -47,9 +46,7 @@ class QueueAdapter( return when (viewType) { HeaderViewHolder.ITEM_TYPE -> HeaderViewHolder.from(parent.context) - ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder( - ItemActionHeaderBinding.inflate(parent.context.inflater) - ) + ActionHeaderViewHolder.ITEM_TYPE -> ActionHeaderViewHolder.from(parent.context) QUEUE_SONG_ITEM_TYPE -> QueueSongViewHolder( ItemQueueSongBinding.inflate(parent.context.inflater) diff --git a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt index 8a8948151..5eafeaa27 100644 --- a/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/recycler/viewholders/ModelHolders.kt @@ -186,7 +186,7 @@ class ActionHeaderViewHolder( setImageResource(data.icon) setOnClickListener { - data.action(binding.headerButton) + data.action(data, binding.headerButton) } } } @@ -195,11 +195,11 @@ class ActionHeaderViewHolder( const val ITEM_TYPE = 0xA006 /** - * Create an instance of [HeaderViewHolder] + * Create an instance of [ActionHeaderViewHolder] */ - fun from(context: Context): HeaderViewHolder { - return HeaderViewHolder( - ItemHeaderBinding.inflate(context.inflater) + fun from(context: Context): ActionHeaderViewHolder { + return ActionHeaderViewHolder( + ItemActionHeaderBinding.inflate(context.inflater) ) } } diff --git a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt b/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt index 31613b5dc..e43886918 100644 --- a/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt +++ b/app/src/main/java/org/oxycblt/auxio/ui/ActionMenu.kt @@ -15,7 +15,6 @@ import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.playback.PlaybackViewModel -import org.oxycblt.auxio.playback.state.PlaybackMode /** * Extension method for creating and showing a new [ActionMenu]. @@ -78,6 +77,7 @@ class ActionMenu( when (flag) { FLAG_NONE, FLAG_IN_GENRE -> R.menu.menu_song_actions FLAG_IN_ALBUM -> R.menu.menu_album_song_actions + FLAG_IN_ARTIST -> R.menu.menu_artist_song_actions else -> -1 } @@ -125,12 +125,6 @@ class ActionMenu( } } - R.id.action_play_artist -> { - if (flag == FLAG_IN_ALBUM && data is Song) { - playbackModel.playSong(data, PlaybackMode.IN_ARTIST) - } - } - R.id.action_queue_add -> { when (data) { is Song -> { diff --git a/app/src/main/res/layout-land/item_artist_header.xml b/app/src/main/res/layout-land/item_artist_header.xml index a288b2d45..3737497ce 100644 --- a/app/src/main/res/layout-land/item_artist_header.xml +++ b/app/src/main/res/layout-land/item_artist_header.xml @@ -107,17 +107,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/artist_play_button" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout-large/item_artist_header.xml b/app/src/main/res/layout-large/item_artist_header.xml index 8a4a7a06c..58036bfd5 100644 --- a/app/src/main/res/layout-large/item_artist_header.xml +++ b/app/src/main/res/layout-large/item_artist_header.xml @@ -107,17 +107,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/artist_play_button" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_artist_header.xml b/app/src/main/res/layout/item_artist_header.xml index 9e4f3d986..7352ceec5 100644 --- a/app/src/main/res/layout/item_artist_header.xml +++ b/app/src/main/res/layout/item_artist_header.xml @@ -104,17 +104,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/artist_play_button" /> - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_artist_song.xml b/app/src/main/res/layout/item_artist_song.xml new file mode 100644 index 000000000..e0bb1712a --- /dev/null +++ b/app/src/main/res/layout/item_artist_song.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_album_song_actions.xml b/app/src/main/res/menu/menu_album_song_actions.xml index 38083c11f..8fddaf58e 100644 --- a/app/src/main/res/menu/menu_album_song_actions.xml +++ b/app/src/main/res/menu/menu_album_song_actions.xml @@ -6,7 +6,4 @@ - \ No newline at end of file diff --git a/app/src/main/res/menu/menu_artist_song_actions.xml b/app/src/main/res/menu/menu_artist_song_actions.xml new file mode 100644 index 000000000..1890e56e7 --- /dev/null +++ b/app/src/main/res/menu/menu_artist_song_actions.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/info/ARCHITECTURE.md b/info/ARCHITECTURE.md index 15da9a5ff..1c8e996b8 100644 --- a/info/ARCHITECTURE.md +++ b/info/ARCHITECTURE.md @@ -50,8 +50,9 @@ To prevent any strange bugs, all integer representations must be unique. A table 0xA008 | AlbumSongViewHolder 0xA009 | ArtistHeaderViewHolder 0xA00A | ArtistAlbumViewHolder -0xA00B | GenreHeaderViewHolder -0xA00C | GenreSongViewHolder +0xA00B | ArtistSongViewHolder +0xA00C | GenreHeaderViewHolder +0xA00D | GenreSongViewHolder 0xA0A0 | Auxio notification code 0xA0C0 | Auxio request code