detail: add selection
Add selection to the detail views.
This commit is contained in:
parent
32d01f2027
commit
813daed644
24 changed files with 277 additions and 327 deletions
|
@ -48,6 +48,10 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||||
*
|
*
|
||||||
* TODO: Migrate to material animation system
|
* TODO: Migrate to material animation system
|
||||||
*
|
*
|
||||||
|
* TODO: Re-document project
|
||||||
|
*
|
||||||
|
* TODO: Unit testing
|
||||||
|
*
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
|
@ -32,7 +32,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
||||||
import org.oxycblt.auxio.list.Item
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
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].
|
* A fragment that shows information for a particular [Album].
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
class AlbumDetailFragment : ListFragment<FragmentDetailBinding>() {
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
private val args: AlbumDetailFragmentArgs by navArgs()
|
private val args: AlbumDetailFragmentArgs by navArgs()
|
||||||
|
@ -64,7 +64,7 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
AlbumDetailAdapter.Callback(
|
AlbumDetailAdapter.Callback(
|
||||||
::handleClick,
|
::handleClick,
|
||||||
::handleOpenItemMenu,
|
::handleOpenItemMenu,
|
||||||
{},
|
::handleSelect,
|
||||||
::handlePlay,
|
::handlePlay,
|
||||||
::handleShuffle,
|
::handleShuffle,
|
||||||
::handleOpenSortMenu,
|
::handleOpenSortMenu,
|
||||||
|
@ -81,7 +81,7 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setAlbumUid(args.albumUid)
|
setupSelectionToolbar(binding.detailSelectionToolbar)
|
||||||
|
|
||||||
binding.detailToolbar.apply {
|
binding.detailToolbar.apply {
|
||||||
inflateMenu(R.menu.menu_album_detail)
|
inflateMenu(R.menu.menu_album_detail)
|
||||||
|
@ -96,11 +96,14 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
|
|
||||||
// -- VIEWMODEL SETUP ---
|
// -- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
detailModel.setAlbumUid(args.albumUid)
|
||||||
|
|
||||||
collectImmediately(detailModel.currentAlbum, ::updateItem)
|
collectImmediately(detailModel.currentAlbum, ::updateItem)
|
||||||
collectImmediately(detailModel.albumData, detailAdapter::submitList)
|
collectImmediately(detailModel.albumData, detailAdapter::submitList)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
|
collectImmediately(selectionModel.selected, ::handleSelection)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||||
|
@ -108,13 +111,13 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleClick(item: Item) {
|
override fun onRealClick(music: Music) {
|
||||||
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
|
check(music is Song) { "Unexpected datatype: ${music::class.java}"}
|
||||||
when (settings.detailPlaybackMode) {
|
when (settings.detailPlaybackMode) {
|
||||||
null,
|
null,
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(music)
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(music)
|
||||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
MusicMode.ARTISTS -> playbackModel.playFromArtist(music)
|
||||||
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,6 +182,26 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
requireBinding().detailToolbar.title = album.resolveName(requireContext())
|
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?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
when (item) {
|
when (item) {
|
||||||
|
@ -221,7 +244,7 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Scroll to an [song]. */
|
/** Scroll to a [song]. */
|
||||||
private fun scrollToItem(song: Song) {
|
private fun scrollToItem(song: Song) {
|
||||||
// Calculate where the item for the currently played song is
|
// Calculate where the item for the currently played song is
|
||||||
val pos = detailModel.albumData.value.indexOf(song)
|
val pos = detailModel.albumData.value.indexOf(song)
|
||||||
|
@ -241,24 +264,9 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
private fun handleSelection(selected: List<Music>) {
|
||||||
val binding = requireBinding()
|
detailAdapter.setSelectedItems(selected)
|
||||||
|
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -279,4 +287,5 @@ class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
snapPreference: Int
|
snapPreference: Int
|
||||||
): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
|
): Int = (boxStart + (boxEnd - boxStart) / 2) - (viewStart + (viewEnd - viewStart) / 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
||||||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
||||||
import org.oxycblt.auxio.list.Item
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Music
|
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].
|
* A fragment that shows information for a particular [Artist].
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
class ArtistDetailFragment : ListFragment<FragmentDetailBinding>() {
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
private val args: ArtistDetailFragmentArgs by navArgs()
|
||||||
|
@ -61,7 +61,7 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
DetailAdapter.Callback(
|
DetailAdapter.Callback(
|
||||||
::handleClick,
|
::handleClick,
|
||||||
::handleOpenItemMenu,
|
::handleOpenItemMenu,
|
||||||
{},
|
::handleSelect,
|
||||||
::handlePlay,
|
::handlePlay,
|
||||||
::handleShuffle,
|
::handleShuffle,
|
||||||
::handleOpenSortMenu))
|
::handleOpenSortMenu))
|
||||||
|
@ -77,7 +77,7 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setArtistUid(args.artistUid)
|
setupSelectionToolbar(binding.detailSelectionToolbar)
|
||||||
|
|
||||||
binding.detailToolbar.apply {
|
binding.detailToolbar.apply {
|
||||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||||
|
@ -92,11 +92,14 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
detailModel.setArtistUid(args.artistUid)
|
||||||
|
|
||||||
collectImmediately(detailModel.currentArtist, ::updateItem)
|
collectImmediately(detailModel.currentArtist, ::updateItem)
|
||||||
collectImmediately(detailModel.artistData, detailAdapter::submitList)
|
collectImmediately(detailModel.artistData, detailAdapter::submitList)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
|
collectImmediately(selectionModel.selected, ::handleSelection)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||||
|
@ -104,21 +107,21 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleClick(item: Item) {
|
override fun onRealClick(music: Music) {
|
||||||
when (item) {
|
when (music) {
|
||||||
is Song -> {
|
is Song -> {
|
||||||
when (settings.detailPlaybackMode) {
|
when (settings.detailPlaybackMode) {
|
||||||
null ->
|
null ->
|
||||||
playbackModel.playFromArtist(
|
playbackModel.playFromArtist(
|
||||||
item, unlikelyToBeNull(detailModel.currentArtist.value))
|
music, unlikelyToBeNull(detailModel.currentArtist.value))
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(music)
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(music)
|
||||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
MusicMode.ARTISTS -> playbackModel.playFromArtist(music)
|
||||||
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Album -> navModel.exploreNavigateTo(item)
|
is Album -> navModel.exploreNavigateTo(music)
|
||||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
else -> error("Unexpected datatype: ${music::class.simpleName}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,6 +183,20 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
requireBinding().detailToolbar.title = artist.resolveName(requireContext())
|
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?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
|
|
||||||
|
@ -210,17 +227,8 @@ class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
private fun handleSelection(selected: List<Music>) {
|
||||||
var item: Item? = null
|
detailAdapter.setSelectedItems(selected)
|
||||||
|
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||||
if (parent is Album) {
|
|
||||||
item = parent
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parent is Artist && parent == unlikelyToBeNull(detailModel.currentArtist.value)) {
|
|
||||||
item = song
|
|
||||||
}
|
|
||||||
|
|
||||||
detailAdapter.updateIndicator(item, isPlaying)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
||||||
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
|
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
|
||||||
import org.oxycblt.auxio.list.Item
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
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].
|
* A fragment that shows information for a particular [Genre].
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
class GenreDetailFragment : ListFragment<FragmentDetailBinding>() {
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
|
||||||
private val args: GenreDetailFragmentArgs by navArgs()
|
private val args: GenreDetailFragmentArgs by navArgs()
|
||||||
|
@ -62,10 +62,11 @@ class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
DetailAdapter.Callback(
|
DetailAdapter.Callback(
|
||||||
::handleClick,
|
::handleClick,
|
||||||
::handleOpenItemMenu,
|
::handleOpenItemMenu,
|
||||||
{},
|
::handleSelect,
|
||||||
::handlePlay,
|
::handlePlay,
|
||||||
::handleShuffle,
|
::handleShuffle,
|
||||||
::handleOpenSortMenu))
|
::handleOpenSortMenu))
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||||
|
@ -77,7 +78,7 @@ class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentDetailBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentDetailBinding, savedInstanceState: Bundle?) {
|
||||||
detailModel.setGenreUid(args.genreUid)
|
setupSelectionToolbar(binding.detailSelectionToolbar)
|
||||||
|
|
||||||
binding.detailToolbar.apply {
|
binding.detailToolbar.apply {
|
||||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||||
|
@ -92,11 +93,14 @@ class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
detailModel.setGenreUid(args.genreUid)
|
||||||
|
|
||||||
collectImmediately(detailModel.currentGenre, ::handleItemChange)
|
collectImmediately(detailModel.currentGenre, ::handleItemChange)
|
||||||
collectImmediately(detailModel.genreData, detailAdapter::submitList)
|
collectImmediately(detailModel.genreData, detailAdapter::submitList)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||||
|
collectImmediately(selectionModel.selected, ::handleSelection)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||||
|
@ -117,20 +121,20 @@ class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleClick(item: Item) {
|
override fun onRealClick(music: Music) {
|
||||||
when (item) {
|
when (music) {
|
||||||
is Artist -> navModel.exploreNavigateTo(item)
|
is Artist -> navModel.exploreNavigateTo(music)
|
||||||
is Song ->
|
is Song ->
|
||||||
when (settings.detailPlaybackMode) {
|
when (settings.detailPlaybackMode) {
|
||||||
null ->
|
null ->
|
||||||
playbackModel.playFromGenre(
|
playbackModel.playFromGenre(
|
||||||
item, unlikelyToBeNull(detailModel.currentGenre.value))
|
music, unlikelyToBeNull(detailModel.currentGenre.value))
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(item)
|
MusicMode.SONGS -> playbackModel.playFromAll(music)
|
||||||
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
|
MusicMode.ALBUMS -> playbackModel.playFromAlbum(music)
|
||||||
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
|
MusicMode.ARTISTS -> playbackModel.playFromArtist(music)
|
||||||
else -> error("Unexpected playback mode: ${settings.detailPlaybackMode}")
|
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<FragmentDetailBinding>() {
|
||||||
requireBinding().detailToolbar.title = genre.resolveName(requireContext())
|
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?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> {
|
is Song -> {
|
||||||
|
@ -201,17 +220,8 @@ class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
private fun handleSelection(selected: List<Music>) {
|
||||||
var item: Item? = null
|
detailAdapter.setSelectedItems(selected)
|
||||||
|
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
||||||
if (parent is Artist) {
|
|
||||||
item = parent
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parent is Genre && parent.uid == unlikelyToBeNull(detailModel.currentGenre.value).uid) {
|
|
||||||
item = song
|
|
||||||
}
|
|
||||||
|
|
||||||
detailAdapter.updateIndicator(item, isPlaying)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
||||||
import org.oxycblt.auxio.detail.DiscHeader
|
import org.oxycblt.auxio.detail.DiscHeader
|
||||||
import org.oxycblt.auxio.list.Item
|
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.list.recycler.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Song
|
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.
|
* An adapter for displaying [Album] information and it's children.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AlbumDetailAdapter(private val callback: AlbumDetailAdapter.Callback) :
|
class AlbumDetailAdapter(private val callback: Callback) :
|
||||||
DetailAdapter(callback, DIFFER) {
|
DetailAdapter(callback, DIFFER) {
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
|
@ -180,7 +180,7 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
|
||||||
}
|
}
|
||||||
|
|
||||||
private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) :
|
private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) :
|
||||||
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||||
fun bind(item: Song, callback: AlbumDetailAdapter.Callback) {
|
fun bind(item: Song, callback: AlbumDetailAdapter.Callback) {
|
||||||
// Hide the track number view if the song does not have a track.
|
// Hide the track number view if the song does not have a track.
|
||||||
if (item.track != null) {
|
if (item.track != null) {
|
||||||
|
@ -215,6 +215,10 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
||||||
binding.songTrackBg.isPlaying = isPlaying
|
binding.songTrackBg.isPlaying = isPlaying
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
binding.root.isActivated = isSelected
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_SONG
|
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ALBUM_SONG
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,9 @@ import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ItemMenuCallback
|
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.PlayingIndicatorAdapter
|
||||||
|
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
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) :
|
private class ArtistAlbumViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||||
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||||
fun bind(item: Album, callback: ItemMenuCallback) {
|
fun bind(item: Album, callback: ItemSelectCallback) {
|
||||||
binding.parentImage.bind(item)
|
binding.parentImage.bind(item)
|
||||||
binding.parentName.text = item.resolveName(binding.context)
|
binding.parentName.text = item.resolveName(binding.context)
|
||||||
binding.parentInfo.text =
|
binding.parentInfo.text =
|
||||||
item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
|
item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
|
||||||
binding.parentMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
binding.parentMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
||||||
binding.root.setOnClickListener { callback.onClick(item) }
|
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) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
|
@ -168,6 +177,10 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
|
||||||
binding.parentImage.isPlaying = isPlaying
|
binding.parentImage.isPlaying = isPlaying
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
binding.root.isActivated = isSelected
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_ALBUM
|
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) :
|
private class ArtistSongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
||||||
fun bind(item: Song, callback: ItemMenuCallback) {
|
fun bind(item: Song, callback: ItemSelectCallback) {
|
||||||
binding.songAlbumCover.bind(item)
|
binding.songAlbumCover.bind(item)
|
||||||
binding.songName.text = item.resolveName(binding.context)
|
binding.songName.text = item.resolveName(binding.context)
|
||||||
binding.songInfo.text = item.album.resolveName(binding.context)
|
binding.songInfo.text = item.album.resolveName(binding.context)
|
||||||
binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
||||||
binding.root.setOnClickListener { callback.onClick(item) }
|
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) {
|
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||||
|
@ -197,6 +217,10 @@ private class ArtistSongViewHolder private constructor(private val binding: Item
|
||||||
binding.songAlbumCover.isPlaying = isPlaying
|
binding.songAlbumCover.isPlaying = isPlaying
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateSelectionIndicator(isSelected: Boolean) {
|
||||||
|
binding.root.isActivated = isSelected
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_SONG
|
const val VIEW_TYPE = IntegerTable.VIEW_TYPE_ARTIST_SONG
|
||||||
|
|
||||||
|
|
|
@ -29,17 +29,14 @@ import org.oxycblt.auxio.detail.SortHeader
|
||||||
import org.oxycblt.auxio.list.Header
|
import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ItemSelectCallback
|
import org.oxycblt.auxio.list.ItemSelectCallback
|
||||||
import org.oxycblt.auxio.list.recycler.AuxioRecyclerView
|
import org.oxycblt.auxio.list.recycler.*
|
||||||
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.util.context
|
import org.oxycblt.auxio.util.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
import org.oxycblt.auxio.util.inflater
|
||||||
|
|
||||||
abstract class DetailAdapter(
|
abstract class DetailAdapter(
|
||||||
private val callback: Callback,
|
private val callback: Callback,
|
||||||
diffCallback: DiffUtil.ItemCallback<Item>
|
diffCallback: DiffUtil.ItemCallback<Item>
|
||||||
) : PlayingIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
) : SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
||||||
@Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size
|
@Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size
|
||||||
|
|
||||||
override fun getItemViewType(position: Int) =
|
override fun getItemViewType(position: Int) =
|
||||||
|
|
|
@ -45,7 +45,8 @@ import org.oxycblt.auxio.home.list.AlbumListFragment
|
||||||
import org.oxycblt.auxio.home.list.ArtistListFragment
|
import org.oxycblt.auxio.home.list.ArtistListFragment
|
||||||
import org.oxycblt.auxio.home.list.GenreListFragment
|
import org.oxycblt.auxio.home.list.GenreListFragment
|
||||||
import org.oxycblt.auxio.home.list.SongListFragment
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
@ -63,7 +64,7 @@ import org.oxycblt.auxio.util.*
|
||||||
* respective item.
|
* respective item.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
class HomeFragment : ListFragment<FragmentHomeBinding>() {
|
||||||
private val homeModel: HomeViewModel by androidActivityViewModels()
|
private val homeModel: HomeViewModel by androidActivityViewModels()
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
private val musicModel: MusicViewModel by activityViewModels()
|
||||||
|
|
||||||
|
@ -96,7 +97,7 @@ class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
|
||||||
binding.homeAppbar.addOnOffsetChangedListener { _, it -> handleAppBarAnimation(it) }
|
binding.homeAppbar.addOnOffsetChangedListener { _, it -> handleAppBarAnimation(it) }
|
||||||
setupOverlay(binding.homeToolbarOverlay)
|
setupSelectionToolbar(binding.homeSelectionToolbar)
|
||||||
binding.homeToolbar.setOnMenuItemClickListener {
|
binding.homeToolbar.setOnMenuItemClickListener {
|
||||||
handleHomeMenuItem(it)
|
handleHomeMenuItem(it)
|
||||||
true
|
true
|
||||||
|
@ -171,7 +172,7 @@ class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
val range = binding.homeAppbar.totalScrollRange
|
val range = binding.homeAppbar.totalScrollRange
|
||||||
|
|
||||||
binding.homeToolbarOverlay.alpha =
|
binding.homeSelectionToolbar.alpha =
|
||||||
1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2))
|
1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2))
|
||||||
|
|
||||||
binding.homeContent.updatePadding(
|
binding.homeContent.updatePadding(
|
||||||
|
@ -182,8 +183,6 @@ class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.action_search -> {
|
R.id.action_search -> {
|
||||||
logD("Navigating to search")
|
logD("Navigating to search")
|
||||||
// Reset selection (navigating to another selectable screen)
|
|
||||||
selectionModel.consume()
|
|
||||||
initAxisTransitions(MaterialSharedAxis.Z)
|
initAxisTransitions(MaterialSharedAxis.Z)
|
||||||
findNavController().navigate(HomeFragmentDirections.actionShowSearch())
|
findNavController().navigate(HomeFragmentDirections.actionShowSearch())
|
||||||
}
|
}
|
||||||
|
@ -382,15 +381,13 @@ class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
||||||
else -> return
|
else -> return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset selection (navigating to another selectable screen)
|
|
||||||
selectionModel.consume()
|
|
||||||
initAxisTransitions(MaterialSharedAxis.X)
|
initAxisTransitions(MaterialSharedAxis.X)
|
||||||
findNavController().navigate(action)
|
findNavController().navigate(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
if (binding.homeToolbarOverlay.updateSelectionAmount(selected.size) &&
|
if (binding.homeSelectionToolbar.updateSelectionAmount(selected.size) &&
|
||||||
selected.isNotEmpty()) {
|
selected.isNotEmpty()) {
|
||||||
logD("Significant selection occurred, expanding AppBar")
|
logD("Significant selection occurred, expanding AppBar")
|
||||||
// Significant enough change where we want to expand the RecyclerView
|
// Significant enough change where we want to expand the RecyclerView
|
||||||
|
@ -409,7 +406,7 @@ class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupTabs(binding: FragmentHomeBinding) {
|
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) {
|
if (homeModel.tabs.size == 1) {
|
||||||
// A single tab makes the tab layout redundant, hide it and disable the collapsing
|
// A single tab makes the tab layout redundant, hide it and disable the collapsing
|
||||||
// behavior.
|
// behavior.
|
||||||
|
|
|
@ -28,7 +28,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.list.*
|
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.AlbumViewHolder
|
||||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
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.
|
* A [HomeListFragment] for showing a list of [Album]s.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class AlbumListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
class AlbumListFragment : ListFragment<FragmentHomeListBinding>() {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
private val homeAdapter =
|
private val homeAdapter =
|
||||||
|
@ -69,7 +69,7 @@ class AlbumListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.albums, homeAdapter::replaceList)
|
collectImmediately(homeModel.albums, homeAdapter::replaceList)
|
||||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
||||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,7 +78,7 @@ class AlbumListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
binding.homeRecycler.adapter = null
|
binding.homeRecycler.adapter = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(music: Music) {
|
override fun onRealClick(music: Music) {
|
||||||
check(music is Album) { "Unexpected datatype: ${music::class.java}" }
|
check(music is Album) { "Unexpected datatype: ${music::class.java}" }
|
||||||
navModel.exploreNavigateTo(music)
|
navModel.exploreNavigateTo(music)
|
||||||
}
|
}
|
||||||
|
@ -128,10 +128,10 @@ class AlbumListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
}
|
}
|
||||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||||
if (parent is Album) {
|
if (parent is Album) {
|
||||||
homeAdapter.updateIndicator(parent, isPlaying)
|
homeAdapter.setPlayingItem(parent, isPlaying)
|
||||||
} else {
|
} else {
|
||||||
// Ignore playback not from albums
|
// Ignore playback not from albums
|
||||||
homeAdapter.updateIndicator(null, isPlaying)
|
homeAdapter.setPlayingItem(null, isPlaying)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.list.*
|
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.ArtistViewHolder
|
||||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
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.
|
* A [HomeListFragment] for showing a list of [Artist]s.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class ArtistListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
class ArtistListFragment : ListFragment<FragmentHomeListBinding>() {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
private val homeAdapter =
|
private val homeAdapter =
|
||||||
|
@ -64,7 +64,7 @@ class ArtistListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.artists, homeAdapter::replaceList)
|
collectImmediately(homeModel.artists, homeAdapter::replaceList)
|
||||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
||||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ class ArtistListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(music: Music) {
|
override fun onRealClick(music: Music) {
|
||||||
check(music is Artist) { "Unexpected datatype: ${music::class.java}" }
|
check(music is Artist) { "Unexpected datatype: ${music::class.java}" }
|
||||||
navModel.exploreNavigateTo(music)
|
navModel.exploreNavigateTo(music)
|
||||||
}
|
}
|
||||||
|
@ -104,10 +104,10 @@ class ArtistListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
|
|
||||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||||
if (parent is Artist) {
|
if (parent is Artist) {
|
||||||
homeAdapter.updateIndicator(parent, isPlaying)
|
homeAdapter.setPlayingItem(parent, isPlaying)
|
||||||
} else {
|
} else {
|
||||||
// Ignore playback not from artists
|
// Ignore playback not from artists
|
||||||
homeAdapter.updateIndicator(null, isPlaying)
|
homeAdapter.setPlayingItem(null, isPlaying)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.list.*
|
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.GenreViewHolder
|
||||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
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.
|
* A [HomeListFragment] for showing a list of [Genre]s.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class GenreListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
class GenreListFragment : ListFragment<FragmentHomeListBinding>() {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
private val homeAdapter =
|
private val homeAdapter =
|
||||||
|
@ -63,7 +63,7 @@ class GenreListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.genres, homeAdapter::replaceList)
|
collectImmediately(homeModel.genres, homeAdapter::replaceList)
|
||||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
||||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ class GenreListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(music: Music) {
|
override fun onRealClick(music: Music) {
|
||||||
check(music is Genre) { "Unexpected datatype: ${music::class.java}" }
|
check(music is Genre) { "Unexpected datatype: ${music::class.java}" }
|
||||||
navModel.exploreNavigateTo(music)
|
navModel.exploreNavigateTo(music)
|
||||||
}
|
}
|
||||||
|
@ -103,10 +103,10 @@ class GenreListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
|
|
||||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||||
if (parent is Genre) {
|
if (parent is Genre) {
|
||||||
homeAdapter.updateIndicator(parent, isPlaying)
|
homeAdapter.setPlayingItem(parent, isPlaying)
|
||||||
} else {
|
} else {
|
||||||
// Ignore playback not from genres
|
// Ignore playback not from genres
|
||||||
homeAdapter.updateIndicator(null, isPlaying)
|
homeAdapter.setPlayingItem(null, isPlaying)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||||
import org.oxycblt.auxio.home.HomeViewModel
|
import org.oxycblt.auxio.home.HomeViewModel
|
||||||
import org.oxycblt.auxio.list.*
|
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.SelectionIndicatorAdapter
|
||||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
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.
|
* A [HomeListFragment] for showing a list of [Song]s.
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SongListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
class SongListFragment : ListFragment<FragmentHomeListBinding>() {
|
||||||
private val homeModel: HomeViewModel by activityViewModels()
|
private val homeModel: HomeViewModel by activityViewModels()
|
||||||
|
|
||||||
private val homeAdapter =
|
private val homeAdapter =
|
||||||
|
@ -73,7 +73,7 @@ class SongListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
collectImmediately(homeModel.songs, homeAdapter::replaceList)
|
collectImmediately(homeModel.songs, homeAdapter::replaceList)
|
||||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
collectImmediately(selectionModel.selected, homeAdapter::setSelectedItems)
|
||||||
collectImmediately(
|
collectImmediately(
|
||||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ class SongListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(music: Music) {
|
override fun onRealClick(music: Music) {
|
||||||
check(music is Song) { "Unexpected datatype: ${music::class.java}" }
|
check(music is Song) { "Unexpected datatype: ${music::class.java}" }
|
||||||
when (settings.libPlaybackMode) {
|
when (settings.libPlaybackMode) {
|
||||||
MusicMode.SONGS -> playbackModel.playFromAll(music)
|
MusicMode.SONGS -> playbackModel.playFromAll(music)
|
||||||
|
@ -142,10 +142,10 @@ class SongListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||||
if (parent == null) {
|
if (parent == null) {
|
||||||
homeAdapter.updateIndicator(song, isPlaying)
|
homeAdapter.setPlayingItem(song, isPlaying)
|
||||||
} else {
|
} else {
|
||||||
// Ignore playback that is not from all songs
|
// Ignore playback that is not from all songs
|
||||||
homeAdapter.updateIndicator(null, isPlaying)
|
homeAdapter.setPlayingItem(null, isPlaying)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,18 +39,3 @@ open class ItemSelectCallback(
|
||||||
onOpenMenu: (Item, View) -> Unit,
|
onOpenMenu: (Item, View) -> Unit,
|
||||||
val onSelect: (Item) -> Unit
|
val onSelect: (Item) -> Unit
|
||||||
) : ItemMenuCallback(onClick, onOpenMenu)
|
) : 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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.list
|
package org.oxycblt.auxio.list
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.MenuRes
|
import androidx.annotation.MenuRes
|
||||||
|
@ -25,10 +26,9 @@ import androidx.fragment.app.activityViewModels
|
||||||
import androidx.viewbinding.ViewBinding
|
import androidx.viewbinding.ViewBinding
|
||||||
import org.oxycblt.auxio.MainFragmentDirections
|
import org.oxycblt.auxio.MainFragmentDirections
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.*
|
||||||
import org.oxycblt.auxio.music.Song
|
|
||||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.shared.MainNavigationAction
|
import org.oxycblt.auxio.shared.MainNavigationAction
|
||||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
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.logD
|
||||||
import org.oxycblt.auxio.util.showToast
|
import org.oxycblt.auxio.util.showToast
|
||||||
|
|
||||||
/**
|
abstract class ListFragment<VB : ViewBinding> : ViewBindingFragment<VB>() {
|
||||||
* A fragment capable of creating menus. Automatically keeps track of and disposes of menus,
|
protected val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
* preventing UI issues and memory leaks.
|
|
||||||
* @author OxygenCobalt
|
|
||||||
*/
|
|
||||||
abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|
||||||
private var currentMenu: PopupMenu? = null
|
private var currentMenu: PopupMenu? = null
|
||||||
|
|
||||||
protected val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
protected val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||||
protected val navModel: NavigationViewModel by activityViewModels()
|
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
|
* Opens the given menu in context of [song]. Assumes that the menu is only composed of common
|
||||||
* [Song] options.
|
* [Song] options.
|
||||||
|
@ -207,10 +257,4 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: T) {
|
|
||||||
super.onDestroyBinding(binding)
|
|
||||||
currentMenu?.dismiss()
|
|
||||||
currentMenu = 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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<VB : ViewBinding> : MenuFragment<VB>() {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -44,7 +44,12 @@ abstract class PlayingIndicatorAdapter<VH : RecyclerView.ViewHolder> : RecyclerV
|
||||||
|
|
||||||
abstract val currentList: List<Item>
|
abstract val currentList: List<Item>
|
||||||
|
|
||||||
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
|
var updatedItem = false
|
||||||
|
|
||||||
if (currentItem != item) {
|
if (currentItem != item) {
|
||||||
|
|
|
@ -37,7 +37,10 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSelected(items: List<Music>) {
|
/**
|
||||||
|
* Update the list of selected [items] within the adapter.
|
||||||
|
*/
|
||||||
|
fun setSelectedItems(items: List<Music>) {
|
||||||
val oldSelectedItems = selectedItems
|
val oldSelectedItems = selectedItems
|
||||||
val newSelectedItems = items.toSet()
|
val newSelectedItems = items.toSet()
|
||||||
if (newSelectedItems == oldSelectedItems) {
|
if (newSelectedItems == oldSelectedItems) {
|
||||||
|
|
|
@ -26,7 +26,6 @@ import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||||
import org.oxycblt.auxio.list.Header
|
import org.oxycblt.auxio.list.Header
|
||||||
import org.oxycblt.auxio.list.ItemSelectCallback
|
import org.oxycblt.auxio.list.ItemSelectCallback
|
||||||
import org.oxycblt.auxio.list.MenuItemListener
|
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
@ -41,20 +40,6 @@ import org.oxycblt.auxio.util.inflater
|
||||||
*/
|
*/
|
||||||
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
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) {
|
fun bind(item: Song, callback: ItemSelectCallback) {
|
||||||
binding.songAlbumCover.bind(item)
|
binding.songAlbumCover.bind(item)
|
||||||
|
@ -100,21 +85,6 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||||
class AlbumViewHolder private constructor(private val binding: ItemParentBinding) :
|
class AlbumViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
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) {
|
fun bind(item: Album, callback: ItemSelectCallback) {
|
||||||
binding.parentImage.bind(item)
|
binding.parentImage.bind(item)
|
||||||
binding.parentName.text = item.resolveName(binding.context)
|
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) :
|
class ArtistViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
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) {
|
fun bind(item: Artist, callback: ItemSelectCallback) {
|
||||||
binding.parentImage.bind(item)
|
binding.parentImage.bind(item)
|
||||||
binding.parentName.text = item.resolveName(binding.context)
|
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) :
|
class GenreViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||||
SelectionIndicatorAdapter.ViewHolder(binding.root) {
|
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) {
|
fun bind(item: Genre, callback: ItemSelectCallback) {
|
||||||
binding.parentImage.bind(item)
|
binding.parentImage.bind(item)
|
||||||
binding.parentName.text = item.resolveName(binding.context)
|
binding.parentName.text = item.resolveName(binding.context)
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.list
|
package org.oxycblt.auxio.list.selection
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
@ -43,6 +43,10 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
MaterialToolbar(context).apply {
|
MaterialToolbar(context).apply {
|
||||||
setNavigationIcon(R.drawable.ic_close_24)
|
setNavigationIcon(R.drawable.ic_close_24)
|
||||||
inflateMenu(R.menu.menu_selection_actions)
|
inflateMenu(R.menu.menu_selection_actions)
|
||||||
|
|
||||||
|
if (isInEditMode) {
|
||||||
|
isInvisible = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var fadeThroughAnimator: ValueAnimator? = null
|
private var fadeThroughAnimator: ValueAnimator? = null
|
|
@ -15,7 +15,7 @@
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.list
|
package org.oxycblt.auxio.list.selection
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
@ -46,9 +46,7 @@ class SelectionViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Clear and return all selected items. */
|
/** Clear and return all selected items. */
|
||||||
fun consume(): List<Music> {
|
fun consume() = _selected.value.also { _selected.value = listOf() }
|
||||||
return _selected.value.also { _selected.value = listOf() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
|
@ -32,7 +32,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
||||||
import org.oxycblt.auxio.list.Item
|
import org.oxycblt.auxio.list.Item
|
||||||
import org.oxycblt.auxio.list.ItemSelectCallback
|
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.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
|
@ -48,7 +48,7 @@ import org.oxycblt.auxio.util.*
|
||||||
* better keyboard logic, recycler updating, and chips
|
* better keyboard logic, recycler updating, and chips
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
class SearchFragment : ListFragment<FragmentSearchBinding>() {
|
||||||
|
|
||||||
// SearchViewModel is only scoped to this Fragment
|
// SearchViewModel is only scoped to this Fragment
|
||||||
private val searchModel: SearchViewModel by androidViewModels()
|
private val searchModel: SearchViewModel by androidViewModels()
|
||||||
|
@ -73,7 +73,7 @@ class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
||||||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater)
|
override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater)
|
||||||
|
|
||||||
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
|
||||||
setupOverlay(binding.searchToolbarOverlay)
|
setupSelectionToolbar(binding.searchSelectionToolbar)
|
||||||
|
|
||||||
binding.searchToolbar.apply {
|
binding.searchToolbar.apply {
|
||||||
val itemIdToSelect =
|
val itemIdToSelect =
|
||||||
|
@ -124,7 +124,7 @@ class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
||||||
binding.searchRecycler.adapter = null
|
binding.searchRecycler.adapter = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onClick(music: Music) {
|
override fun onRealClick(music: Music) {
|
||||||
when (music) {
|
when (music) {
|
||||||
is Song ->
|
is Song ->
|
||||||
when (settings.libPlaybackMode) {
|
when (settings.libPlaybackMode) {
|
||||||
|
@ -138,8 +138,6 @@ class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleSearchNavigateUp() {
|
private fun handleSearchNavigateUp() {
|
||||||
// Reset selection (navigating to another selectable screen)
|
|
||||||
selectionModel.consume()
|
|
||||||
// Drop keyboard as it's no longer needed
|
// Drop keyboard as it's no longer needed
|
||||||
imm.hide()
|
imm.hide()
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
|
@ -176,7 +174,7 @@ class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||||
searchAdapter.updateIndicator(parent ?: song, isPlaying)
|
searchAdapter.setPlayingItem(parent ?: song, isPlaying)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleNavigation(item: Music?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
|
@ -190,17 +188,13 @@ class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
findNavController().navigate(action)
|
findNavController().navigate(action)
|
||||||
|
|
||||||
// Reset selection (navigating to another selectable screen)
|
|
||||||
selectionModel.consume()
|
|
||||||
|
|
||||||
// Drop keyboard as it's no longer needed
|
// Drop keyboard as it's no longer needed
|
||||||
imm.hide()
|
imm.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
searchAdapter.setSelected(selected)
|
searchAdapter.setSelectedItems(selected)
|
||||||
if (requireBinding().searchToolbarOverlay.updateSelectionAmount(selected.size) &&
|
if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) &&
|
||||||
selected.isNotEmpty()) {
|
selected.isNotEmpty()) {
|
||||||
imm.hide()
|
imm.hide()
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,20 @@
|
||||||
app:liftOnScroll="true"
|
app:liftOnScroll="true"
|
||||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||||
|
|
||||||
|
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||||
|
android:id="@+id/detail_selection_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/detail_toolbar"
|
android:id="@+id/detail_toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:navigationIcon="@drawable/ic_back_24" />
|
app:navigationIcon="@drawable/ic_back_24" />
|
||||||
|
|
||||||
|
</org.oxycblt.auxio.list.selection.SelectionToolbarOverlay>
|
||||||
|
|
||||||
|
|
||||||
</org.oxycblt.auxio.detail.DetailAppBarLayout>
|
</org.oxycblt.auxio.detail.DetailAppBarLayout>
|
||||||
|
|
||||||
<org.oxycblt.auxio.list.recycler.AuxioRecyclerView
|
<org.oxycblt.auxio.list.recycler.AuxioRecyclerView
|
||||||
|
|
|
@ -13,8 +13,8 @@
|
||||||
style="@style/Widget.Auxio.AppBarLayout"
|
style="@style/Widget.Auxio.AppBarLayout"
|
||||||
android:fitsSystemWindows="true">
|
android:fitsSystemWindows="true">
|
||||||
|
|
||||||
<org.oxycblt.auxio.list.SelectionToolbarOverlay
|
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||||
android:id="@+id/home_toolbar_overlay"
|
android:id="@+id/home_selection_toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
app:menu="@menu/menu_home"
|
app:menu="@menu/menu_home"
|
||||||
app:title="@string/info_app_name" />
|
app:title="@string/info_app_name" />
|
||||||
|
|
||||||
</org.oxycblt.auxio.list.SelectionToolbarOverlay>
|
</org.oxycblt.auxio.list.selection.SelectionToolbarOverlay>
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/home_tabs"
|
android:id="@+id/home_tabs"
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
app:liftOnScroll="true"
|
app:liftOnScroll="true"
|
||||||
app:liftOnScrollTargetViewId="@id/search_recycler">
|
app:liftOnScrollTargetViewId="@id/search_recycler">
|
||||||
|
|
||||||
<org.oxycblt.auxio.list.SelectionToolbarOverlay
|
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||||
android:id="@+id/search_toolbar_overlay"
|
android:id="@+id/search_selection_toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@
|
||||||
|
|
||||||
</com.google.android.material.appbar.MaterialToolbar>
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
</org.oxycblt.auxio.list.SelectionToolbarOverlay>
|
</org.oxycblt.auxio.list.selection.SelectionToolbarOverlay>
|
||||||
|
|
||||||
</org.oxycblt.auxio.shared.AuxioAppBarLayout>
|
</org.oxycblt.auxio.shared.AuxioAppBarLayout>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue