all: refactor list management

Refactor list management (largely callbacks) into a declarative system.

This should make it easier to re-use selection components across the
app.
This commit is contained in:
Alexander Capehart 2022-12-18 10:57:33 -07:00
parent 8aeb6d092e
commit 32d01f2027
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
87 changed files with 989 additions and 911 deletions

View file

@ -26,8 +26,6 @@ import androidx.core.view.ViewCompat
import androidx.core.view.isInvisible
import androidx.core.view.updatePadding
import androidx.fragment.app.activityViewModels
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
@ -39,13 +37,12 @@ import org.oxycblt.auxio.databinding.FragmentMainBinding
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackSheetBehavior
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.queue.QueueSheetBehavior
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.ui.selection.SelectionViewModel
import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior
import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment
import org.oxycblt.auxio.util.*
/**
@ -83,17 +80,17 @@ class MainFragment :
insets
}
// Send meaningful accessibility events for bottom sheets
ViewCompat.setAccessibilityPaneTitle(
binding.playbackSheet, context.getString(R.string.lbl_playback))
ViewCompat.setAccessibilityPaneTitle(
binding.queueSheet, context.getString(R.string.lbl_queue))
val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
if (queueSheetBehavior != null) {
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
unlikelyToBeNull(binding.handleWrapper).setOnClickListener {
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED &&
@ -146,7 +143,7 @@ class MainFragment :
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f)
@ -154,7 +151,8 @@ class MainFragment :
val halfOutRatio = min(playbackRatio * 2, 1f)
val halfInPlaybackRatio = max(playbackRatio - 0.5f, 0f) * 2
val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
if (queueSheetBehavior != null) {
// Queue sheet, take queue into account so the playback bar is shown and the playback
@ -262,7 +260,7 @@ class MainFragment :
private fun tryExpandAll() {
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
// State is collapsed and non-hidden, expand
@ -273,12 +271,12 @@ class MainFragment :
private fun tryCollapseAll() {
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
// Make sure the queue is also collapsed here.
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
queueSheetBehavior?.state = NeoBottomSheetBehavior.STATE_COLLAPSED
@ -288,11 +286,11 @@ class MainFragment :
private fun tryUnhideAll() {
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_HIDDEN) {
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
// Queue sheet behavior is either collapsed or expanded, no hiding needed
queueSheetBehavior?.isDraggable = true
@ -308,11 +306,11 @@ class MainFragment :
private fun tryHideAll() {
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
// Make these views non-draggable so the user can't halt the hiding event.
@ -336,9 +334,9 @@ class MainFragment :
override fun handleOnBackPressed() {
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
if (queueSheetBehavior != null &&
queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
@ -361,9 +359,9 @@ class MainFragment :
fun updateEnabledState() {
val binding = requireBinding()
val playbackSheetBehavior =
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
val queueSheetBehavior =
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
val exploreNavController = binding.exploreNavHost.findNavController()

View file

@ -22,7 +22,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.core.view.children
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
@ -32,6 +31,8 @@ import com.google.android.material.transition.MaterialSharedAxis
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.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
@ -40,8 +41,6 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.canScroll
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
@ -54,16 +53,23 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* A fragment that shows information for a particular [Album].
* @author OxygenCobalt
*/
class AlbumDetailFragment :
MenuFragment<FragmentDetailBinding>(),
Toolbar.OnMenuItemClickListener,
AlbumDetailAdapter.Listener {
class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
private val detailModel: DetailViewModel by activityViewModels()
private val args: AlbumDetailFragmentArgs by navArgs()
private val detailAdapter = AlbumDetailAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val detailAdapter =
AlbumDetailAdapter(
AlbumDetailAdapter.Callback(
::handleClick,
::handleOpenItemMenu,
{},
::handlePlay,
::handleShuffle,
::handleOpenSortMenu,
::handleArtistNavigation))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@ -80,14 +86,17 @@ class AlbumDetailFragment :
binding.detailToolbar.apply {
inflateMenu(R.menu.menu_album_detail)
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@AlbumDetailFragment)
setOnMenuItemClickListener {
handleDetailMenuItem(it)
true
}
}
binding.detailRecycler.adapter = detailAdapter
// -- VIEWMODEL SETUP ---
collectImmediately(detailModel.currentAlbum, ::handleItemChange)
collectImmediately(detailModel.currentAlbum, ::updateItem)
collectImmediately(detailModel.albumData, detailAdapter::submitList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
@ -96,35 +105,10 @@ class AlbumDetailFragment :
override fun onDestroyBinding(binding: FragmentDetailBinding) {
super.onDestroyBinding(binding)
binding.detailToolbar.apply {
setNavigationOnClickListener(null)
setOnMenuItemClickListener(null)
}
binding.detailRecycler.adapter = null
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_play_next -> {
playbackModel.playNext(unlikelyToBeNull(detailModel.currentAlbum.value))
requireContext().showToast(R.string.lng_queue_added)
true
}
R.id.action_queue_add -> {
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentAlbum.value))
requireContext().showToast(R.string.lng_queue_added)
true
}
R.id.action_go_artist -> {
onNavigateToArtist()
true
}
else -> false
}
}
override fun onItemClick(item: Item) {
private fun handleClick(item: Item) {
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
when (settings.detailPlaybackMode) {
null,
@ -135,21 +119,21 @@ class AlbumDetailFragment :
}
}
override fun onOpenMenu(item: Item, anchor: View) {
private fun handleOpenItemMenu(item: Item, anchor: View) {
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
musicMenu(anchor, R.menu.menu_album_song_actions, item)
openMusicMenu(anchor, R.menu.menu_album_song_actions, item)
}
override fun onPlayParent() {
private fun handlePlay() {
playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value))
}
override fun onShuffleParent() {
private fun handleShuffle() {
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value))
}
override fun onShowSortMenu(anchor: View) {
menu(anchor, R.menu.menu_album_sort) {
private fun handleOpenSortMenu(anchor: View) {
openMenu(anchor, R.menu.menu_album_sort) {
val sort = detailModel.albumSort
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
@ -166,11 +150,27 @@ class AlbumDetailFragment :
}
}
override fun onNavigateToArtist() {
private fun handleArtistNavigation() {
navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists)
}
private fun handleItemChange(album: Album?) {
private fun handleDetailMenuItem(item: MenuItem) {
when (item.itemId) {
R.id.action_play_next -> {
playbackModel.playNext(unlikelyToBeNull(detailModel.currentAlbum.value))
requireContext().showToast(R.string.lng_queue_added)
}
R.id.action_queue_add -> {
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentAlbum.value))
requireContext().showToast(R.string.lng_queue_added)
}
R.id.action_go_artist -> {
handleArtistNavigation()
}
}
}
private fun updateItem(album: Album?) {
if (album == null) {
findNavController().navigateUp()
return

View file

@ -21,7 +21,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@ -30,6 +29,8 @@ import org.oxycblt.auxio.R
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.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
@ -38,8 +39,6 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context
@ -51,14 +50,22 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* A fragment that shows information for a particular [Artist].
* @author OxygenCobalt
*/
class ArtistDetailFragment :
MenuFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
private val detailModel: DetailViewModel by activityViewModels()
private val args: ArtistDetailFragmentArgs by navArgs()
private val detailAdapter = ArtistDetailAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val detailAdapter =
ArtistDetailAdapter(
DetailAdapter.Callback(
::handleClick,
::handleOpenItemMenu,
{},
::handlePlay,
::handleShuffle,
::handleOpenSortMenu))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@ -75,14 +82,17 @@ class ArtistDetailFragment :
binding.detailToolbar.apply {
inflateMenu(R.menu.menu_genre_artist_detail)
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@ArtistDetailFragment)
setOnMenuItemClickListener {
handleDetailMenuItem(it)
true
}
}
binding.detailRecycler.adapter = detailAdapter
// --- VIEWMODEL SETUP ---
collectImmediately(detailModel.currentArtist, ::handleItemChange)
collectImmediately(detailModel.currentArtist, ::updateItem)
collectImmediately(detailModel.artistData, detailAdapter::submitList)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
@ -91,31 +101,10 @@ class ArtistDetailFragment :
override fun onDestroyBinding(binding: FragmentDetailBinding) {
super.onDestroyBinding(binding)
binding.detailToolbar.apply {
setNavigationOnClickListener(null)
setOnMenuItemClickListener(null)
}
binding.detailRecycler.adapter = null
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.action_play_next -> {
playbackModel.playNext(unlikelyToBeNull(detailModel.currentArtist.value))
requireContext().showToast(R.string.lng_queue_added)
true
}
R.id.action_queue_add -> {
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentArtist.value))
requireContext().showToast(R.string.lng_queue_added)
true
}
else -> false
}
}
override fun onItemClick(item: Item) {
private fun handleClick(item: Item) {
when (item) {
is Song -> {
when (settings.detailPlaybackMode) {
@ -133,24 +122,24 @@ class ArtistDetailFragment :
}
}
override fun onOpenMenu(item: Item, anchor: View) {
private fun handleOpenItemMenu(item: Item, anchor: View) {
when (item) {
is Song -> musicMenu(anchor, R.menu.menu_artist_song_actions, item)
is Album -> musicMenu(anchor, R.menu.menu_artist_album_actions, item)
is Song -> openMusicMenu(anchor, R.menu.menu_artist_song_actions, item)
is Album -> openMusicMenu(anchor, R.menu.menu_artist_album_actions, item)
else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}
override fun onPlayParent() {
private fun handlePlay() {
playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value))
}
override fun onShuffleParent() {
private fun handleShuffle() {
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentArtist.value))
}
override fun onShowSortMenu(anchor: View) {
menu(anchor, R.menu.menu_artist_sort) {
private fun handleOpenSortMenu(anchor: View) {
openMenu(anchor, R.menu.menu_artist_sort) {
val sort = detailModel.artistSort
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
@ -169,7 +158,20 @@ class ArtistDetailFragment :
}
}
private fun handleItemChange(artist: Artist?) {
private fun handleDetailMenuItem(item: MenuItem) {
when (item.itemId) {
R.id.action_play_next -> {
playbackModel.playNext(unlikelyToBeNull(detailModel.currentArtist.value))
requireContext().showToast(R.string.lng_queue_added)
}
R.id.action_queue_add -> {
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentArtist.value))
requireContext().showToast(R.string.lng_queue_added)
}
}
}
private fun updateItem(artist: Artist?) {
if (artist == null) {
findNavController().navigateUp()
return

View file

@ -31,7 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.appbar.AppBarLayout
import java.lang.reflect.Field
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.AuxioAppBarLayout
import org.oxycblt.auxio.shared.AuxioAppBarLayout
import org.oxycblt.auxio.util.lazyReflectedField
/**

View file

@ -30,6 +30,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -40,8 +42,6 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.storage.MimeType
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW

View file

@ -21,7 +21,6 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
@ -30,6 +29,8 @@ import org.oxycblt.auxio.R
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.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -39,8 +40,6 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context
@ -52,14 +51,21 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* A fragment that shows information for a particular [Genre].
* @author OxygenCobalt
*/
class GenreDetailFragment :
MenuFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
private val detailModel: DetailViewModel by activityViewModels()
private val args: GenreDetailFragmentArgs by navArgs()
private val detailAdapter = GenreDetailAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val detailAdapter =
GenreDetailAdapter(
DetailAdapter.Callback(
::handleClick,
::handleOpenItemMenu,
{},
::handlePlay,
::handleShuffle,
::handleOpenSortMenu))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@ -76,7 +82,10 @@ class GenreDetailFragment :
binding.detailToolbar.apply {
inflateMenu(R.menu.menu_genre_artist_detail)
setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@GenreDetailFragment)
setOnMenuItemClickListener {
handleDetailMenuItem(it)
true
}
}
binding.detailRecycler.adapter = detailAdapter
@ -92,31 +101,23 @@ class GenreDetailFragment :
override fun onDestroyBinding(binding: FragmentDetailBinding) {
super.onDestroyBinding(binding)
binding.detailToolbar.apply {
setNavigationOnClickListener(null)
setOnMenuItemClickListener(null)
}
binding.detailRecycler.adapter = null
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
private fun handleDetailMenuItem(item: MenuItem) {
when (item.itemId) {
R.id.action_play_next -> {
playbackModel.playNext(unlikelyToBeNull(detailModel.currentGenre.value))
requireContext().showToast(R.string.lng_queue_added)
true
}
R.id.action_queue_add -> {
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentGenre.value))
requireContext().showToast(R.string.lng_queue_added)
true
}
else -> false
}
}
override fun onItemClick(item: Item) {
private fun handleClick(item: Item) {
when (item) {
is Artist -> navModel.exploreNavigateTo(item)
is Song ->
@ -133,24 +134,24 @@ class GenreDetailFragment :
}
}
override fun onOpenMenu(item: Item, anchor: View) {
private fun handleOpenItemMenu(item: Item, anchor: View) {
when (item) {
is Artist -> musicMenu(anchor, R.menu.menu_artist_actions, item)
is Song -> musicMenu(anchor, R.menu.menu_song_actions, item)
is Artist -> openMusicMenu(anchor, R.menu.menu_artist_actions, item)
is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item)
else -> error("Unexpected datatype: ${item::class.simpleName}")
}
}
override fun onPlayParent() {
private fun handlePlay() {
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value))
}
override fun onShuffleParent() {
private fun handleShuffle() {
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value))
}
override fun onShowSortMenu(anchor: View) {
menu(anchor, R.menu.menu_genre_sort) {
private fun handleOpenSortMenu(anchor: View) {
openMenu(anchor, R.menu.menu_genre_sort) {
val sort = detailModel.genreSort
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending

View file

@ -27,7 +27,7 @@ import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately

View file

@ -27,13 +27,12 @@ import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
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.SimpleItemCallback
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater
@ -42,8 +41,8 @@ import org.oxycblt.auxio.util.inflater
* An adapter for displaying [Album] information and it's children.
* @author OxygenCobalt
*/
class AlbumDetailAdapter(private val listener: Listener) :
DetailAdapter<AlbumDetailAdapter.Listener>(listener, DIFFER) {
class AlbumDetailAdapter(private val callback: AlbumDetailAdapter.Callback) :
DetailAdapter(callback, DIFFER) {
override fun getItemViewType(position: Int) =
when (differ.currentList[position]) {
@ -70,9 +69,9 @@ class AlbumDetailAdapter(private val listener: Listener) :
if (payloads.isEmpty()) {
when (val item = differ.currentList[position]) {
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
is Album -> (holder as AlbumDetailViewHolder).bind(item, callback)
is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
is Song -> (holder as AlbumSongViewHolder).bind(item, callback)
}
}
}
@ -99,15 +98,23 @@ class AlbumDetailAdapter(private val listener: Listener) :
}
}
interface Listener : DetailAdapter.Listener {
fun onNavigateToArtist()
}
class Callback(
onClick: (Item) -> Unit,
onOpenItemMenu: (Item, View) -> Unit,
onSelect: (Item) -> Unit,
onPlay: () -> Unit,
onShuffle: () -> Unit,
onOpenSortMenu: (View) -> Unit,
val onNavigateToArtist: () -> Unit
) :
DetailAdapter.Callback(
onClick, onOpenItemMenu, onSelect, onPlay, onShuffle, onOpenSortMenu)
}
private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Album, listener: AlbumDetailAdapter.Listener) {
fun bind(item: Album, callback: AlbumDetailAdapter.Callback) {
binding.detailCover.bind(item)
binding.detailType.text = binding.context.getString(item.releaseType.stringRes)
@ -115,7 +122,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
binding.detailSubhead.apply {
text = item.resolveArtistContents(context)
setOnClickListener { listener.onNavigateToArtist() }
setOnClickListener { callback.onNavigateToArtist() }
}
binding.detailInfo.apply {
@ -128,8 +135,8 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
text = context.getString(R.string.fmt_three, date, songCount, duration)
}
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
binding.detailPlayButton.setOnClickListener { callback.onPlay() }
binding.detailShuffleButton.setOnClickListener { callback.onShuffle() }
}
companion object {
@ -174,7 +181,7 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) :
PlayingIndicatorAdapter.ViewHolder(binding.root) {
fun bind(item: Song, listener: MenuItemListener) {
fun bind(item: Song, callback: AlbumDetailAdapter.Callback) {
// Hide the track number view if the song does not have a track.
if (item.track != null) {
binding.songTrack.apply {
@ -193,11 +200,11 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
binding.songName.text = item.resolveName(binding.context)
binding.songDuration.text = item.durationMs.formatDurationMs(false)
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) }
binding.root.apply {
setOnClickListener { listener.onItemClick(item) }
setOnClickListener { callback.onClick(item) }
setOnLongClickListener {
listener.onSelect(item)
callback.onSelect(item)
true
}
}

View file

@ -26,13 +26,13 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemDetailBinding
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.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater
@ -42,8 +42,7 @@ import org.oxycblt.auxio.util.inflater
* one actually contains both album information and song information.
* @author OxygenCobalt
*/
class ArtistDetailAdapter(private val listener: Listener) :
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
class ArtistDetailAdapter(private val callback: Callback) : DetailAdapter(callback, DIFFER) {
override fun getItemViewType(position: Int) =
when (differ.currentList[position]) {
@ -70,9 +69,9 @@ class ArtistDetailAdapter(private val listener: Listener) :
if (payloads.isEmpty()) {
when (val item = differ.currentList[position]) {
is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener)
is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener)
is Song -> (holder as ArtistSongViewHolder).bind(item, listener)
is Artist -> (holder as ArtistDetailViewHolder).bind(item, callback)
is Album -> (holder as ArtistAlbumViewHolder).bind(item, callback)
is Song -> (holder as ArtistSongViewHolder).bind(item, callback)
}
}
}
@ -103,7 +102,7 @@ class ArtistDetailAdapter(private val listener: Listener) :
private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Artist, listener: DetailAdapter.Listener) {
fun bind(item: Artist, callback: DetailAdapter.Callback) {
binding.detailCover.bind(item)
binding.detailType.text = binding.context.getString(R.string.lbl_artist)
binding.detailName.text = item.resolveName(binding.context)
@ -132,8 +131,8 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
binding.detailShuffleButton.isVisible = false
}
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
binding.detailPlayButton.setOnClickListener { callback.onPlay() }
binding.detailShuffleButton.setOnClickListener { callback.onShuffle() }
}
companion object {
@ -155,13 +154,13 @@ 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, listener: MenuItemListener) {
fun bind(item: Album, callback: ItemMenuCallback) {
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 { listener.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) }
binding.parentMenu.setOnClickListener { callback.onOpenMenu(item, it) }
binding.root.setOnClickListener { callback.onClick(item) }
}
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
@ -185,12 +184,12 @@ 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, listener: MenuItemListener) {
fun bind(item: Song, callback: ItemMenuCallback) {
binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.album.resolveName(binding.context)
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) }
binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) }
binding.root.setOnClickListener { callback.onClick(item) }
}
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {

View file

@ -26,22 +26,20 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.detail.SortHeader
import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.HeaderViewHolder
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
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.util.context
import org.oxycblt.auxio.util.inflater
abstract class DetailAdapter<L : DetailAdapter.Listener>(
private val listener: L,
abstract class DetailAdapter(
private val callback: Callback,
diffCallback: DiffUtil.ItemCallback<Item>
) : PlayingIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
private var isPlaying = false
@Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size
override fun getItemViewType(position: Int) =
@ -71,7 +69,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
if (payloads.isEmpty()) {
when (item) {
is Header -> (holder as HeaderViewHolder).bind(item)
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, callback)
}
}
@ -107,20 +105,23 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
}
}
interface Listener : MenuItemListener {
fun onPlayParent()
fun onShuffleParent()
fun onShowSortMenu(anchor: View)
}
open class Callback(
onClick: (Item) -> Unit,
onOpenItemMenu: (Item, View) -> Unit,
onSelect: (Item) -> Unit,
val onPlay: () -> Unit,
val onShuffle: () -> Unit,
val onOpenSortMenu: (View) -> Unit
) : ItemSelectCallback(onClick, onOpenItemMenu, onSelect)
}
class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: SortHeader, listener: DetailAdapter.Listener) {
fun bind(item: SortHeader, callback: DetailAdapter.Callback) {
binding.headerTitle.text = binding.context.getString(item.string)
binding.headerButton.apply {
TooltipCompat.setTooltipText(this, contentDescription)
setOnClickListener(listener::onShowSortMenu)
setOnClickListener(callback.onOpenSortMenu)
}
}

View file

@ -24,13 +24,13 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
import org.oxycblt.auxio.ui.recycler.SongViewHolder
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater
@ -39,8 +39,7 @@ import org.oxycblt.auxio.util.inflater
* An adapter for displaying genre information and it's children.
* @author OxygenCobalt
*/
class GenreDetailAdapter(private val listener: Listener) :
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
class GenreDetailAdapter(private val callback: Callback) : DetailAdapter(callback, DIFFER) {
override fun getItemViewType(position: Int) =
when (differ.currentList[position]) {
is Genre -> GenreDetailViewHolder.VIEW_TYPE
@ -66,9 +65,9 @@ class GenreDetailAdapter(private val listener: Listener) :
if (payloads.isEmpty()) {
when (val item = differ.currentList[position]) {
is Genre -> (holder as GenreDetailViewHolder).bind(item, listener)
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
is Song -> (holder as SongViewHolder).bind(item, listener)
is Genre -> (holder as GenreDetailViewHolder).bind(item, callback)
is Artist -> (holder as ArtistViewHolder).bind(item, callback)
is Song -> (holder as SongViewHolder).bind(item, callback)
}
}
}
@ -98,7 +97,7 @@ class GenreDetailAdapter(private val listener: Listener) :
private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Genre, listener: DetailAdapter.Listener) {
fun bind(item: Genre, callback: DetailAdapter.Callback) {
binding.detailCover.bind(item)
binding.detailType.text = binding.context.getString(R.string.lbl_genre)
binding.detailName.text = item.resolveName(binding.context)
@ -109,8 +108,8 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
binding.detailPlayButton.setOnClickListener { callback.onPlay() }
binding.detailShuffleButton.setOnClickListener { callback.onShuffle() }
}
companion object {

View file

@ -23,7 +23,6 @@ import android.view.MenuItem
import android.view.View
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isVisible
import androidx.core.view.iterator
import androidx.core.view.updatePadding
@ -46,6 +45,7 @@ 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.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -55,12 +55,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
import org.oxycblt.auxio.ui.selection.SelectionViewModel
import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.util.*
/**
@ -68,12 +63,9 @@ import org.oxycblt.auxio.util.*
* respective item.
* @author OxygenCobalt
*/
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener, SelectionToolbarOverlay.Callback {
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
private val homeModel: HomeViewModel by androidActivityViewModels()
private val musicModel: MusicViewModel by activityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
private val selectionModel: SelectionViewModel by activityViewModels()
// lifecycleObject builds this in the creation step, so doing this is okay.
private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject {
@ -103,22 +95,14 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
binding.homeAppbar.apply {
addOnOffsetChangedListener { _, offset ->
val range = binding.homeAppbar.totalScrollRange
binding.homeToolbarOverlay.alpha =
1f - (abs(offset.toFloat()) / (range.toFloat() / 2))
binding.homeContent.updatePadding(
bottom = binding.homeAppbar.totalScrollRange + offset)
}
binding.homeAppbar.addOnOffsetChangedListener { _, it -> handleAppBarAnimation(it) }
setupOverlay(binding.homeToolbarOverlay)
binding.homeToolbar.setOnMenuItemClickListener {
handleHomeMenuItem(it)
true
}
binding.homeToolbarOverlay.callback = this
binding.homeToolbar.setOnMenuItemClickListener(this@HomeFragment)
updateTabConfiguration()
setupTabs(binding)
// Load the track color in manually as it's unclear whether the track actually supports
// using a ColorStateList in the resources
@ -128,17 +112,11 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homePager.apply {
adapter = HomePagerAdapter()
// We know that there will only be a fixed amount of tabs, so we manually set this
// limit to that. This also prevents the appbar lift state from being confused during
// page transitions.
offscreenPageLimit = homeModel.tabs.size
reduceSensitivity(3)
registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) =
homeModel.updateCurrentTab(position)
override fun onPageSelected(position: Int) {
homeModel.setCurrentTab(position)
}
})
TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel))
@ -148,6 +126,22 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
// insets applied to the indexing view before API 30. Fix this by overriding the
// callback with a non-consuming listener.
setOnApplyWindowInsetsListener { _, insets -> insets }
// We know that there will only be a fixed amount of tabs, so we manually set this
// limit to that. This also prevents the appbar lift state from being confused during
// page transitions.
offscreenPageLimit = homeModel.tabs.size
// By default, ViewPager2's sensitivity is high enough to result in vertical scroll
// events being
// registered as horizontal scroll events. Reflect into the internal recyclerview and
// change the
// touch slope so that touch actions will act more as a scroll than as a swipe. Derived
// from:
// https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414
val recycler = VP_RECYCLER_FIELD.get(this@apply)
val slop = RV_TOUCH_SLOP_FIELD.get(recycler) as Int
RV_TOUCH_SLOP_FIELD.set(recycler, slop * 3)
}
binding.homeFab.setOnClickListener { playbackModel.shuffleAll() }
@ -157,9 +151,11 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
collect(homeModel.recreateTabs, ::handleRecreateTabs)
collectImmediately(homeModel.currentTab, ::updateCurrentTab)
collectImmediately(homeModel.songs, homeModel.isFastScrolling, ::updateFab)
collectImmediately(musicModel.indexerState, ::handleIndexerState)
collectImmediately(selectionModel.selected, ::updateSelection)
collectImmediately(musicModel.indexerState, ::updateIndexerState)
collect(navModel.exploreNavigationItem, ::handleNavigation)
collectImmediately(selectionModel.selected, ::updateSelection)
}
override fun onSaveInstanceState(outState: Bundle) {
@ -171,13 +167,18 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
super.onSaveInstanceState(outState)
}
override fun onDestroyBinding(binding: FragmentHomeBinding) {
super.onDestroyBinding(binding)
binding.homeToolbarOverlay.callback = null
binding.homeToolbar.setOnMenuItemClickListener(null)
private fun handleAppBarAnimation(verticalOffset: Int) {
val binding = requireBinding()
val range = binding.homeAppbar.totalScrollRange
binding.homeToolbarOverlay.alpha =
1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2))
binding.homeContent.updatePadding(
bottom = binding.homeAppbar.totalScrollRange + verticalOffset)
}
override fun onMenuItemClick(item: MenuItem): Boolean {
private fun handleHomeMenuItem(item: MenuItem) {
when (item.itemId) {
R.id.action_search -> {
logD("Navigating to search")
@ -206,7 +207,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
.getSortForTab(homeModel.currentTab.value)
.withAscending(item.isChecked))
}
else -> {
// Sorting option was selected, mark it as selected and update the mode
item.isChecked = true
@ -216,23 +216,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))))
}
}
// Always handling an item
return true
}
override fun onClearSelection() {
selectionModel.consume()
}
override fun onPlaySelectionNext() {
playbackModel.playNext(selectionModel.consume())
requireContext().showToast(R.string.lng_queue_added)
}
override fun onAddSelectionToQueue() {
playbackModel.addToQueue(selectionModel.consume())
requireContext().showToast(R.string.lng_queue_added)
}
private fun updateCurrentTab(tab: MusicMode) {
@ -263,7 +246,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
}
}
requireBinding().homeAppbar.liftOnScrollTargetViewId = getRecyclerId(tab)
requireBinding().homeAppbar.liftOnScrollTargetViewId = getTabRecyclerId(tab)
}
private fun updateSortMenu(mode: MusicMode, isVisible: (Int) -> Boolean) {
@ -285,34 +268,24 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private fun handleRecreateTabs(recreate: Boolean) {
if (recreate) {
requireBinding().homePager.recreate()
updateTabConfiguration()
val binding = requireBinding()
binding.homePager.apply {
currentItem = 0
adapter = HomePagerAdapter()
}
setupTabs(binding)
homeModel.finishRecreateTabs()
}
}
private fun updateTabConfiguration() {
val binding = requireBinding()
val toolbarParams = binding.homeToolbarOverlay.layoutParams as AppBarLayout.LayoutParams
if (homeModel.tabs.size == 1) {
// A single tab makes the tab layout redundant, hide it and disable the collapsing
// behavior.
binding.homeTabs.isVisible = false
binding.homeAppbar.setExpanded(true, false)
toolbarParams.scrollFlags = 0
} else {
binding.homeTabs.isVisible = true
toolbarParams.scrollFlags =
AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or
AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
}
}
private fun handleIndexerState(state: Indexer.State?) {
private fun updateIndexerState(state: Indexer.State?) {
val binding = requireBinding()
when (state) {
is Indexer.State.Complete -> handleIndexerResponse(binding, state.response)
is Indexer.State.Indexing -> handleIndexingState(binding, state.indexing)
is Indexer.State.Complete -> setupCompleteState(binding, state.response)
is Indexer.State.Indexing -> setupIndexingState(binding, state.indexing)
null -> {
logD("Indexer is in indeterminate state")
binding.homeIndexingContainer.visibility = View.INVISIBLE
@ -320,7 +293,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
}
}
private fun handleIndexerResponse(binding: FragmentHomeBinding, response: Indexer.Response) {
private fun setupCompleteState(binding: FragmentHomeBinding, response: Indexer.Response) {
if (response is Indexer.Response.Ok) {
binding.homeFab.show()
binding.homeIndexingContainer.visibility = View.INVISIBLE
@ -366,7 +339,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
}
}
private fun handleIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) {
private fun setupIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) {
binding.homeIndexingContainer.visibility = View.VISIBLE
binding.homeIndexingProgress.visibility = View.VISIBLE
binding.homeIndexingAction.visibility = View.INVISIBLE
@ -399,17 +372,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
}
}
private fun updateSelection(selected: List<Music>) {
val binding = requireBinding()
if (binding.homeToolbarOverlay.updateSelectionAmount(selected.size) &&
selected.isNotEmpty()) {
logD("Significant selection occurred, expanding AppBar")
// Significant enough change where we want to expand the RecyclerView
binding.homeAppbar.expandWithRecycler(
binding.homePager.findViewById(getRecyclerId(homeModel.currentTab.value)))
}
}
private fun handleNavigation(item: Music?) {
val action =
when (item) {
@ -426,6 +388,42 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
findNavController().navigate(action)
}
private fun updateSelection(selected: List<Music>) {
val binding = requireBinding()
if (binding.homeToolbarOverlay.updateSelectionAmount(selected.size) &&
selected.isNotEmpty()) {
logD("Significant selection occurred, expanding AppBar")
// Significant enough change where we want to expand the RecyclerView
binding.homeAppbar.expandWithRecycler(
binding.homePager.findViewById(getTabRecyclerId(homeModel.currentTab.value)))
}
}
/** Returns the ID of a RecyclerView that the given [tab] contains */
private fun getTabRecyclerId(tab: MusicMode) =
when (tab) {
MusicMode.SONGS -> R.id.home_song_recycler
MusicMode.ALBUMS -> R.id.home_album_recycler
MusicMode.ARTISTS -> R.id.home_artist_recycler
MusicMode.GENRES -> R.id.home_genre_recycler
}
private fun setupTabs(binding: FragmentHomeBinding) {
val toolbarParams = binding.homeToolbarOverlay.layoutParams as AppBarLayout.LayoutParams
if (homeModel.tabs.size == 1) {
// A single tab makes the tab layout redundant, hide it and disable the collapsing
// behavior.
binding.homeTabs.isVisible = false
binding.homeAppbar.setExpanded(true, false)
toolbarParams.scrollFlags = 0
} else {
binding.homeTabs.isVisible = true
toolbarParams.scrollFlags =
AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or
AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
}
}
private fun initAxisTransitions(axis: Int) {
// Sanity check
check(axis == MaterialSharedAxis.X || axis == MaterialSharedAxis.Z) {
@ -437,35 +435,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
reenterTransition = MaterialSharedAxis(axis, false)
}
/**
* Returns the ID of a RecyclerView that the given [tab] contains
*/
private fun getRecyclerId(tab: MusicMode) =
when (tab) {
MusicMode.SONGS -> R.id.home_song_recycler
MusicMode.ALBUMS -> R.id.home_album_recycler
MusicMode.ARTISTS -> R.id.home_artist_recycler
MusicMode.GENRES -> R.id.home_genre_recycler
}
/**
* By default, ViewPager2's sensitivity is high enough to result in vertical scroll events being
* registered as horizontal scroll events. Reflect into the internal recyclerview and change the
* touch slope so that touch actions will act more as a scroll than as a swipe. Derived from:
* https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414
*/
private fun ViewPager2.reduceSensitivity(by: Int) {
val recycler = VP_RECYCLER_FIELD.get(this@reduceSensitivity)
val slop = RV_TOUCH_SLOP_FIELD.get(recycler) as Int
RV_TOUCH_SLOP_FIELD.set(recycler, slop * by)
}
/** Forces the view to recreate all fragments contained within it. */
private fun ViewPager2.recreate() {
currentItem = 0
adapter = HomePagerAdapter()
}
private inner class HomePagerAdapter :
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {

View file

@ -84,7 +84,7 @@ class HomeViewModel(application: Application) :
}
/** Update the current tab based off of the new ViewPager position. */
fun updateCurrentTab(pos: Int) {
fun setCurrentTab(pos: Int) {
logD("Updating current tab to ${tabs[pos]}")
_currentTab.value = tabs[pos]
}
@ -129,9 +129,9 @@ class HomeViewModel(application: Application) :
* Update the fast scroll state. This is used to control the FAB visibility whenever the user
* begins to fast scroll.
*/
fun updateFastScrolling(scrolling: Boolean) {
logD("Updating fast scrolling state: $scrolling")
_isFastScrolling.value = scrolling
fun setFastScrolling(fastScrolling: Boolean) {
logD("Updating fast scrolling state: $fastScrolling")
_isFastScrolling.value = fastScrolling
}
// --- OVERRIDES ---

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.fastscroll
package org.oxycblt.auxio.home.fastscroll
import android.content.Context
import android.graphics.Canvas

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.fastscroll
package org.oxycblt.auxio.home.fastscroll
import android.content.Context
import android.graphics.Canvas
@ -35,7 +35,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
import org.oxycblt.auxio.list.recycler.AuxioRecyclerView
import org.oxycblt.auxio.util.getDimenSize
import org.oxycblt.auxio.util.getDrawableCompat
import org.oxycblt.auxio.util.isRtl
@ -137,33 +137,26 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
removeCallbacks(hideThumbRunnable)
showScrollbar()
showPopup()
listener?.onFastScrollStart()
} else {
postAutoHideScrollbar()
hidePopup()
listener?.onFastScrollStop()
}
fastScrollCallback?.invoke(field)
}
private val tRect = Rect()
interface PopupProvider {
fun getPopup(pos: Int): String?
}
/** Callback to provide a string to be shown on the popup when an item is passed */
var popupProvider: PopupProvider? = null
var popupProvider: ((Int) -> String?)? = null
interface OnFastScrollListener {
fun onFastScrollStart()
fun onFastScrollStop()
}
class FastScrollCallback(val onStart: () -> Unit, val onEnd: () -> Unit)
/**
* A listener for when a drag event occurs. The value will be true if a drag has begun, and
* A callback for when a drag event occurs. The value will be true if a drag has begun, and
* false if a drag ended.
*/
var listener: OnFastScrollListener? = null
var fastScrollCallback: ((Boolean) -> Unit)? = null
init {
overlay.add(thumbView)
@ -225,7 +218,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (firstAdapterPos != NO_POSITION && provider != null) {
popupView.isInvisible = false
// Get the popup text. If there is none, we default to "?".
popupText = provider.getPopup(firstAdapterPos) ?: "?"
popupText = provider.invoke(firstAdapterPos) ?: "?"
} else {
// No valid position or provider, do not show the popup.
popupView.isInvisible = true

View file

@ -19,11 +19,19 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import java.util.Formatter
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.recycler.AlbumViewHolder
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
@ -31,36 +39,56 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.playback.secsToMs
import org.oxycblt.auxio.ui.recycler.AlbumViewHolder
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately
/**
* A [HomeListFragment] for showing a list of [Album]s.
* @author OxygenCobalt
*/
class AlbumListFragment : HomeListFragment<Album>() {
private val homeAdapter = AlbumAdapter(this)
class AlbumListFragment : SelectionFragment<FragmentHomeListBinding>() {
private val homeModel: HomeViewModel by activityViewModels()
private val homeAdapter =
AlbumAdapter(ItemSelectCallback(::handleClick, ::handleOpenMenu, ::handleClick))
private val formatterSb = StringBuilder(32)
private val formatter = Formatter(formatterSb)
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply {
id = R.id.home_album_recycler
adapter = homeAdapter
popupProvider = ::updatePopup
fastScrollCallback = { homeModel.setFastScrolling(it) }
}
collectImmediately(homeModel.albums, homeAdapter::replaceList)
collectImmediately(selectionModel.selected, homeAdapter::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handleParent)
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
override fun getPopup(pos: Int): String? {
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
super.onDestroyBinding(binding)
binding.homeRecycler.adapter = null
}
override fun onClick(music: Music) {
check(music is Album) { "Unexpected datatype: ${music::class.java}" }
navModel.exploreNavigateTo(music)
}
private fun handleOpenMenu(item: Item, anchor: View) {
check(item is Album) { "Unexpected datatype: ${item::class.java}" }
openMusicMenu(anchor, R.menu.menu_album_actions, item)
}
private fun updatePopup(pos: Int): String? {
val album = homeModel.albums.value[pos]
// Change how we display the popup depending on the mode.
@ -98,18 +126,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
else -> null
}
}
override fun onItemClick(music: Music) {
check(music is Album) { "Unexpected datatype: ${music::class.java}" }
navModel.exploreNavigateTo(music)
}
override fun onOpenMenu(item: Item, anchor: View) {
check(item is Album) { "Unexpected datatype: ${item::class.java}" }
musicMenu(anchor, R.menu.menu_album_actions, item)
}
private fun handleParent(parent: MusicParent?, isPlaying: Boolean) {
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
if (parent is Album) {
homeAdapter.updateIndicator(parent, isPlaying)
} else {
@ -118,7 +135,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
}
}
private class AlbumAdapter(private val listener: MenuItemListener) :
private class AlbumAdapter(private val callback: ItemSelectCallback) :
SelectionIndicatorAdapter<AlbumViewHolder>() {
private val differ = SyncListDiffer(this, AlbumViewHolder.DIFFER)
@ -134,7 +151,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) {
holder.bind(differ.currentList[position], listener)
holder.bind(differ.currentList[position], callback)
}
}

View file

@ -18,21 +18,24 @@
package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
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.recycler.ArtistViewHolder
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.nonZeroOrNull
@ -40,8 +43,14 @@ import org.oxycblt.auxio.util.nonZeroOrNull
* A [HomeListFragment] for showing a list of [Artist]s.
* @author OxygenCobalt
*/
class ArtistListFragment : HomeListFragment<Artist>() {
private val homeAdapter = ArtistAdapter(this)
class ArtistListFragment : SelectionFragment<FragmentHomeListBinding>() {
private val homeModel: HomeViewModel by activityViewModels()
private val homeAdapter =
ArtistAdapter(ItemSelectCallback(::handleClick, ::handleOpenMenu, ::handleSelect))
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
@ -49,14 +58,22 @@ class ArtistListFragment : HomeListFragment<Artist>() {
binding.homeRecycler.apply {
id = R.id.home_artist_recycler
adapter = homeAdapter
popupProvider = ::updatePopup
fastScrollCallback = homeModel::setFastScrolling
}
collectImmediately(homeModel.artists, homeAdapter::replaceList)
collectImmediately(selectionModel.selected, homeAdapter::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handleParent)
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
override fun getPopup(pos: Int): String? {
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
super.onDestroyBinding(binding)
binding.homeRecycler.adapter = null
}
private fun updatePopup(pos: Int): String? {
val artist = homeModel.artists.value[pos]
// Change how we display the popup depending on the mode.
@ -75,17 +92,17 @@ class ArtistListFragment : HomeListFragment<Artist>() {
}
}
override fun onItemClick(music: Music) {
override fun onClick(music: Music) {
check(music is Artist) { "Unexpected datatype: ${music::class.java}" }
navModel.exploreNavigateTo(music)
}
override fun onOpenMenu(item: Item, anchor: View) {
private fun handleOpenMenu(item: Item, anchor: View) {
check(item is Artist) { "Unexpected datatype: ${item::class.java}" }
musicMenu(anchor, R.menu.menu_artist_actions, item)
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
}
private fun handleParent(parent: MusicParent?, isPlaying: Boolean) {
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
if (parent is Artist) {
homeAdapter.updateIndicator(parent, isPlaying)
} else {
@ -94,7 +111,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
}
}
private class ArtistAdapter(private val listener: MenuItemListener) :
private class ArtistAdapter(private val callback: ItemSelectCallback) :
SelectionIndicatorAdapter<ArtistViewHolder>() {
private val differ = SyncListDiffer(this, ArtistViewHolder.DIFFER)
@ -114,7 +131,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) {
holder.bind(differ.currentList[position], listener)
holder.bind(differ.currentList[position], callback)
}
}

View file

@ -18,29 +18,38 @@
package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
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.recycler.GenreViewHolder
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.recycler.GenreViewHolder
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately
/**
* A [HomeListFragment] for showing a list of [Genre]s.
* @author OxygenCobalt
*/
class GenreListFragment : HomeListFragment<Genre>() {
private val homeAdapter = GenreAdapter(this)
class GenreListFragment : SelectionFragment<FragmentHomeListBinding>() {
private val homeModel: HomeViewModel by activityViewModels()
private val homeAdapter =
GenreAdapter(ItemSelectCallback(::handleClick, ::handleOpenMenu, ::handleSelect))
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
@ -48,14 +57,22 @@ class GenreListFragment : HomeListFragment<Genre>() {
binding.homeRecycler.apply {
id = R.id.home_genre_recycler
adapter = homeAdapter
popupProvider = ::updatePopup
fastScrollCallback = homeModel::setFastScrolling
}
collectImmediately(homeModel.genres, homeAdapter::replaceList)
collectImmediately(selectionModel.selected, homeAdapter::updateSelection)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
override fun getPopup(pos: Int): String? {
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
super.onDestroyBinding(binding)
binding.homeRecycler.adapter = null
}
private fun updatePopup(pos: Int): String? {
val genre = homeModel.genres.value[pos]
// Change how we display the popup depending on the mode.
@ -74,17 +91,17 @@ class GenreListFragment : HomeListFragment<Genre>() {
}
}
override fun onItemClick(music: Music) {
override fun onClick(music: Music) {
check(music is Genre) { "Unexpected datatype: ${music::class.java}" }
navModel.exploreNavigateTo(music)
}
override fun onOpenMenu(item: Item, anchor: View) {
private fun handleOpenMenu(item: Item, anchor: View) {
check(item is Genre) { "Unexpected datatype: ${item::class.java}" }
musicMenu(anchor, R.menu.menu_artist_actions, item)
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
}
private fun handlePlayback(parent: MusicParent?, isPlaying: Boolean) {
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
if (parent is Genre) {
homeAdapter.updateIndicator(parent, isPlaying)
} else {
@ -93,7 +110,7 @@ class GenreListFragment : HomeListFragment<Genre>() {
}
}
private class GenreAdapter(private val listener: MenuItemListener) :
private class GenreAdapter(private val callback: ItemSelectCallback) :
SelectionIndicatorAdapter<GenreViewHolder>() {
private val differ = SyncListDiffer(this, GenreViewHolder.DIFFER)
@ -109,7 +126,7 @@ class GenreListFragment : HomeListFragment<Genre>() {
super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) {
holder.bind(differ.currentList[position], listener)
holder.bind(differ.currentList[position], callback)
}
}

View file

@ -1,86 +0,0 @@
/*
* Copyright (c) 2021 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.home.list
import android.os.Bundle
import android.view.LayoutInflater
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.fastscroll.FastScrollRecyclerView
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.selection.SelectionViewModel
import org.oxycblt.auxio.util.androidActivityViewModels
/**
* A Base [Fragment] implementing the base features shared across all list fragments in the home UI.
* @author OxygenCobalt
*/
abstract class HomeListFragment<T : Item> :
MenuFragment<FragmentHomeListBinding>(),
MenuItemListener,
FastScrollRecyclerView.PopupProvider,
FastScrollRecyclerView.OnFastScrollListener {
protected val homeModel: HomeViewModel by androidActivityViewModels()
protected val selectionModel: SelectionViewModel by activityViewModels()
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
binding.homeRecycler.popupProvider = this
binding.homeRecycler.listener = this
}
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
homeModel.updateFastScrolling(false)
binding.homeRecycler.apply {
adapter = null
popupProvider = null
listener = null
}
}
override fun onFastScrollStart() {
homeModel.updateFastScrolling(true)
}
override fun onFastScrollStop() {
homeModel.updateFastScrolling(false)
}
abstract fun onItemClick(music: Music)
override fun onItemClick(item: Item) {
check(item is Music) { "Unexpected datatype: ${item::class.java}" }
if (selectionModel.selected.value.isEmpty()) {
onItemClick(item)
} else {
onSelect(item)
}
}
override fun onSelect(item: Item) {
check(item is Music) { "Unexpected datatype: ${item::class.java}" }
selectionModel.select(item)
}
}

View file

@ -19,11 +19,19 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle
import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import java.util.Formatter
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.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
@ -32,11 +40,6 @@ import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.playback.secsToMs
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.recycler.SelectionIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.SongViewHolder
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context
@ -44,27 +47,43 @@ import org.oxycblt.auxio.util.context
* A [HomeListFragment] for showing a list of [Song]s.
* @author OxygenCobalt
*/
class SongListFragment : HomeListFragment<Song>() {
private val homeAdapter = SongAdapter(this)
class SongListFragment : SelectionFragment<FragmentHomeListBinding>() {
private val homeModel: HomeViewModel by activityViewModels()
private val homeAdapter =
SongAdapter(ItemSelectCallback(::handleClick, ::handleOpenMenu, ::handleSelect))
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val formatterSb = StringBuilder(50)
private val formatter = Formatter(formatterSb)
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply {
id = R.id.home_song_recycler
adapter = homeAdapter
popupProvider = ::updatePopup
fastScrollCallback = homeModel::setFastScrolling
}
collectImmediately(homeModel.songs, homeAdapter::replaceList)
collectImmediately(selectionModel.selected, homeAdapter::updateSelection)
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
}
override fun getPopup(pos: Int): String? {
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
super.onDestroyBinding(binding)
binding.homeRecycler.adapter = null
}
private fun updatePopup(pos: Int): String? {
val song = homeModel.songs.value[pos]
// Change how we display the popup depending on the mode.
@ -106,7 +125,7 @@ class SongListFragment : HomeListFragment<Song>() {
}
}
override fun onItemClick(music: Music) {
override fun onClick(music: Music) {
check(music is Song) { "Unexpected datatype: ${music::class.java}" }
when (settings.libPlaybackMode) {
MusicMode.SONGS -> playbackModel.playFromAll(music)
@ -116,12 +135,12 @@ class SongListFragment : HomeListFragment<Song>() {
}
}
override fun onOpenMenu(item: Item, anchor: View) {
private fun handleOpenMenu(item: Item, anchor: View) {
check(item is Song) { "Unexpected datatype: ${item::class.java}" }
musicMenu(anchor, R.menu.menu_song_actions, item)
openMusicMenu(anchor, R.menu.menu_song_actions, item)
}
private fun handlePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
if (parent == null) {
homeAdapter.updateIndicator(song, isPlaying)
} else {
@ -130,7 +149,7 @@ class SongListFragment : HomeListFragment<Song>() {
}
}
private class SongAdapter(private val listener: MenuItemListener) :
private class SongAdapter(private val callback: ItemSelectCallback) :
SelectionIndicatorAdapter<SongViewHolder>() {
private val differ = SyncListDiffer(this, SongViewHolder.DIFFER)
@ -146,7 +165,7 @@ class SongListFragment : HomeListFragment<Song>() {
super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) {
holder.bind(differ.currentList[position], listener)
holder.bind(differ.currentList[position], callback)
}
}

View file

@ -24,11 +24,11 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemTabBinding
import org.oxycblt.auxio.list.recycler.DialogViewHolder
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.ui.recycler.DialogViewHolder
import org.oxycblt.auxio.util.inflater
class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewHolder>() {
class TabAdapter(private val callback: Callback) : RecyclerView.Adapter<TabViewHolder>() {
var tabs = arrayOf<Tab>()
private set
@ -37,7 +37,7 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.new(parent)
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
holder.bind(tabs[position], listener)
holder.bind(tabs[position], callback)
}
@Suppress("NotifyDatasetChanged")
@ -59,10 +59,10 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
notifyItemMoved(from, to)
}
interface Listener {
fun onVisibilityToggled(mode: MusicMode)
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder)
}
class Callback(
val toggleVisibility: (MusicMode) -> Unit,
val pickUpTab: (RecyclerView.ViewHolder) -> Unit
)
companion object {
val PAYLOAD_TAB_CHANGED = Any()
@ -72,8 +72,8 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
class TabViewHolder private constructor(private val binding: ItemTabBinding) :
DialogViewHolder(binding.root) {
@SuppressLint("ClickableViewAccessibility")
fun bind(item: Tab, listener: TabAdapter.Listener) {
binding.root.setOnClickListener { listener.onVisibilityToggled(item.mode) }
fun bind(item: Tab, callback: TabAdapter.Callback) {
binding.root.setOnClickListener { callback.toggleVisibility(item.mode) }
binding.tabIcon.apply {
setText(
@ -90,7 +90,7 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
binding.tabDragHandle.setOnTouchListener { _, motionEvent ->
binding.tabDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
listener.onPickUpTab(this)
callback.pickUpTab(this)
true
} else false
}

View file

@ -27,7 +27,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding
import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD
@ -35,8 +35,8 @@ import org.oxycblt.auxio.util.logD
* The dialog for customizing library tabs.
* @author OxygenCobalt
*/
class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAdapter.Listener {
private val tabAdapter = TabAdapter(this)
class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>() {
private val tabAdapter = TabAdapter(TabAdapter.Callback(::toggleVisibility, ::pickUpTab))
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val touchHelper: ItemTouchHelper by lifecycleObject {
ItemTouchHelper(TabDragCallback(tabAdapter))
@ -79,7 +79,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
binding.tabRecycler.adapter = null
}
override fun onVisibilityToggled(mode: MusicMode) {
private fun toggleVisibility(mode: MusicMode) {
val index = tabAdapter.tabs.indexOfFirst { it.mode == mode }
if (index > -1) {
val tab = tabAdapter.tabs[index]
@ -95,7 +95,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
tabAdapter.tabs.filterIsInstance<Tab.Visible>().isNotEmpty()
}
override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) {
private fun pickUpTab(viewHolder: RecyclerView.ViewHolder) {
touchHelper.startDrag(viewHolder)
}

View file

@ -0,0 +1,56 @@
/*
* 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.View
import androidx.annotation.StringRes
/** A marker for something that is a RecyclerView item. Has no functionality on it's own. */
interface Item
/** A data object used solely for the "Header" UI element. */
data class Header(
/** The string resource used for the header. */
@StringRes val string: Int
) : Item
open class ItemClickCallback(val onClick: (Item) -> Unit)
open class ItemMenuCallback(onClick: (Item) -> Unit, val onOpenMenu: (Item, View) -> Unit) :
ItemClickCallback(onClick)
open class ItemSelectCallback(
onClick: (Item) -> Unit,
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)
}

View file

@ -15,8 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.fragment
package org.oxycblt.auxio.list
import android.view.MenuItem
import android.view.View
import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu
@ -29,8 +30,9 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
@ -50,11 +52,11 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
* Opens the given menu in context of [song]. Assumes that the menu is only composed of common
* [Song] options.
*/
protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, song: Song) {
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, song: Song) {
logD("Launching new song menu: ${song.rawName}")
musicMenuImpl(anchor, menuRes) { id ->
when (id) {
openMusicMenuImpl(anchor, menuRes) {
when (it.itemId) {
R.id.action_play_next -> {
playbackModel.playNext(song)
requireContext().showToast(R.string.lng_queue_added)
@ -78,8 +80,6 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
error("Unexpected menu item selected")
}
}
true
}
}
@ -87,11 +87,11 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
* Opens the given menu in context of [album]. Assumes that the menu is only composed of common
* [Album] options.
*/
protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) {
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) {
logD("Launching new album menu: ${album.rawName}")
musicMenuImpl(anchor, menuRes) { id ->
when (id) {
openMusicMenuImpl(anchor, menuRes) {
when (it.itemId) {
R.id.action_play -> {
playbackModel.play(album)
}
@ -113,8 +113,6 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
error("Unexpected menu item selected")
}
}
true
}
}
@ -122,11 +120,11 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
* Opens the given menu in context of [artist]. Assumes that the menu is only composed of common
* [Artist] options.
*/
protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) {
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) {
logD("Launching new artist menu: ${artist.rawName}")
musicMenuImpl(anchor, menuRes) { id ->
when (id) {
openMusicMenuImpl(anchor, menuRes) {
when (it.itemId) {
R.id.action_play -> {
playbackModel.play(artist)
}
@ -145,8 +143,6 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
error("Unexpected menu item selected")
}
}
true
}
}
@ -154,11 +150,11 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
* Opens the given menu in context of [genre]. Assumes that the menu is only composed of common
* [Genre] options.
*/
protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) {
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) {
logD("Launching new genre menu: ${genre.rawName}")
musicMenuImpl(anchor, menuRes) { id ->
when (id) {
openMusicMenuImpl(anchor, menuRes) {
when (it.itemId) {
R.id.action_play -> {
playbackModel.play(genre)
}
@ -177,20 +173,27 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
error("Unexpected menu item selected")
}
}
}
}
private fun openMusicMenuImpl(
anchor: View,
@MenuRes menuRes: Int,
onClick: (MenuItem) -> Unit
) {
openMenu(anchor, menuRes) {
setOnMenuItemClickListener { item ->
onClick(item)
true
}
}
private fun musicMenuImpl(anchor: View, @MenuRes menuRes: Int, onSelect: (Int) -> Boolean) {
menu(anchor, menuRes) { setOnMenuItemClickListener { item -> onSelect(item.itemId) } }
}
/**
* Open a generic menu with configuration in [block]. If a menu is already opened, then this
* function is a no-op.
*/
protected fun menu(anchor: View, @MenuRes menuRes: Int, block: PopupMenu.() -> Unit) {
protected fun openMenu(anchor: View, @MenuRes menuRes: Int, block: PopupMenu.() -> Unit) {
if (currentMenu != null) {
logD("Menu already present, not launching")
return

View file

@ -0,0 +1,70 @@
/*
* 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)
}
}

View file

@ -15,15 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.selection
package org.oxycblt.auxio.list
import android.animation.ValueAnimator
import android.content.Context
import android.util.AttributeSet
import android.view.MenuItem
import android.widget.FrameLayout
import androidx.annotation.AttrRes
import androidx.appcompat.widget.Toolbar
import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener
import androidx.core.view.isInvisible
import com.google.android.material.appbar.MaterialToolbar
import org.oxycblt.auxio.R
@ -38,29 +37,12 @@ class SelectionToolbarOverlay
@JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) {
var callback: Callback? = null
private lateinit var innerToolbar: MaterialToolbar
private val selectionToolbar =
MaterialToolbar(context).apply {
setNavigationIcon(R.drawable.ic_close_24)
setNavigationOnClickListener {
callback?.onClearSelection()
}
inflateMenu(R.menu.menu_selection_actions)
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_play_next -> {
callback?.onPlaySelectionNext()
}
R.id.action_queue_add -> {
callback?.onAddSelectionToQueue()
}
}
true
}
}
private var fadeThroughAnimator: ValueAnimator? = null
@ -75,9 +57,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
addView(selectionToolbar)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
callback = null
fun setOnSelectionCancelListener(listener: OnClickListener) {
selectionToolbar.setNavigationOnClickListener(listener)
}
fun setOnMenuItemClickListener(listener: OnMenuItemClickListener) {
selectionToolbar.setOnMenuItemClickListener(listener)
}
/**
@ -151,10 +136,4 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
isInvisible = innerAlpha == 1f
}
}
interface Callback {
fun onClearSelection()
fun onPlaySelectionNext()
fun onAddSelectionToQueue()
}
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.selection
package org.oxycblt.auxio.list
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
@ -33,15 +33,15 @@ class SelectionViewModel : ViewModel() {
get() = _selected
/** Select a music item. */
fun select(item: Music) {
val items = _selected.value.toMutableList()
if (items.remove(item)) {
logD("Unselecting item $item")
_selected.value = items
fun select(music: Music) {
val selected = _selected.value.toMutableList()
if (selected.remove(music)) {
logD("Unselecting item $music")
_selected.value = selected
} else {
logD("Selecting item $item")
items.add(item)
_selected.value = items
logD("Selecting item $music")
selected.add(music)
_selected.value = selected
}
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.recycler
package org.oxycblt.auxio.list.recycler
import android.content.Context
import android.graphics.Rect

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.recycler
package org.oxycblt.auxio.list.recycler
import android.content.Context
import android.util.AttributeSet

View file

@ -15,10 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.recycler
package org.oxycblt.auxio.list.recycler
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.util.logW
/**

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.recycler
package org.oxycblt.auxio.list.recycler
import android.view.View
import androidx.recyclerview.widget.RecyclerView
@ -37,7 +37,7 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
}
}
fun updateSelection(items: List<Music>) {
fun setSelected(items: List<Music>) {
val oldSelectedItems = selectedItems
val newSelectedItems = items.toSet()
if (newSelectedItems == oldSelectedItems) {

View file

@ -0,0 +1,29 @@
/*
* 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.recycler
import androidx.recyclerview.widget.DiffUtil
import org.oxycblt.auxio.list.Item
/**
* A base [DiffUtil.ItemCallback] that automatically provides an implementation of
* [areContentsTheSame] any object that is derived from [Item].
*/
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
final override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem == newItem
}

View file

@ -15,38 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.recycler
package org.oxycblt.auxio.list.recycler
import android.view.View
import androidx.annotation.StringRes
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
/** A marker for something that is a RecyclerView item. Has no functionality on it's own. */
interface Item
/** A data object used solely for the "Header" UI element. */
data class Header(
/** The string resource used for the header. */
@StringRes val string: Int
) : Item
/** 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)
}
/**
* Like AsyncListDiffer, but synchronous. This may seem like it would be inefficient, but in
* practice Auxio's lists tend to be small enough to the point where this does not matter, and
@ -153,11 +127,3 @@ class SyncListDiffer<T>(
currentList = newList
}
}
/**
* A base [DiffUtil.ItemCallback] that automatically provides an implementation of
* [areContentsTheSame] any object that is derived from [Item].
*/
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
final override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem == newItem
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.recycler
package org.oxycblt.auxio.list.recycler
import android.view.View
import androidx.recyclerview.widget.RecyclerView
@ -24,6 +24,9 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemHeaderBinding
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
@ -53,6 +56,21 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
}
}
fun bind(item: Song, callback: ItemSelectCallback) {
binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.resolveArtistContents(binding.context)
binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) }
binding.root.apply {
setOnClickListener { callback.onClick(item) }
setOnLongClickListener {
callback.onSelect(item)
true
}
}
}
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.songAlbumCover.isPlaying = isPlaying
@ -97,6 +115,21 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
}
}
fun bind(item: Album, callback: ItemSelectCallback) {
binding.parentImage.bind(item)
binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text = item.resolveArtistContents(binding.context)
binding.parentMenu.setOnClickListener { callback.onOpenMenu(item, it) }
binding.root.apply {
setOnClickListener { callback.onClick(item) }
setOnLongClickListener {
callback.onSelect(item)
true
}
}
}
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.parentImage.isPlaying = isPlaying
@ -153,6 +186,31 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
}
}
fun bind(item: Artist, callback: ItemSelectCallback) {
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 { callback.onOpenMenu(item, it) }
binding.root.apply {
setOnClickListener { callback.onClick(item) }
setOnLongClickListener {
callback.onSelect(item)
true
}
}
}
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.parentImage.isPlaying = isPlaying
@ -203,6 +261,25 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
}
}
fun bind(item: Genre, callback: ItemSelectCallback) {
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 { callback.onOpenMenu(item, it) }
binding.root.apply {
setOnClickListener { callback.onClick(item) }
setOnLongClickListener {
callback.onSelect(item)
true
}
}
}
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive
binding.parentImage.isPlaying = isPlaying

View file

@ -29,6 +29,7 @@ import kotlin.math.max
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.extractor.parseId3GenreNames
import org.oxycblt.auxio.music.extractor.parseMultiValue
import org.oxycblt.auxio.music.extractor.toUuidOrNull
@ -38,7 +39,6 @@ import org.oxycblt.auxio.music.storage.Path
import org.oxycblt.auxio.music.storage.albumCoverUri
import org.oxycblt.auxio.music.storage.audioUri
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull

View file

@ -124,12 +124,9 @@ class MusicStore private constructor() {
// We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
// song. Do what we can to hopefully find the song the user wanted to open.
val displayName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
songs.find { it.path.name == displayName && it.size == size }
}
}

View file

@ -25,7 +25,7 @@ import com.google.android.material.checkbox.MaterialCheckBox
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {

View file

@ -21,14 +21,14 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemPickerChoiceBinding
import org.oxycblt.auxio.list.ItemClickCallback
import org.oxycblt.auxio.list.recycler.DialogViewHolder
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.ui.recycler.DialogViewHolder
import org.oxycblt.auxio.ui.recycler.ItemClickListener
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater
/** The adapter that displays a list of artist choices in the picker UI. */
class ArtistChoiceAdapter(private val listener: ItemClickListener) :
class ArtistChoiceAdapter(private val callback: ItemClickCallback) :
RecyclerView.Adapter<ArtistChoiceViewHolder>() {
private var artists = listOf<Artist>()
@ -38,7 +38,7 @@ class ArtistChoiceAdapter(private val listener: ItemClickListener) :
ArtistChoiceViewHolder.new(parent)
override fun onBindViewHolder(holder: ArtistChoiceViewHolder, position: Int) =
holder.bind(artists[position], listener)
holder.bind(artists[position], callback)
fun submitList(newArtists: List<Artist>) {
if (newArtists != artists) {
@ -55,10 +55,10 @@ class ArtistChoiceAdapter(private val listener: ItemClickListener) :
*/
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
DialogViewHolder(binding.root) {
fun bind(artist: Artist, listener: ItemClickListener) {
fun bind(artist: Artist, callback: ItemClickCallback) {
binding.pickerImage.bind(artist)
binding.pickerName.text = artist.resolveName(binding.context)
binding.root.setOnClickListener { listener.onItemClick(artist) }
binding.root.setOnClickListener { callback.onClick(artist) }
}
companion object {

View file

@ -21,9 +21,9 @@ import android.os.Bundle
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.shared.NavigationViewModel
/**
* The [ArtistPickerDialog] for ambiguous artist navigation operations.
@ -31,6 +31,7 @@ import org.oxycblt.auxio.ui.recycler.Item
*/
class ArtistNavigationPickerDialog : ArtistPickerDialog() {
private val navModel: NavigationViewModel by activityViewModels()
private val args: ArtistNavigationPickerDialogArgs by navArgs()
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
@ -38,9 +39,9 @@ class ArtistNavigationPickerDialog : ArtistPickerDialog() {
super.onBindingCreated(binding, savedInstanceState)
}
override fun onItemClick(item: Item) {
super.onItemClick(item)
check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" }
override fun onChoiceConfirmed(item: Item) {
super.onChoiceConfirmed(item)
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
navModel.exploreNavigateTo(item)
}
}

View file

@ -24,15 +24,15 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.ItemClickListener
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ItemClickCallback
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
import org.oxycblt.auxio.util.collectImmediately
abstract class ArtistPickerDialog :
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener {
abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>() {
protected val pickerModel: MusicPickerViewModel by viewModels()
private val artistAdapter = ArtistChoiceAdapter(this)
private val artistAdapter = ArtistChoiceAdapter(ItemClickCallback(::onChoiceConfirmed))
override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicPickerBinding.inflate(inflater)
@ -43,6 +43,7 @@ abstract class ArtistPickerDialog :
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
binding.pickerRecycler.adapter = artistAdapter
collectImmediately(pickerModel.currentArtists) { artists ->
if (!artists.isNullOrEmpty()) {
artistAdapter.submitList(artists)
@ -56,7 +57,8 @@ abstract class ArtistPickerDialog :
binding.pickerRecycler.adapter = null
}
override fun onItemClick(item: Item) {
open fun onChoiceConfirmed(item: Item) {
check(item is Artist) { "Unexpected datatype: ${item::class.java}" }
findNavController().navigateUp()
}
}

View file

@ -20,9 +20,9 @@ package org.oxycblt.auxio.music.picker
import android.os.Bundle
import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.androidActivityViewModels
/**
@ -31,6 +31,7 @@ import org.oxycblt.auxio.util.androidActivityViewModels
*/
class ArtistPlaybackPickerDialog : ArtistPickerDialog() {
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val args: ArtistPlaybackPickerDialogArgs by navArgs()
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
@ -38,8 +39,8 @@ class ArtistPlaybackPickerDialog : ArtistPickerDialog() {
super.onBindingCreated(binding, savedInstanceState)
}
override fun onItemClick(item: Item) {
super.onItemClick(item)
override fun onChoiceConfirmed(item: Item) {
super.onChoiceConfirmed(item)
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
pickerModel.currentSong.value?.let { song -> playbackModel.playFromArtist(song, item) }
}

View file

@ -21,7 +21,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemMusicDirBinding
import org.oxycblt.auxio.ui.recycler.DialogViewHolder
import org.oxycblt.auxio.list.recycler.DialogViewHolder
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD
@ -59,7 +59,8 @@ class MusicDirsDialog :
.setPositiveButton(R.string.lbl_save) { _, _ ->
val dirs = settings.getMusicDirs(storageManager)
val newDirs =
MusicDirs(dirs = dirAdapter.dirs, shouldInclude = isInclude(requireBinding()))
MusicDirs(
dirs = dirAdapter.dirs, shouldInclude = isUiModeInclude(requireBinding()))
if (dirs != newDirs) {
logD("Committing changes")
settings.setMusicDirs(newDirs)
@ -122,7 +123,7 @@ class MusicDirsDialog :
super.onSaveInstanceState(outState)
outState.putStringArrayList(
KEY_PENDING_DIRS, ArrayList(dirAdapter.dirs.map { it.toString() }))
outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding()))
outState.putBoolean(KEY_PENDING_MODE, isUiModeInclude(requireBinding()))
}
override fun onDestroyBinding(binding: DialogMusicDirsBinding) {
@ -166,14 +167,14 @@ class MusicDirsDialog :
private fun updateMode() {
val binding = requireBinding()
if (isInclude(binding)) {
if (isUiModeInclude(binding)) {
binding.dirsModeDesc.setText(R.string.set_dirs_mode_include_desc)
} else {
binding.dirsModeDesc.setText(R.string.set_dirs_mode_exclude_desc)
}
}
private fun isInclude(binding: DialogMusicDirsBinding) =
private fun isUiModeInclude(binding: DialogMusicDirsBinding) =
binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include
companion object {

View file

@ -23,7 +23,7 @@ import androidx.core.app.NotificationCompat
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.system.ServiceNotification
import org.oxycblt.auxio.shared.ServiceNotification
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.newMainPendingIntent

View file

@ -35,7 +35,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.system.ForegroundManager
import org.oxycblt.auxio.shared.ForegroundManager
import org.oxycblt.auxio.util.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD

View file

@ -25,9 +25,9 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getAttrColorCompat
@ -63,16 +63,29 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
binding.playbackSong.isSelected = true
binding.playbackInfo.isSelected = true
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }
setupSecondaryActions(binding, Settings(context))
// Load the track color in manually as it's unclear whether the track actually supports
// using a ColorStateList in the resources
binding.playbackProgressBar.trackColor =
context.getColorCompat(R.color.sel_track).defaultColor
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }
// -- VIEWMODEL SETUP ---
// Update the secondary action to match the setting.
collectImmediately(playbackModel.song, ::updateSong)
collectImmediately(playbackModel.isPlaying, ::updatePlaying)
collectImmediately(playbackModel.positionDs, ::updatePosition)
}
when (Settings(context).actionMode) {
override fun onDestroyBinding(binding: FragmentPlaybackBarBinding) {
super.onDestroyBinding(binding)
binding.playbackSong.isSelected = false
binding.playbackInfo.isSelected = false
}
private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, settings: Settings) {
when (settings.actionMode) {
ActionMode.NEXT -> {
binding.playbackSecondaryAction.apply {
setIconResource(R.drawable.ic_skip_next_24)
@ -99,18 +112,6 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
}
}
}
// -- VIEWMODEL SETUP ---
collectImmediately(playbackModel.song, ::updateSong)
collectImmediately(playbackModel.isPlaying, ::updateIsPlaying)
collectImmediately(playbackModel.positionDs, ::updatePosition)
}
override fun onDestroyBinding(binding: FragmentPlaybackBarBinding) {
super.onDestroyBinding(binding)
binding.playbackSong.isSelected = false
binding.playbackInfo.isSelected = false
}
private fun updateSong(song: Song?) {
@ -124,7 +125,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
}
}
private fun updateIsPlaying(isPlaying: Boolean) {
private fun updatePlaying(isPlaying: Boolean) {
requireBinding().playbackPlayPause.isActivated = isPlaying
}

View file

@ -25,7 +25,7 @@ import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.AuxioSheetBehavior
import org.oxycblt.auxio.shared.AuxioBottomSheetBehavior
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen
@ -34,8 +34,8 @@ import org.oxycblt.auxio.util.getDimen
* to make bottom sheets like this work.
* @author OxygenCobalt
*/
class PlaybackSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
AuxioSheetBehavior<V>(context, attributeSet) {
class PlaybackBottomSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
AuxioBottomSheetBehavior<V>(context, attributeSet) {
val sheetBackgroundDrawable =
MaterialShapeDrawable.createWithElevationOverlay(context).apply {
fillColor = context.getAttrColorCompat(R.attr.colorSurface)

View file

@ -24,18 +24,19 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.MainFragmentDirections
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.playback.ui.StyledSeekBar
import org.oxycblt.auxio.ui.MainNavigationAction
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.showToast
import org.oxycblt.auxio.util.systemBarInsetsCompat
@ -47,10 +48,10 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
*
* TODO: Make seek thumb grow when selected
*/
class PlaybackPanelFragment :
MenuFragment<FragmentPlaybackPanelBinding>(),
StyledSeekBar.Callback,
Toolbar.OnMenuItemClickListener {
class PlaybackPanelFragment : ViewBindingFragment<FragmentPlaybackPanelBinding>() {
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val navModel: NavigationViewModel by activityViewModels()
// AudioEffect expects you to use startActivityForResult with the panel intent. Use
// the contract analogue for this since there is no built-in contract for AudioEffect.
private val activityLauncher by lifecycleObject {
@ -76,7 +77,10 @@ class PlaybackPanelFragment :
binding.playbackToolbar.apply {
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.Collapse) }
setOnMenuItemClickListener(this@PlaybackPanelFragment)
setOnMenuItemClickListener {
handleMenuItem(it)
true
}
}
// Make sure we enable marquee on the song info
@ -96,7 +100,7 @@ class PlaybackPanelFragment :
setOnClickListener { playbackModel.song.value?.let { showCurrentAlbum() } }
}
binding.playbackSeekBar.callback = this
binding.playbackSeekBar.onSeekConfirmed = playbackModel::seekTo
binding.playbackRepeat.setOnClickListener { playbackModel.incrementRepeatMode() }
binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() }
@ -115,18 +119,14 @@ class PlaybackPanelFragment :
}
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
binding.playbackToolbar.setOnMenuItemClickListener(null)
// Leaving marquee on will cause a leak
binding.playbackSong.isSelected = false
binding.playbackArtist.isSelected = false
binding.playbackAlbum.isSelected = false
binding.playbackSeekBar.callback = null
}
override fun onMenuItemClick(item: MenuItem): Boolean {
return when (item.itemId) {
private fun handleMenuItem(item: MenuItem) {
when (item.itemId) {
R.id.action_open_equalizer -> {
val equalizerIntent =
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
@ -139,16 +139,12 @@ class PlaybackPanelFragment :
} catch (e: ActivityNotFoundException) {
requireContext().showToast(R.string.err_no_app)
}
true
}
R.id.action_go_artist -> {
showCurrentArtist()
true
}
R.id.action_go_album -> {
showCurrentAlbum()
true
}
R.id.action_song_detail -> {
playbackModel.song.value?.let { song ->
@ -156,15 +152,8 @@ class PlaybackPanelFragment :
MainNavigationAction.Directions(
MainFragmentDirections.actionShowDetails(song.uid)))
}
true
}
else -> false
}
}
override fun seekTo(positionDs: Long) {
playbackModel.seekTo(positionDs)
}
private fun updateSong(song: Song?) {
@ -208,6 +197,7 @@ class PlaybackPanelFragment :
val song = playbackModel.song.value ?: return
navModel.exploreNavigateTo(song.artists)
}
private fun showCurrentAlbum() {
val song = playbackModel.song.value ?: return
navModel.exploreNavigateTo(song.album)

View file

@ -27,10 +27,10 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SongViewHolder
import org.oxycblt.auxio.list.recycler.SyncListDiffer
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.SongViewHolder
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen

View file

@ -24,7 +24,7 @@ import android.view.WindowInsets
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.ui.AuxioSheetBehavior
import org.oxycblt.auxio.shared.AuxioBottomSheetBehavior
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.getDimenSize
@ -35,8 +35,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
* The bottom sheet behavior designed for the queue in particular.
* @author OxygenCobalt
*/
class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
AuxioSheetBehavior<V>(context, attributeSet) {
class QueueBottomSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
AuxioBottomSheetBehavior<V>(context, attributeSet) {
private var barHeight = 0
private var barSpacing = context.getDimenSize(R.dimen.spacing_small)

View file

@ -19,7 +19,6 @@ package org.oxycblt.auxio.playback.queue
import android.os.Bundle
import android.view.LayoutInflater
import androidx.core.view.isInvisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.ItemTouchHelper
@ -28,7 +27,7 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.shared.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
@ -52,16 +51,6 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
binding.queueRecycler.apply {
adapter = queueAdapter
touchHelper.attachToRecyclerView(this)
// Sometimes the scroll can change without the listener being updated, so we also
// check for relayout events.
addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> invalidateDivider() }
addOnScrollListener(
object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
invalidateDivider()
}
})
}
// --- VIEWMODEL SETUP ----
@ -95,10 +84,6 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
queueAdapter.submitList(queue)
}
binding.queueDivider.isInvisible =
(binding.queueRecycler.layoutManager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition() < 1
queueModel.finishReplace()
val scrollTo = queueModel.scrollTo
@ -117,11 +102,4 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
queueAdapter.updateIndicator(index, isPlaying)
}
private fun invalidateDivider() {
val binding = requireBinding()
binding.queueDivider.isInvisible =
(binding.queueRecycler.layoutManager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition() < 1
}
}

View file

@ -25,7 +25,7 @@ import kotlin.math.abs
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
/**

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.ui.system.ServiceNotification
import org.oxycblt.auxio.shared.ServiceNotification
import org.oxycblt.auxio.util.newBroadcastPendingIntent
import org.oxycblt.auxio.util.newMainPendingIntent

View file

@ -55,7 +55,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.system.ForegroundManager
import org.oxycblt.auxio.shared.ForegroundManager
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.widgets.WidgetComponent
import org.oxycblt.auxio.widgets.WidgetProvider

View file

@ -45,7 +45,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
binding.seekBarSlider.addOnChangeListener(this)
}
var callback: Callback? = null
var onSeekConfirmed: ((Long) -> Unit)? = null
/**
* The current position, in seconds. This is the current value of the SeekBar and is indicated
@ -104,17 +104,10 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
logD("Confirming seek")
// End of seek event, send off new value to callback.
isActivated = false
callback?.seekTo(slider.value.toLong())
onSeekConfirmed?.invoke(slider.value.toLong())
}
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
binding.seekBarPosition.text = value.toLong().formatDurationDs(true)
}
interface Callback {
/**
* Called when a seek event was completed and the new position must be seeked to by the app.
*/
fun seekTo(positionDs: Long)
}
}

View file

@ -20,13 +20,14 @@ package org.oxycblt.auxio.search
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.list.*
import org.oxycblt.auxio.list.recycler.*
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.ui.recycler.*
class SearchAdapter(private val listener: MenuItemListener) :
class SearchAdapter(private val callback: ItemSelectCallback) :
SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
private val differ = AsyncListDiffer(this, DIFFER)
@ -61,10 +62,10 @@ class SearchAdapter(private val listener: MenuItemListener) :
if (payloads.isEmpty()) {
when (val item = differ.currentList[position]) {
is Song -> (holder as SongViewHolder).bind(item, listener)
is Album -> (holder as AlbumViewHolder).bind(item, listener)
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
is Genre -> (holder as GenreViewHolder).bind(item, listener)
is Song -> (holder as SongViewHolder).bind(item, callback)
is Album -> (holder as AlbumViewHolder).bind(item, callback)
is Artist -> (holder as ArtistViewHolder).bind(item, callback)
is Genre -> (holder as GenreViewHolder).bind(item, callback)
is Header -> (holder as HeaderViewHolder).bind(item)
}
}

View file

@ -22,16 +22,17 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isInvisible
import androidx.core.view.postDelayed
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.transition.MaterialSharedAxis
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.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -40,26 +41,20 @@ import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.MenuFragment
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.ui.recycler.MenuItemListener
import org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
import org.oxycblt.auxio.ui.selection.SelectionViewModel
import org.oxycblt.auxio.util.*
/**
* A [Fragment] that allows for the searching of the entire music library.
* FIXME: Keyboard logic is really wonky
* A [Fragment] that allows for the searching of the entire music library. TODO: Minor rework with
* better keyboard logic, recycler updating, and chips
* @author OxygenCobalt
*/
class SearchFragment :
MenuFragment<FragmentSearchBinding>(), MenuItemListener, Toolbar.OnMenuItemClickListener, SelectionToolbarOverlay.Callback {
class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
// SearchViewModel is only scoped to this Fragment
private val searchModel: SearchViewModel by androidViewModels()
private val selectionModel: SelectionViewModel by activityViewModels()
private val searchAdapter = SearchAdapter(this)
private val searchAdapter =
SearchAdapter(ItemSelectCallback(::handleClick, ::handleOpenMenu, ::handleSelect))
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val imm: InputMethodManager by lifecycleObject { binding ->
binding.context.getSystemServiceCompat(InputMethodManager::class)
@ -78,7 +73,7 @@ class SearchFragment :
override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
binding.searchToolbarOverlay.callback = this
setupOverlay(binding.searchToolbarOverlay)
binding.searchToolbar.apply {
val itemIdToSelect =
@ -92,17 +87,11 @@ class SearchFragment :
menu.findItem(itemIdToSelect).isChecked = true
setNavigationOnClickListener {
// Reset selection (navigating to another selectable screen)
selectionModel.consume()
// Drop keyboard as it's no longer needed
imm.hide()
findNavController().navigateUp()
setNavigationOnClickListener { handleSearchNavigateUp() }
setOnMenuItemClickListener {
handleSearchMenuItem(it)
true
}
setOnMenuItemClickListener(this@SearchFragment)
}
binding.searchEditText.apply {
@ -113,9 +102,7 @@ class SearchFragment :
if (!launchedKeyboard) {
// Auto-open the keyboard when this view is shown
requestFocus()
postDelayed(200) { imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) }
imm.show(this)
launchedKeyboard = true
}
}
@ -124,76 +111,58 @@ class SearchFragment :
// --- VIEWMODEL SETUP ---
collectImmediately(searchModel.searchResults, ::handleResults)
collectImmediately(searchModel.searchResults, ::updateResults)
collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
collect(navModel.exploreNavigationItem, ::handleNavigation)
collectImmediately(selectionModel.selected, ::handleSelection)
collectImmediately(selectionModel.selected, ::updateSelection)
}
override fun onDestroyBinding(binding: FragmentSearchBinding) {
binding.searchToolbarOverlay.callback = null
binding.searchToolbar.setOnMenuItemClickListener(null)
binding.searchRecycler.adapter = null
}
override fun onMenuItemClick(item: MenuItem): Boolean {
if (item.itemId != R.id.submenu_filtering) {
searchModel.updateFilterModeWithId(item.itemId)
item.isChecked = true
}
return true
}
override fun onClearSelection() {
selectionModel.consume()
}
override fun onPlaySelectionNext() {
playbackModel.playNext(selectionModel.consume())
requireContext().showToast(R.string.lng_queue_added)
}
override fun onAddSelectionToQueue() {
playbackModel.addToQueue(selectionModel.consume())
requireContext().showToast(R.string.lng_queue_added)
}
override fun onItemClick(item: Item) {
check(item is Music) { "Unexpected datatype ${item::class.simpleName}"}
if (selectionModel.selected.value.isEmpty()) {
when (item) {
override fun onClick(music: Music) {
when (music) {
is Song ->
when (settings.libPlaybackMode) {
MusicMode.SONGS -> playbackModel.playFromAll(item)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(item)
MusicMode.ARTISTS -> playbackModel.playFromArtist(item)
MusicMode.SONGS -> playbackModel.playFromAll(music)
MusicMode.ALBUMS -> playbackModel.playFromAlbum(music)
MusicMode.ARTISTS -> playbackModel.playFromArtist(music)
else -> error("Unexpected playback mode: ${settings.libPlaybackMode}")
}
is MusicParent -> navModel.exploreNavigateTo(item)
}
} else {
selectionModel.select(item)
is MusicParent -> navModel.exploreNavigateTo(music)
}
}
override fun onSelect(item: Item) {
check(item is Music) { "Unexpected datatype ${item::class.simpleName}"}
selectionModel.select(item)
private fun handleSearchNavigateUp() {
// Reset selection (navigating to another selectable screen)
selectionModel.consume()
// Drop keyboard as it's no longer needed
imm.hide()
findNavController().navigateUp()
}
override fun onOpenMenu(item: Item, anchor: View) {
private fun handleSearchMenuItem(item: MenuItem) {
// Ignore junk sub-menu click events
if (item.itemId != R.id.submenu_filtering) {
searchModel.updateFilterModeWithId(item.itemId)
}
}
private fun handleOpenMenu(item: Item, anchor: View) {
when (item) {
is Song -> musicMenu(anchor, R.menu.menu_song_actions, item)
is Album -> musicMenu(anchor, R.menu.menu_album_actions, item)
is Artist -> musicMenu(anchor, R.menu.menu_artist_actions, item)
is Genre -> musicMenu(anchor, R.menu.menu_artist_actions, item)
is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item)
is Album -> openMusicMenu(anchor, R.menu.menu_album_actions, item)
is Artist -> openMusicMenu(anchor, R.menu.menu_artist_actions, item)
is Genre -> openMusicMenu(anchor, R.menu.menu_artist_actions, item)
else -> logW("Unexpected datatype when opening menu: ${item::class.java}")
}
}
private fun handleResults(results: List<Item>) {
private fun updateResults(results: List<Item>) {
val binding = requireBinding()
searchAdapter.submitList(results.toMutableList()) {
@ -206,20 +175,21 @@ class SearchFragment :
binding.searchRecycler.isInvisible = results.isEmpty()
}
private fun handlePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
searchAdapter.updateIndicator(parent ?: song, isPlaying)
}
private fun handleNavigation(item: Music?) {
findNavController()
.navigate(
val action =
when (item) {
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.uid)
is Album -> SearchFragmentDirections.actionShowAlbum(item.uid)
is Artist -> SearchFragmentDirections.actionShowArtist(item.uid)
is Genre -> SearchFragmentDirections.actionShowGenre(item.uid)
else -> return
})
}
findNavController().navigate(action)
// Reset selection (navigating to another selectable screen)
selectionModel.consume()
@ -228,13 +198,21 @@ class SearchFragment :
imm.hide()
}
private fun handleSelection(selected: List<Music>) {
searchAdapter.updateSelection(selected)
if (requireBinding().searchToolbarOverlay.updateSelectionAmount(selected.size) && selected.isNotEmpty()) {
private fun updateSelection(selected: List<Music>) {
searchAdapter.setSelected(selected)
if (requireBinding().searchToolbarOverlay.updateSelectionAmount(selected.size) &&
selected.isNotEmpty()) {
imm.hide()
}
}
private fun InputMethodManager.show(view: View) {
view.apply {
requestFocus()
postDelayed(200) { showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) }
}
}
private fun InputMethodManager.hide() {
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
}

View file

@ -30,6 +30,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.yield
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre
@ -39,8 +41,6 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.recycler.Header
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.application
import org.oxycblt.auxio.util.logD

View file

@ -34,7 +34,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentAboutBinding
import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.shared.ViewBindingFragment
import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast

View file

@ -35,7 +35,7 @@ import org.oxycblt.auxio.music.storage.MusicDirs
import org.oxycblt.auxio.playback.ActionMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
import org.oxycblt.auxio.ui.accent.Accent
import org.oxycblt.auxio.settings.accent.Accent
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull

View file

@ -23,7 +23,7 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.transition.MaterialFadeThrough
import org.oxycblt.auxio.databinding.FragmentSettingsBinding
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
import org.oxycblt.auxio.shared.ViewBindingFragment
/**
* A container [Fragment] for the settings menu.

View file

@ -15,10 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.accent
package org.oxycblt.auxio.settings.accent
import android.os.Build
import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.util.logW
private val ACCENT_NAMES =
@ -114,7 +115,7 @@ private val ACCENT_PRIMARY_COLORS =
* @property primary The primary color resource for this accent
* @author OxygenCobalt
*/
class Accent private constructor(val index: Int) {
class Accent private constructor(val index: Int) : Item {
val name: Int
get() = ACCENT_NAMES[index]
val theme: Int

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.accent
package org.oxycblt.auxio.settings.accent
import android.view.View
import android.view.ViewGroup
@ -23,6 +23,7 @@ import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemAccentBinding
import org.oxycblt.auxio.list.ItemClickCallback
import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.inflater
@ -31,7 +32,8 @@ import org.oxycblt.auxio.util.inflater
* An adapter that displays the accent palette.
* @author OxygenCobalt
*/
class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<AccentViewHolder>() {
class AccentAdapter(private val callback: ItemClickCallback) :
RecyclerView.Adapter<AccentViewHolder>() {
var selectedAccent: Accent? = null
private set
@ -50,7 +52,7 @@ class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<Accen
val item = Accent.from(position)
if (payloads.isEmpty()) {
holder.bind(item, listener)
holder.bind(item, callback)
}
holder.setSelected(item == selectedAccent)
@ -63,10 +65,6 @@ class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<Accen
notifyItemChanged(accent.index, PAYLOAD_SELECTION_CHANGED)
}
interface Listener {
fun onAccentSelected(accent: Accent)
}
companion object {
val PAYLOAD_SELECTION_CHANGED = Any()
}
@ -75,14 +73,14 @@ class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<Accen
class AccentViewHolder private constructor(private val binding: ItemAccentBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(item: Accent, listener: AccentAdapter.Listener) {
fun bind(item: Accent, callback: ItemClickCallback) {
setSelected(false)
binding.accent.apply {
backgroundTintList = context.getColorCompat(item.primary)
contentDescription = context.getString(item.name)
TooltipCompat.setTooltipText(this, contentDescription)
setOnClickListener { listener.onAccentSelected(item) }
setOnClickListener { callback.onClick(item) }
}
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.accent
package org.oxycblt.auxio.settings.accent
import android.os.Bundle
import android.view.LayoutInflater
@ -23,8 +23,10 @@ import androidx.appcompat.app.AlertDialog
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogAccentBinding
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.ItemClickCallback
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
@ -33,9 +35,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* Dialog responsible for showing the list of accents to select.
* @author OxygenCobalt
*/
class AccentCustomizeDialog :
ViewBindingDialogFragment<DialogAccentBinding>(), AccentAdapter.Listener {
private var accentAdapter = AccentAdapter(this)
class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>() {
private var accentAdapter = AccentAdapter(ItemClickCallback(::handleClick))
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater)
@ -74,8 +75,9 @@ class AccentCustomizeDialog :
binding.accentRecycler.adapter = null
}
override fun onAccentSelected(accent: Accent) {
accentAdapter.setSelectedAccent(accent)
private fun handleClick(item: Item) {
check(item is Accent) { "Unexpected datatype: ${item::class.java}" }
accentAdapter.setSelectedAccent(item)
}
companion object {

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.accent
package org.oxycblt.auxio.settings.accent
import android.content.Context
import android.util.AttributeSet

View file

@ -35,7 +35,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.SettingsFragmentDirections
import org.oxycblt.auxio.ui.NavigationViewModel
import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui
package org.oxycblt.auxio.shared
import android.content.Context
import android.util.AttributeSet

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui
package org.oxycblt.auxio.shared
import android.content.Context
import android.graphics.drawable.Drawable
@ -34,7 +34,7 @@ import org.oxycblt.auxio.util.systemGestureInsetsCompat
* the vendored code because of course I have to) for normal use without absurd bugs.
* @author OxygenCobalt
*/
abstract class AuxioSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
abstract class AuxioBottomSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
NeoBottomSheetBehavior<V>(context, attributeSet) {
private var setup = false

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui
package org.oxycblt.auxio.shared
import android.content.Context
import android.util.AttributeSet

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.system
package org.oxycblt.auxio.shared
import android.app.Service
import androidx.core.app.ServiceCompat

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui
package org.oxycblt.auxio.shared
import androidx.lifecycle.ViewModel
import androidx.navigation.NavDirections
@ -108,7 +108,7 @@ class NavigationViewModel : ViewModel() {
/**
* Represents the navigation options for the Main Fragment, which tends to be multiple layers above
* normal fragments. This can be passed to [NavigationViewModel.mainNavigateTo] in order to
* facilitate navigation without stupid fragment hacks.
* facilitate navigation without workarounds..
*/
sealed class MainNavigationAction {
/** Expand the playback panel. */

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.system
package org.oxycblt.auxio.shared
import android.content.Context
import androidx.annotation.StringRes

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.fragment
package org.oxycblt.auxio.shared
import android.os.Bundle
import android.view.LayoutInflater

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.ui.fragment
package org.oxycblt.auxio.shared
import android.os.Bundle
import android.view.LayoutInflater

View file

@ -12,7 +12,7 @@
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"
app:layout_behavior="org.oxycblt.auxio.shared.BottomSheetContentBehavior"
app:navGraph="@navigation/nav_explore"
tools:layout="@layout/fragment_home" />
@ -20,7 +20,7 @@
android:id="@+id/playback_sheet"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="org.oxycblt.auxio.playback.PlaybackSheetBehavior">
app:layout_behavior="org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/playback_bar_fragment"

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.oxycblt.auxio.ui.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
<org.oxycblt.auxio.list.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/accent_recycler"
@ -7,6 +7,6 @@
android:layout_height="wrap_content"
android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium"
app:layoutManager="org.oxycblt.auxio.ui.accent.AccentGridLayoutManager"
app:layoutManager="org.oxycblt.auxio.settings.accent.AccentGridLayoutManager"
tools:itemCount="16"
tools:listitem="@layout/item_accent" />

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.oxycblt.auxio.ui.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
<org.oxycblt.auxio.list.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/picker_recycler"

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.oxycblt.auxio.ui.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
<org.oxycblt.auxio.list.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_recycler"
style="@style/Widget.Auxio.RecyclerView.Linear"

View file

@ -9,7 +9,7 @@
android:transitionGroup="true"
tools:context=".settings.AboutFragment">
<org.oxycblt.auxio.ui.AuxioAppBarLayout
<org.oxycblt.auxio.shared.AuxioAppBarLayout
android:id="@+id/about_appbar"
style="@style/Widget.Auxio.AppBarLayout"
app:liftOnScroll="true">
@ -21,7 +21,7 @@
app:navigationIcon="@drawable/ic_back_24"
app:title="@string/lbl_about" />
</org.oxycblt.auxio.ui.AuxioAppBarLayout>
</org.oxycblt.auxio.shared.AuxioAppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/about_contents"

View file

@ -21,7 +21,7 @@
</org.oxycblt.auxio.detail.DetailAppBarLayout>
<org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
<org.oxycblt.auxio.list.recycler.AuxioRecyclerView
android:id="@+id/detail_recycler"
style="@style/Widget.Auxio.RecyclerView.Grid"
android:layout_width="match_parent"

View file

@ -8,12 +8,12 @@
android:background="?attr/colorSurface"
android:transitionGroup="true">
<org.oxycblt.auxio.ui.AuxioAppBarLayout
<org.oxycblt.auxio.shared.AuxioAppBarLayout
android:id="@+id/home_appbar"
style="@style/Widget.Auxio.AppBarLayout"
android:fitsSystemWindows="true">
<org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
<org.oxycblt.auxio.list.SelectionToolbarOverlay
android:id="@+id/home_toolbar_overlay"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -26,7 +26,7 @@
app:menu="@menu/menu_home"
app:title="@string/info_app_name" />
</org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay>
</org.oxycblt.auxio.list.SelectionToolbarOverlay>
<com.google.android.material.tabs.TabLayout
android:id="@+id/home_tabs"
@ -37,7 +37,7 @@
app:tabGravity="start"
app:tabMode="scrollable" />
</org.oxycblt.auxio.ui.AuxioAppBarLayout>
</org.oxycblt.auxio.shared.AuxioAppBarLayout>
<FrameLayout

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.oxycblt.auxio.ui.fastscroll.FastScrollRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
<org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/home_recycler"
style="@style/Widget.Auxio.RecyclerView.Grid.WithAdaptiveFab"

View file

@ -13,7 +13,7 @@
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"
app:layout_behavior="org.oxycblt.auxio.shared.BottomSheetContentBehavior"
app:navGraph="@navigation/nav_explore"
tools:layout="@layout/fragment_home" />
@ -22,7 +22,7 @@
style="@style/Widget.Auxio.DisableDropShadows"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="org.oxycblt.auxio.playback.PlaybackSheetBehavior">
app:layout_behavior="org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/playback_bar_fragment"
@ -35,7 +35,7 @@
android:name="org.oxycblt.auxio.playback.PlaybackPanelFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior" />
app:layout_behavior="org.oxycblt.auxio.shared.BottomSheetContentBehavior" />
<LinearLayout
android:id="@+id/queue_sheet"
@ -43,7 +43,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_behavior="org.oxycblt.auxio.playback.queue.QueueSheetBehavior">
app:layout_behavior="org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/handle_wrapper"

View file

@ -5,7 +5,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
<org.oxycblt.auxio.list.recycler.AuxioRecyclerView
android:id="@+id/queue_recycler"
style="@style/Widget.Auxio.RecyclerView.Linear"
android:layout_width="match_parent"

View file

@ -7,12 +7,12 @@
android:background="?attr/colorSurface"
android:transitionGroup="true">
<org.oxycblt.auxio.ui.AuxioAppBarLayout
<org.oxycblt.auxio.shared.AuxioAppBarLayout
style="@style/Widget.Auxio.AppBarLayout"
app:liftOnScroll="true"
app:liftOnScrollTargetViewId="@id/search_recycler">
<org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
<org.oxycblt.auxio.list.SelectionToolbarOverlay
android:id="@+id/search_toolbar_overlay"
android:layout_width="match_parent"
android:layout_height="wrap_content">
@ -49,11 +49,11 @@
</com.google.android.material.appbar.MaterialToolbar>
</org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay>
</org.oxycblt.auxio.list.SelectionToolbarOverlay>
</org.oxycblt.auxio.ui.AuxioAppBarLayout>
</org.oxycblt.auxio.shared.AuxioAppBarLayout>
<org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
<org.oxycblt.auxio.list.recycler.AuxioRecyclerView
android:id="@+id/search_recycler"
style="@style/Widget.Auxio.RecyclerView.Grid"
android:layout_width="match_parent"

View file

@ -8,7 +8,7 @@
android:orientation="vertical"
android:transitionGroup="true">
<org.oxycblt.auxio.ui.AuxioAppBarLayout
<org.oxycblt.auxio.shared.AuxioAppBarLayout
android:id="@+id/settings_appbar"
style="@style/Widget.Auxio.AppBarLayout"
android:clickable="true"
@ -22,7 +22,7 @@
app:navigationIcon="@drawable/ic_back_24"
app:title="@string/set_title" />
</org.oxycblt.auxio.ui.AuxioAppBarLayout>
</org.oxycblt.auxio.shared.AuxioAppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/settings_list_fragment"

View file

@ -78,7 +78,7 @@
</fragment>
<dialog
android:id="@+id/accent_dialog"
android:name="org.oxycblt.auxio.ui.accent.AccentCustomizeDialog"
android:name="org.oxycblt.auxio.settings.accent.AccentCustomizeDialog"
android:label="accent_dialog"
tools:layout="@layout/dialog_accent" />
<dialog