diff --git a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt index e1a1b440b..e62d4222d 100644 --- a/app/src/main/java/org/oxycblt/auxio/MainActivity.kt +++ b/app/src/main/java/org/oxycblt/auxio/MainActivity.kt @@ -48,6 +48,10 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat * * TODO: Migrate to material animation system * + * TODO: Re-document project + * + * TODO: Unit testing + * * @author OxygenCobalt */ class MainActivity : AppCompatActivity() { 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 c4e644c61..fe0b3613d 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/AlbumDetailFragment.kt @@ -32,7 +32,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.list.MenuFragment +import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music @@ -53,7 +53,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * A fragment that shows information for a particular [Album]. * @author OxygenCobalt */ -class AlbumDetailFragment : MenuFragment() { +class AlbumDetailFragment : ListFragment() { private val detailModel: DetailViewModel by activityViewModels() private val args: AlbumDetailFragmentArgs by navArgs() @@ -64,7 +64,7 @@ class AlbumDetailFragment : MenuFragment() { AlbumDetailAdapter.Callback( ::handleClick, ::handleOpenItemMenu, - {}, + ::handleSelect, ::handlePlay, ::handleShuffle, ::handleOpenSortMenu, @@ -81,7 +81,7 @@ class AlbumDetailFragment : MenuFragment() { override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { - detailModel.setAlbumUid(args.albumUid) + setupSelectionToolbar(binding.detailSelectionToolbar) binding.detailToolbar.apply { inflateMenu(R.menu.menu_album_detail) @@ -96,11 +96,14 @@ class AlbumDetailFragment : MenuFragment() { // -- VIEWMODEL SETUP --- + detailModel.setAlbumUid(args.albumUid) + collectImmediately(detailModel.currentAlbum, ::updateItem) collectImmediately(detailModel.albumData, detailAdapter::submitList) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) collect(navModel.exploreNavigationItem, ::handleNavigation) + collectImmediately(selectionModel.selected, ::handleSelection) } override fun onDestroyBinding(binding: FragmentDetailBinding) { @@ -108,13 +111,13 @@ class AlbumDetailFragment : MenuFragment() { binding.detailRecycler.adapter = null } - private fun handleClick(item: Item) { - check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" } + override fun onRealClick(music: Music) { + check(music is Song) { "Unexpected datatype: ${music::class.java}"} when (settings.detailPlaybackMode) { null, - MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) - MusicMode.SONGS -> playbackModel.playFromAll(item) - MusicMode.ARTISTS -> playbackModel.playFromArtist(item) + MusicMode.ALBUMS -> playbackModel.playFromAlbum(music) + MusicMode.SONGS -> playbackModel.playFromAll(music) + MusicMode.ARTISTS -> playbackModel.playFromArtist(music) else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}") } } @@ -179,6 +182,26 @@ class AlbumDetailFragment : MenuFragment() { requireBinding().detailToolbar.title = album.resolveName(requireContext()) } + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { + val binding = requireBinding() + + for (item in binding.detailToolbar.menu.children) { + // If there is no playback going in, any queue additions will be wiped as soon as + // something is played. Disable these actions when playback is going on so that + // it isn't possible to add anything during that time. + if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) { + item.isEnabled = song != null + } + } + + if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) { + detailAdapter.setPlayingItem(song, isPlaying) + } else { + // Clear the ViewHolders if the mode isn't ALL_SONGS + detailAdapter.setPlayingItem(null, isPlaying) + } + } + private fun handleNavigation(item: Music?) { val binding = requireBinding() when (item) { @@ -221,7 +244,7 @@ class AlbumDetailFragment : MenuFragment() { } } - /** Scroll to an [song]. */ + /** Scroll to a [song]. */ private fun scrollToItem(song: Song) { // Calculate where the item for the currently played song is val pos = detailModel.albumData.value.indexOf(song) @@ -241,24 +264,9 @@ class AlbumDetailFragment : MenuFragment() { } } - private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { - val binding = requireBinding() - - for (item in binding.detailToolbar.menu.children) { - // If there is no playback going in, any queue additions will be wiped as soon as - // something is played. Disable these actions when playback is going on so that - // it isn't possible to add anything during that time. - if (item.itemId == R.id.action_play_next || item.itemId == R.id.action_queue_add) { - item.isEnabled = song != null - } - } - - if (parent is Album && parent == unlikelyToBeNull(detailModel.currentAlbum.value)) { - detailAdapter.updateIndicator(song, isPlaying) - } else { - // Clear the ViewHolders if the mode isn't ALL_SONGS - detailAdapter.updateIndicator(null, isPlaying) - } + private fun handleSelection(selected: List) { + detailAdapter.setSelectedItems(selected) + requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) } /** @@ -279,4 +287,5 @@ class AlbumDetailFragment : MenuFragment() { snapPreference: Int ): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2) } + } 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 4207a6d2f..a162dc547 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/ArtistDetailFragment.kt @@ -30,7 +30,7 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.list.MenuFragment +import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Music @@ -50,7 +50,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * A fragment that shows information for a particular [Artist]. * @author OxygenCobalt */ -class ArtistDetailFragment : MenuFragment() { +class ArtistDetailFragment : ListFragment() { private val detailModel: DetailViewModel by activityViewModels() private val args: ArtistDetailFragmentArgs by navArgs() @@ -61,7 +61,7 @@ class ArtistDetailFragment : MenuFragment() { DetailAdapter.Callback( ::handleClick, ::handleOpenItemMenu, - {}, + ::handleSelect, ::handlePlay, ::handleShuffle, ::handleOpenSortMenu)) @@ -77,7 +77,7 @@ class ArtistDetailFragment : MenuFragment() { override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { - detailModel.setArtistUid(args.artistUid) + setupSelectionToolbar(binding.detailSelectionToolbar) binding.detailToolbar.apply { inflateMenu(R.menu.menu_genre_artist_detail) @@ -92,11 +92,14 @@ class ArtistDetailFragment : MenuFragment() { // --- VIEWMODEL SETUP --- + detailModel.setArtistUid(args.artistUid) + collectImmediately(detailModel.currentArtist, ::updateItem) collectImmediately(detailModel.artistData, detailAdapter::submitList) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) collect(navModel.exploreNavigationItem, ::handleNavigation) + collectImmediately(selectionModel.selected, ::handleSelection) } override fun onDestroyBinding(binding: FragmentDetailBinding) { @@ -104,21 +107,21 @@ class ArtistDetailFragment : MenuFragment() { binding.detailRecycler.adapter = null } - private fun handleClick(item: Item) { - when (item) { + override fun onRealClick(music: Music) { + when (music) { is Song -> { when (settings.detailPlaybackMode) { null -> playbackModel.playFromArtist( - item, unlikelyToBeNull(detailModel.currentArtist.value)) - MusicMode.SONGS -> playbackModel.playFromAll(item) - MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) - MusicMode.ARTISTS -> playbackModel.playFromArtist(item) + music, unlikelyToBeNull(detailModel.currentArtist.value)) + MusicMode.SONGS -> playbackModel.playFromAll(music) + MusicMode.ALBUMS -> playbackModel.playFromAlbum(music) + MusicMode.ARTISTS -> playbackModel.playFromArtist(music) else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}") } } - is Album -> navModel.exploreNavigateTo(item) - else -> error("Unexpected datatype: ${item::class.simpleName}") + is Album -> navModel.exploreNavigateTo(music) + else -> error("Unexpected datatype: ${music::class.simpleName}") } } @@ -180,6 +183,20 @@ class ArtistDetailFragment : MenuFragment() { requireBinding().detailToolbar.title = artist.resolveName(requireContext()) } + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { + var item: Item? = null + + if (parent is Album) { + item = parent + } + + if (parent is Artist && parent == unlikelyToBeNull(detailModel.currentArtist.value)) { + item = song + } + + detailAdapter.setPlayingItem(item, isPlaying) + } + private fun handleNavigation(item: Music?) { val binding = requireBinding() @@ -210,17 +227,8 @@ class ArtistDetailFragment : MenuFragment() { } } - private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { - var item: Item? = null - - if (parent is Album) { - item = parent - } - - if (parent is Artist && parent == unlikelyToBeNull(detailModel.currentArtist.value)) { - item = song - } - - detailAdapter.updateIndicator(item, isPlaying) + private fun handleSelection(selected: List) { + detailAdapter.setSelectedItems(selected) + requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) } } 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 5273e1c9d..321a78092 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/GenreDetailFragment.kt @@ -30,7 +30,7 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.list.MenuFragment +import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre @@ -51,7 +51,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull * A fragment that shows information for a particular [Genre]. * @author OxygenCobalt */ -class GenreDetailFragment : MenuFragment() { +class GenreDetailFragment : ListFragment() { private val detailModel: DetailViewModel by activityViewModels() private val args: GenreDetailFragmentArgs by navArgs() @@ -62,10 +62,11 @@ class GenreDetailFragment : MenuFragment() { DetailAdapter.Callback( ::handleClick, ::handleOpenItemMenu, - {}, + ::handleSelect, ::handlePlay, ::handleShuffle, ::handleOpenSortMenu)) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) @@ -77,7 +78,7 @@ class GenreDetailFragment : MenuFragment() { override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater) override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) { - detailModel.setGenreUid(args.genreUid) + setupSelectionToolbar(binding.detailSelectionToolbar) binding.detailToolbar.apply { inflateMenu(R.menu.menu_genre_artist_detail) @@ -92,11 +93,14 @@ class GenreDetailFragment : MenuFragment() { // --- VIEWMODEL SETUP --- + detailModel.setGenreUid(args.genreUid) + collectImmediately(detailModel.currentGenre, ::handleItemChange) collectImmediately(detailModel.genreData, detailAdapter::submitList) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) collect(navModel.exploreNavigationItem, ::handleNavigation) + collectImmediately(selectionModel.selected, ::handleSelection) } override fun onDestroyBinding(binding: FragmentDetailBinding) { @@ -117,20 +121,20 @@ class GenreDetailFragment : MenuFragment() { } } - private fun handleClick(item: Item) { - when (item) { - is Artist -> navModel.exploreNavigateTo(item) + override fun onRealClick(music: Music) { + when (music) { + is Artist -> navModel.exploreNavigateTo(music) is Song -> when (settings.detailPlaybackMode) { null -> playbackModel.playFromGenre( - item, unlikelyToBeNull(detailModel.currentGenre.value)) - MusicMode.SONGS -> playbackModel.playFromAll(item) - MusicMode.ALBUMS -> playbackModel.playFromAlbum(item) - MusicMode.ARTISTS -> playbackModel.playFromArtist(item) + music, unlikelyToBeNull(detailModel.currentGenre.value)) + MusicMode.SONGS -> playbackModel.playFromAll(music) + MusicMode.ALBUMS -> playbackModel.playFromAlbum(music) + MusicMode.ARTISTS -> playbackModel.playFromArtist(music) else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}") } - else -> error("Unexpected datatype: ${item::class.simpleName}") + else -> error("Unexpected datatype: ${music::class.simpleName}") } } @@ -177,6 +181,21 @@ class GenreDetailFragment : MenuFragment() { requireBinding().detailToolbar.title = genre.resolveName(requireContext()) } + private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { + var item: Item? = null + + if (parent is Artist) { + item = parent + } + + if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) { + item = song + } + + detailAdapter.setPlayingItem(item, isPlaying) + } + + private fun handleNavigation(item: Music?) { when (item) { is Song -> { @@ -201,17 +220,8 @@ class GenreDetailFragment : MenuFragment() { } } - private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { - var item: Item? = null - - if (parent is Artist) { - item = parent - } - - if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) { - item = song - } - - detailAdapter.updateIndicator(item, isPlaying) + private fun handleSelection(selected: List) { + detailAdapter.setSelectedItems(selected) + requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size) } } 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 017c741d3..c5674a45d 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 @@ -28,7 +28,7 @@ import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding import org.oxycblt.auxio.detail.DiscHeader import org.oxycblt.auxio.list.Item -import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter +import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SimpleItemCallback import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Song @@ -41,7 +41,7 @@ import org.oxycblt.auxio.util.inflater * An adapter for displaying [Album] information and it's children. * @author OxygenCobalt */ -class AlbumDetailAdapter(private val callback: AlbumDetailAdapter.Callback) : +class AlbumDetailAdapter(private val callback: Callback) : DetailAdapter(callback, DIFFER) { override fun getItemViewType(position: Int) = @@ -180,7 +180,7 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) : } private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) : - PlayingIndicatorAdapter.ViewHolder(binding.root) { + SelectionIndicatorAdapter.ViewHolder(binding.root) { fun bind(item: Song, callback: AlbumDetailAdapter.Callback) { // Hide the track number view if the song does not have a track. if (item.track != null) { @@ -215,6 +215,10 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA binding.songTrackBg.isPlaying = isPlaying } + override fun updateSelectionIndicator(isSelected: Boolean) { + binding.root.isActivated = isSelected + } + 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 2ee1c891c..9f647764d 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 @@ -28,7 +28,9 @@ import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ItemMenuCallback +import org.oxycblt.auxio.list.ItemSelectCallback import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter +import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SimpleItemCallback import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist @@ -153,14 +155,21 @@ private class ArtistDetailViewHolder private constructor(private val binding: It } private class ArtistAlbumViewHolder private constructor(private val binding: ItemParentBinding) : - PlayingIndicatorAdapter.ViewHolder(binding.root) { - fun bind(item: Album, callback: ItemMenuCallback) { + SelectionIndicatorAdapter.ViewHolder(binding.root) { + fun bind(item: Album, callback: ItemSelectCallback) { binding.parentImage.bind(item) binding.parentName.text = item.resolveName(binding.context) binding.parentInfo.text = item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date) binding.parentMenu.setOnClickListener { callback.onOpenMenu(item, it) } binding.root.setOnClickListener { callback.onClick(item) } + binding.root.apply { + setOnClickListener { callback.onClick(item) } + setOnLongClickListener { + callback.onSelect(item) + true + } + } } override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) { @@ -168,6 +177,10 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite binding.parentImage.isPlaying = isPlaying } + override fun updateSelectionIndicator(isSelected: Boolean) { + binding.root.isActivated = isSelected + } + companion object { const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_ALBUM @@ -183,13 +196,20 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite } private class ArtistSongViewHolder private constructor(private val binding: ItemSongBinding) : - PlayingIndicatorAdapter.ViewHolder(binding.root) { - fun bind(item: Song, callback: ItemMenuCallback) { + SelectionIndicatorAdapter.ViewHolder(binding.root) { + fun bind(item: Song, callback: ItemSelectCallback) { binding.songAlbumCover.bind(item) binding.songName.text = item.resolveName(binding.context) binding.songInfo.text = item.album.resolveName(binding.context) binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) } binding.root.setOnClickListener { callback.onClick(item) } + binding.root.apply { + setOnClickListener { callback.onClick(item) } + setOnLongClickListener { + callback.onSelect(item) + true + } + } } override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) { @@ -197,6 +217,10 @@ private class ArtistSongViewHolder private constructor(private val binding: Item binding.songAlbumCover.isPlaying = isPlaying } + override fun updateSelectionIndicator(isSelected: Boolean) { + binding.root.isActivated = isSelected + } + 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 a455742ce..4d50bfd52 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 @@ -29,17 +29,14 @@ import org.oxycblt.auxio.detail.SortHeader import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ItemSelectCallback -import org.oxycblt.auxio.list.recycler.AuxioRecyclerView -import org.oxycblt.auxio.list.recycler.HeaderViewHolder -import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter -import org.oxycblt.auxio.list.recycler.SimpleItemCallback +import org.oxycblt.auxio.list.recycler.* import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater abstract class DetailAdapter( private val callback: Callback, diffCallback: DiffUtil.ItemCallback -) : PlayingIndicatorAdapter(), AuxioRecyclerView.SpanSizeLookup { +) : SelectionIndicatorAdapter(), AuxioRecyclerView.SpanSizeLookup { @Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size override fun getItemViewType(position: Int) = diff --git a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt index a7d38ee8b..50b5895c1 100644 --- a/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt @@ -45,7 +45,8 @@ import org.oxycblt.auxio.home.list.AlbumListFragment import org.oxycblt.auxio.home.list.ArtistListFragment import org.oxycblt.auxio.home.list.GenreListFragment import org.oxycblt.auxio.home.list.SongListFragment -import org.oxycblt.auxio.list.SelectionFragment +import org.oxycblt.auxio.list.ListFragment +import org.oxycblt.auxio.list.selection.SelectionToolbarOverlay import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre @@ -63,7 +64,7 @@ import org.oxycblt.auxio.util.* * respective item. * @author OxygenCobalt */ -class HomeFragment : SelectionFragment() { +class HomeFragment : ListFragment() { private val homeModel: HomeViewModel by androidActivityViewModels() private val musicModel: MusicViewModel by activityViewModels() @@ -96,7 +97,7 @@ class HomeFragment : SelectionFragment() { override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) { binding.homeAppbar.addOnOffsetChangedListener { _, it -> handleAppBarAnimation(it) } - setupOverlay(binding.homeToolbarOverlay) + setupSelectionToolbar(binding.homeSelectionToolbar) binding.homeToolbar.setOnMenuItemClickListener { handleHomeMenuItem(it) true @@ -171,7 +172,7 @@ class HomeFragment : SelectionFragment() { val binding = requireBinding() val range = binding.homeAppbar.totalScrollRange - binding.homeToolbarOverlay.alpha = + binding.homeSelectionToolbar.alpha = 1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2)) binding.homeContent.updatePadding( @@ -182,8 +183,6 @@ class HomeFragment : SelectionFragment() { when (item.itemId) { R.id.action_search -> { logD("Navigating to search") - // Reset selection (navigating to another selectable screen) - selectionModel.consume() initAxisTransitions(MaterialSharedAxis.Z) findNavController().navigate(HomeFragmentDirections.actionShowSearch()) } @@ -382,15 +381,13 @@ class HomeFragment : SelectionFragment() { else -> return } - // Reset selection (navigating to another selectable screen) - selectionModel.consume() initAxisTransitions(MaterialSharedAxis.X) findNavController().navigate(action) } private fun updateSelection(selected: List) { val binding = requireBinding() - if (binding.homeToolbarOverlay.updateSelectionAmount(selected.size) && + if (binding.homeSelectionToolbar.updateSelectionAmount(selected.size) && selected.isNotEmpty()) { logD("Significant selection occurred, expanding AppBar") // Significant enough change where we want to expand the RecyclerView @@ -409,7 +406,7 @@ class HomeFragment : SelectionFragment() { } private fun setupTabs(binding: FragmentHomeBinding) { - val toolbarParams = binding.homeToolbarOverlay.layoutParams as AppBarLayout.LayoutParams + val toolbarParams = binding.homeSelectionToolbar.layoutParams as AppBarLayout.LayoutParams if (homeModel.tabs.size == 1) { // A single tab makes the tab layout redundant, hide it and disable the collapsing // behavior. 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 e038d844a..f4e19b192 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 @@ -28,7 +28,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.list.* -import org.oxycblt.auxio.list.SelectionFragment +import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.recycler.AlbumViewHolder import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SyncListDiffer @@ -45,7 +45,7 @@ import org.oxycblt.auxio.util.collectImmediately * A [HomeListFragment] for showing a list of [Album]s. * @author OxygenCobalt */ -class AlbumListFragment : SelectionFragment() { +class AlbumListFragment : ListFragment() { private val homeModel: HomeViewModel by activityViewModels() private val homeAdapter = @@ -69,7 +69,7 @@ class AlbumListFragment : SelectionFragment() { } collectImmediately(homeModel.albums, homeAdapter::replaceList) - collectImmediately(selectionModel.selected, homeAdapter::setSelected) + collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems) collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) } @@ -78,7 +78,7 @@ class AlbumListFragment : SelectionFragment() { binding.homeRecycler.adapter = null } - override fun onClick(music: Music) { + override fun onRealClick(music: Music) { check(music is Album) { "Unexpected datatype: ${music::class.java}" } navModel.exploreNavigateTo(music) } @@ -128,10 +128,10 @@ class AlbumListFragment : SelectionFragment() { } private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) { if (parent is Album) { - homeAdapter.updateIndicator(parent, isPlaying) + homeAdapter.setPlayingItem(parent, isPlaying) } else { // Ignore playback not from albums - homeAdapter.updateIndicator(null, isPlaying) + homeAdapter.setPlayingItem(null, isPlaying) } } 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 931b171f3..1407c21cc 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 @@ -26,7 +26,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.list.* -import org.oxycblt.auxio.list.SelectionFragment +import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.recycler.ArtistViewHolder import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SyncListDiffer @@ -43,7 +43,7 @@ import org.oxycblt.auxio.util.nonZeroOrNull * A [HomeListFragment] for showing a list of [Artist]s. * @author OxygenCobalt */ -class ArtistListFragment : SelectionFragment() { +class ArtistListFragment : ListFragment() { private val homeModel: HomeViewModel by activityViewModels() private val homeAdapter = @@ -64,7 +64,7 @@ class ArtistListFragment : SelectionFragment() { } collectImmediately(homeModel.artists, homeAdapter::replaceList) - collectImmediately(selectionModel.selected, homeAdapter::setSelected) + collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems) collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) } @@ -92,7 +92,7 @@ class ArtistListFragment : SelectionFragment() { } } - override fun onClick(music: Music) { + override fun onRealClick(music: Music) { check(music is Artist) { "Unexpected datatype: ${music::class.java}" } navModel.exploreNavigateTo(music) } @@ -104,10 +104,10 @@ class ArtistListFragment : SelectionFragment() { private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) { if (parent is Artist) { - homeAdapter.updateIndicator(parent, isPlaying) + homeAdapter.setPlayingItem(parent, isPlaying) } else { // Ignore playback not from artists - homeAdapter.updateIndicator(null, isPlaying) + homeAdapter.setPlayingItem(null, isPlaying) } } 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 7960927e8..9315d2ef3 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 @@ -26,7 +26,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.list.* -import org.oxycblt.auxio.list.SelectionFragment +import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.recycler.GenreViewHolder import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SyncListDiffer @@ -42,7 +42,7 @@ import org.oxycblt.auxio.util.collectImmediately * A [HomeListFragment] for showing a list of [Genre]s. * @author OxygenCobalt */ -class GenreListFragment : SelectionFragment() { +class GenreListFragment : ListFragment() { private val homeModel: HomeViewModel by activityViewModels() private val homeAdapter = @@ -63,7 +63,7 @@ class GenreListFragment : SelectionFragment() { } collectImmediately(homeModel.genres, homeAdapter::replaceList) - collectImmediately(selectionModel.selected, homeAdapter::setSelected) + collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems) collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) } @@ -91,7 +91,7 @@ class GenreListFragment : SelectionFragment() { } } - override fun onClick(music: Music) { + override fun onRealClick(music: Music) { check(music is Genre) { "Unexpected datatype: ${music::class.java}" } navModel.exploreNavigateTo(music) } @@ -103,10 +103,10 @@ class GenreListFragment : SelectionFragment() { private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) { if (parent is Genre) { - homeAdapter.updateIndicator(parent, isPlaying) + homeAdapter.setPlayingItem(parent, isPlaying) } else { // Ignore playback not from genres - homeAdapter.updateIndicator(null, isPlaying) + homeAdapter.setPlayingItem(null, isPlaying) } } 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 ffc68e4e4..baca01114 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 @@ -28,7 +28,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.home.HomeViewModel import org.oxycblt.auxio.list.* -import org.oxycblt.auxio.list.SelectionFragment +import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter import org.oxycblt.auxio.list.recycler.SongViewHolder import org.oxycblt.auxio.list.recycler.SyncListDiffer @@ -47,7 +47,7 @@ import org.oxycblt.auxio.util.context * A [HomeListFragment] for showing a list of [Song]s. * @author OxygenCobalt */ -class SongListFragment : SelectionFragment() { +class SongListFragment : ListFragment() { private val homeModel: HomeViewModel by activityViewModels() private val homeAdapter = @@ -73,7 +73,7 @@ class SongListFragment : SelectionFragment() { } collectImmediately(homeModel.songs, homeAdapter::replaceList) - collectImmediately(selectionModel.selected, homeAdapter::setSelected) + collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems) collectImmediately( playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) } @@ -125,7 +125,7 @@ class SongListFragment : SelectionFragment() { } } - override fun onClick(music: Music) { + override fun onRealClick(music: Music) { check(music is Song) { "Unexpected datatype: ${music::class.java}" } when (settings.libPlaybackMode) { MusicMode.SONGS -> playbackModel.playFromAll(music) @@ -142,10 +142,10 @@ class SongListFragment : SelectionFragment() { private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { if (parent == null) { - homeAdapter.updateIndicator(song, isPlaying) + homeAdapter.setPlayingItem(song, isPlaying) } else { // Ignore playback that is not from all songs - homeAdapter.updateIndicator(null, isPlaying) + homeAdapter.setPlayingItem(null, isPlaying) } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/List.kt b/app/src/main/java/org/oxycblt/auxio/list/List.kt index 520823fd0..3bf842309 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/List.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/List.kt @@ -39,18 +39,3 @@ open class ItemSelectCallback( onOpenMenu: (Item, View) -> Unit, val onSelect: (Item) -> Unit ) : ItemMenuCallback(onClick, onOpenMenu) - -/** An interface for detecting if an item has been clicked once. */ -interface ItemClickListener { - /** Called when an item is clicked once. */ - fun onItemClick(item: Item) -} - -/** An interface for detecting if an item has had it's menu opened. */ -interface MenuItemListener : ItemClickListener { - /** Called when an item is long-clicked. */ - fun onSelect(item: Item) {} - - /** Called when an item desires to open a menu relating to it. */ - fun onOpenMenu(item: Item, anchor: View) -} diff --git a/app/src/main/java/org/oxycblt/auxio/list/MenuFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt similarity index 77% rename from app/src/main/java/org/oxycblt/auxio/list/MenuFragment.kt rename to app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt index 7706262a4..93202e50a 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/MenuFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/ListFragment.kt @@ -17,6 +17,7 @@ package org.oxycblt.auxio.list +import android.os.Bundle import android.view.MenuItem import android.view.View import androidx.annotation.MenuRes @@ -25,10 +26,9 @@ import androidx.fragment.app.activityViewModels import androidx.viewbinding.ViewBinding import org.oxycblt.auxio.MainFragmentDirections import org.oxycblt.auxio.R -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.list.selection.SelectionToolbarOverlay +import org.oxycblt.auxio.list.selection.SelectionViewModel +import org.oxycblt.auxio.music.* import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.shared.MainNavigationAction import org.oxycblt.auxio.shared.NavigationViewModel @@ -37,17 +37,67 @@ import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.showToast -/** - * A fragment capable of creating menus. Automatically keeps track of and disposes of menus, - * preventing UI issues and memory leaks. - * @author OxygenCobalt - */ -abstract class MenuFragment : ViewBindingFragment() { +abstract class ListFragment : ViewBindingFragment() { + protected val selectionModel: SelectionViewModel by activityViewModels() private var currentMenu: PopupMenu? = null protected val playbackModel: PlaybackViewModel by androidActivityViewModels() protected val navModel: NavigationViewModel by activityViewModels() + override fun onDestroyBinding(binding: VB) { + super.onDestroyBinding(binding) + currentMenu?.dismiss() + currentMenu = null + } + + fun setupSelectionToolbar(toolbar: SelectionToolbarOverlay) { + toolbar.apply { + setOnSelectionCancelListener { selectionModel.consume() } + setOnMenuItemClickListener { + handleSelectionMenuItem(it) + true + } + } + } + + /** Handle a media item with a selection. */ + private fun handleSelectionMenuItem(item: MenuItem) { + when (item.itemId) { + R.id.action_play_next -> { + playbackModel.playNext(selectionModel.consume()) + requireContext().showToast(R.string.lng_queue_added) + } + R.id.action_queue_add -> { + playbackModel.addToQueue(selectionModel.consume()) + requireContext().showToast(R.string.lng_queue_added) + } + } + } + + /** + * Called when an item is clicked by the user and was not selected by [handleClick]. This can be + * optionally implemented if [handleClick] is used. + */ + open fun onRealClick(music: Music) { + throw NotImplementedError() + } + + /** Provided implementation of an item click callback that handles selection. */ + protected fun handleClick(item: Item) { + check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" } + if (selectionModel.selected.value.isNotEmpty()) { + selectionModel.select(item) + } else { + onRealClick(item) + } + } + + /** Provided implementation of an item selection callback. */ + protected fun handleSelect(item: Item) { + check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" } + selectionModel.select(item) + } + /** * Opens the given menu in context of [song]. Assumes that the menu is only composed of common * [Song] options. @@ -207,10 +257,4 @@ abstract class MenuFragment : ViewBindingFragment() { show() } } - - override fun onDestroyBinding(binding: T) { - super.onDestroyBinding(binding) - currentMenu?.dismiss() - currentMenu = null - } } diff --git a/app/src/main/java/org/oxycblt/auxio/list/SelectionFragment.kt b/app/src/main/java/org/oxycblt/auxio/list/SelectionFragment.kt deleted file mode 100644 index 50cb68683..000000000 --- a/app/src/main/java/org/oxycblt/auxio/list/SelectionFragment.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.list - -import android.view.MenuItem -import androidx.fragment.app.activityViewModels -import androidx.viewbinding.ViewBinding -import org.oxycblt.auxio.R -import org.oxycblt.auxio.music.Music -import org.oxycblt.auxio.util.showToast - -abstract class SelectionFragment : MenuFragment() { - protected val selectionModel: SelectionViewModel by activityViewModels() - - open fun onClick(music: Music) { - throw NotImplementedError() - } - - protected fun setupOverlay(overlay: SelectionToolbarOverlay) { - overlay.apply { - setOnSelectionCancelListener { selectionModel.consume() } - setOnMenuItemClickListener { - handleSelectionMenuItem(it) - true - } - } - } - - private fun handleSelectionMenuItem(item: MenuItem) { - when (item.itemId) { - R.id.action_play_next -> { - playbackModel.playNext(selectionModel.consume()) - requireContext().showToast(R.string.lng_queue_added) - } - R.id.action_queue_add -> { - playbackModel.addToQueue(selectionModel.consume()) - requireContext().showToast(R.string.lng_queue_added) - } - } - } - - protected fun handleClick(item: Item) { - check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" } - if (selectionModel.selected.value.isNotEmpty()) { - selectionModel.select(item) - } else { - onClick(item) - } - } - - protected fun handleSelect(item: Item) { - check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" } - selectionModel.select(item) - } -} diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt index 98650e760..323dbb6be 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/PlayingIndicatorAdapter.kt @@ -44,7 +44,12 @@ abstract class PlayingIndicatorAdapter : RecyclerV abstract val currentList: List - fun updateIndicator(item: Item?, isPlaying: Boolean) { + /** + * Update the currently playing item in the list. + * @param item The item being played, null if nothing is being played. + * @param isPlaying Whether playback is ongoing or paused. + */ + fun setPlayingItem(item: Item?, isPlaying: Boolean) { var updatedItem = false if (currentItem != item) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt index 86d3f1aa0..37c5cdc9f 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/SelectionIndicatorAdapter.kt @@ -37,7 +37,10 @@ abstract class SelectionIndicatorAdapter : } } - fun setSelected(items: List) { + /** + * Update the list of selected [items] within the adapter. + */ + fun setSelectedItems(items: List) { val oldSelectedItems = selectedItems val newSelectedItems = items.toSet() if (newSelectedItems == oldSelectedItems) { diff --git a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt index 96cac83d2..c3baa4a9e 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/recycler/ViewHolders.kt @@ -26,7 +26,6 @@ import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.list.Header import org.oxycblt.auxio.list.ItemSelectCallback -import org.oxycblt.auxio.list.MenuItemListener import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre @@ -41,20 +40,6 @@ import org.oxycblt.auxio.util.inflater */ class SongViewHolder private constructor(private val binding: ItemSongBinding) : SelectionIndicatorAdapter.ViewHolder(binding.root) { - fun bind(item: Song, listener: MenuItemListener) { - binding.songAlbumCover.bind(item) - binding.songName.text = item.resolveName(binding.context) - binding.songInfo.text = item.resolveArtistContents(binding.context) - - binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) } - binding.root.apply { - setOnClickListener { listener.onItemClick(item) } - setOnLongClickListener { - listener.onSelect(item) - true - } - } - } fun bind(item: Song, callback: ItemSelectCallback) { binding.songAlbumCover.bind(item) @@ -100,21 +85,6 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) : class AlbumViewHolder private constructor(private val binding: ItemParentBinding) : SelectionIndicatorAdapter.ViewHolder(binding.root) { - fun bind(item: Album, listener: MenuItemListener) { - binding.parentImage.bind(item) - binding.parentName.text = item.resolveName(binding.context) - binding.parentInfo.text = item.resolveArtistContents(binding.context) - - binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) } - binding.root.apply { - setOnClickListener { listener.onItemClick(item) } - setOnLongClickListener { - listener.onSelect(item) - true - } - } - } - fun bind(item: Album, callback: ItemSelectCallback) { binding.parentImage.bind(item) binding.parentName.text = item.resolveName(binding.context) @@ -161,31 +131,6 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding class ArtistViewHolder private constructor(private val binding: ItemParentBinding) : SelectionIndicatorAdapter.ViewHolder(binding.root) { - fun bind(item: Artist, listener: MenuItemListener) { - binding.parentImage.bind(item) - binding.parentName.text = item.resolveName(binding.context) - - binding.parentInfo.text = - if (item.songs.isNotEmpty()) { - binding.context.getString( - R.string.fmt_two, - binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size), - binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)) - } else { - // Artist has no songs, only display an album count. - binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size) - } - - binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) } - binding.root.apply { - setOnClickListener { listener.onItemClick(item) } - setOnLongClickListener { - listener.onSelect(item) - true - } - } - } - fun bind(item: Artist, callback: ItemSelectCallback) { binding.parentImage.bind(item) binding.parentName.text = item.resolveName(binding.context) @@ -242,25 +187,6 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin class GenreViewHolder private constructor(private val binding: ItemParentBinding) : SelectionIndicatorAdapter.ViewHolder(binding.root) { - fun bind(item: Genre, listener: MenuItemListener) { - binding.parentImage.bind(item) - binding.parentName.text = item.resolveName(binding.context) - binding.parentInfo.text = - binding.context.getString( - R.string.fmt_two, - binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size), - binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)) - - binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) } - binding.root.apply { - setOnClickListener { listener.onItemClick(item) } - setOnLongClickListener { - listener.onSelect(item) - true - } - } - } - fun bind(item: Genre, callback: ItemSelectCallback) { binding.parentImage.bind(item) binding.parentName.text = item.resolveName(binding.context) diff --git a/app/src/main/java/org/oxycblt/auxio/list/SelectionToolbarOverlay.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt similarity index 97% rename from app/src/main/java/org/oxycblt/auxio/list/SelectionToolbarOverlay.kt rename to app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt index b3dcc33a4..58520f07c 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/SelectionToolbarOverlay.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionToolbarOverlay.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.list +package org.oxycblt.auxio.list.selection import android.animation.ValueAnimator import android.content.Context @@ -43,6 +43,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr MaterialToolbar(context).apply { setNavigationIcon(R.drawable.ic_close_24) inflateMenu(R.menu.menu_selection_actions) + + if (isInEditMode) { + isInvisible = true + } } private var fadeThroughAnimator: ValueAnimator? = null diff --git a/app/src/main/java/org/oxycblt/auxio/list/SelectionViewModel.kt b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt similarity index 92% rename from app/src/main/java/org/oxycblt/auxio/list/SelectionViewModel.kt rename to app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt index e1565e305..eb44dd84a 100644 --- a/app/src/main/java/org/oxycblt/auxio/list/SelectionViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/list/selection/SelectionViewModel.kt @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package org.oxycblt.auxio.list +package org.oxycblt.auxio.list.selection import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow @@ -46,9 +46,7 @@ class SelectionViewModel : ViewModel() { } /** Clear and return all selected items. */ - fun consume(): List { - return _selected.value.also { _selected.value = listOf() } - } + fun consume() = _selected.value.also { _selected.value = listOf() } override fun onCleared() { super.onCleared() 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 2ca0d557b..a3d145bc2 100644 --- a/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt +++ b/app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt @@ -32,7 +32,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.FragmentSearchBinding import org.oxycblt.auxio.list.Item import org.oxycblt.auxio.list.ItemSelectCallback -import org.oxycblt.auxio.list.SelectionFragment +import org.oxycblt.auxio.list.ListFragment import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Genre @@ -48,7 +48,7 @@ import org.oxycblt.auxio.util.* * better keyboard logic, recycler updating, and chips * @author OxygenCobalt */ -class SearchFragment : SelectionFragment() { +class SearchFragment : ListFragment() { // SearchViewModel is only scoped to this Fragment private val searchModel: SearchViewModel by androidViewModels() @@ -73,7 +73,7 @@ class SearchFragment : SelectionFragment() { override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater) override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) { - setupOverlay(binding.searchToolbarOverlay) + setupSelectionToolbar(binding.searchSelectionToolbar) binding.searchToolbar.apply { val itemIdToSelect = @@ -124,7 +124,7 @@ class SearchFragment : SelectionFragment() { binding.searchRecycler.adapter = null } - override fun onClick(music: Music) { + override fun onRealClick(music: Music) { when (music) { is Song -> when (settings.libPlaybackMode) { @@ -138,8 +138,6 @@ class SearchFragment : SelectionFragment() { } private fun handleSearchNavigateUp() { - // Reset selection (navigating to another selectable screen) - selectionModel.consume() // Drop keyboard as it's no longer needed imm.hide() findNavController().navigateUp() @@ -176,7 +174,7 @@ class SearchFragment : SelectionFragment() { } private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) { - searchAdapter.updateIndicator(parent ?: song, isPlaying) + searchAdapter.setPlayingItem(parent ?: song, isPlaying) } private fun handleNavigation(item: Music?) { @@ -190,17 +188,13 @@ class SearchFragment : SelectionFragment() { } findNavController().navigate(action) - - // Reset selection (navigating to another selectable screen) - selectionModel.consume() - // Drop keyboard as it's no longer needed imm.hide() } private fun updateSelection(selected: List) { - searchAdapter.setSelected(selected) - if (requireBinding().searchToolbarOverlay.updateSelectionAmount(selected.size) && + searchAdapter.setSelectedItems(selected) + if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) && selected.isNotEmpty()) { imm.hide() } diff --git a/app/src/main/res/layout/fragment_detail.xml b/app/src/main/res/layout/fragment_detail.xml index 2e7d0ddfd..d62acc2e2 100644 --- a/app/src/main/res/layout/fragment_detail.xml +++ b/app/src/main/res/layout/fragment_detail.xml @@ -13,11 +13,19 @@ app:liftOnScroll="true" app:liftOnScrollTargetViewId="@id/detail_recycler"> - + android:layout_height="wrap_content"> + + + + + diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 1012cb24a..f4c994502 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -13,8 +13,8 @@ style="@style/Widget.Auxio.AppBarLayout" android:fitsSystemWindows="true"> - @@ -26,7 +26,7 @@ app:menu="@menu/menu_home" app:title="@string/info_app_name" /> - + - @@ -49,7 +49,7 @@ - +