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

View file

@ -22,7 +22,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.core.view.children import androidx.core.view.children
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController 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.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.MenuFragment
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
@ -40,8 +41,6 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings 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.canScroll
import org.oxycblt.auxio.util.collect import org.oxycblt.auxio.util.collect
import org.oxycblt.auxio.util.collectImmediately 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]. * A fragment that shows information for a particular [Album].
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AlbumDetailFragment : class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
MenuFragment<FragmentDetailBinding>(),
Toolbar.OnMenuItemClickListener,
AlbumDetailAdapter.Listener {
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val args: AlbumDetailFragmentArgs by navArgs() private val args: AlbumDetailFragmentArgs by navArgs()
private val detailAdapter = AlbumDetailAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@ -80,14 +86,17 @@ class AlbumDetailFragment :
binding.detailToolbar.apply { binding.detailToolbar.apply {
inflateMenu(R.menu.menu_album_detail) inflateMenu(R.menu.menu_album_detail)
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@AlbumDetailFragment) setOnMenuItemClickListener {
handleDetailMenuItem(it)
true
}
} }
binding.detailRecycler.adapter = detailAdapter binding.detailRecycler.adapter = detailAdapter
// -- VIEWMODEL SETUP --- // -- VIEWMODEL SETUP ---
collectImmediately(detailModel.currentAlbum, ::handleItemChange) collectImmediately(detailModel.currentAlbum, ::updateItem)
collectImmediately(detailModel.albumData, detailAdapter::submitList) collectImmediately(detailModel.albumData, detailAdapter::submitList)
collectImmediately( collectImmediately(
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback) playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
@ -96,35 +105,10 @@ class AlbumDetailFragment :
override fun onDestroyBinding(binding: FragmentDetailBinding) { override fun onDestroyBinding(binding: FragmentDetailBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
binding.detailToolbar.apply {
setNavigationOnClickListener(null)
setOnMenuItemClickListener(null)
}
binding.detailRecycler.adapter = null binding.detailRecycler.adapter = null
} }
override fun onMenuItemClick(item: MenuItem): Boolean { private fun handleClick(item: Item) {
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) {
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" } check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
when (settings.detailPlaybackMode) { when (settings.detailPlaybackMode) {
null, 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}" } 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)) playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value))
} }
override fun onShuffleParent() { private fun handleShuffle() {
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value)) playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value))
} }
override fun onShowSortMenu(anchor: View) { private fun handleOpenSortMenu(anchor: View) {
menu(anchor, R.menu.menu_album_sort) { openMenu(anchor, R.menu.menu_album_sort) {
val sort = detailModel.albumSort val sort = detailModel.albumSort
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending 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) 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) { if (album == null) {
findNavController().navigateUp() findNavController().navigateUp()
return return

View file

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

View file

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

View file

@ -30,6 +30,8 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.yield import kotlinx.coroutines.yield
import org.oxycblt.auxio.R 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.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -40,8 +42,6 @@ import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.storage.MimeType import org.oxycblt.auxio.music.storage.MimeType
import org.oxycblt.auxio.settings.Settings 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.application
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW

View file

@ -21,7 +21,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
@ -30,6 +29,8 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentDetailBinding import org.oxycblt.auxio.databinding.FragmentDetailBinding
import org.oxycblt.auxio.detail.recycler.DetailAdapter import org.oxycblt.auxio.detail.recycler.DetailAdapter
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.MenuFragment
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -39,8 +40,6 @@ import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.settings.Settings 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.collect
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.context 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]. * A fragment that shows information for a particular [Genre].
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class GenreDetailFragment : class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
MenuFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
private val detailModel: DetailViewModel by activityViewModels() private val detailModel: DetailViewModel by activityViewModels()
private val args: GenreDetailFragmentArgs by navArgs() private val args: GenreDetailFragmentArgs by navArgs()
private val detailAdapter = GenreDetailAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@ -76,7 +82,10 @@ class GenreDetailFragment :
binding.detailToolbar.apply { binding.detailToolbar.apply {
inflateMenu(R.menu.menu_genre_artist_detail) inflateMenu(R.menu.menu_genre_artist_detail)
setNavigationOnClickListener { findNavController().navigateUp() } setNavigationOnClickListener { findNavController().navigateUp() }
setOnMenuItemClickListener(this@GenreDetailFragment) setOnMenuItemClickListener {
handleDetailMenuItem(it)
true
}
} }
binding.detailRecycler.adapter = detailAdapter binding.detailRecycler.adapter = detailAdapter
@ -92,31 +101,23 @@ class GenreDetailFragment :
override fun onDestroyBinding(binding: FragmentDetailBinding) { override fun onDestroyBinding(binding: FragmentDetailBinding) {
super.onDestroyBinding(binding) super.onDestroyBinding(binding)
binding.detailToolbar.apply {
setNavigationOnClickListener(null)
setOnMenuItemClickListener(null)
}
binding.detailRecycler.adapter = null binding.detailRecycler.adapter = null
} }
override fun onMenuItemClick(item: MenuItem): Boolean { private fun handleDetailMenuItem(item: MenuItem) {
return when (item.itemId) { when (item.itemId) {
R.id.action_play_next -> { R.id.action_play_next -> {
playbackModel.playNext(unlikelyToBeNull(detailModel.currentGenre.value)) playbackModel.playNext(unlikelyToBeNull(detailModel.currentGenre.value))
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
R.id.action_queue_add -> { R.id.action_queue_add -> {
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentGenre.value)) playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentGenre.value))
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
true
} }
else -> false
} }
} }
override fun onItemClick(item: Item) { private fun handleClick(item: Item) {
when (item) { when (item) {
is Artist -> navModel.exploreNavigateTo(item) is Artist -> navModel.exploreNavigateTo(item)
is Song -> is Song ->
@ -133,24 +134,24 @@ class GenreDetailFragment :
} }
} }
override fun onOpenMenu(item: Item, anchor: View) { private fun handleOpenItemMenu(item: Item, anchor: View) {
when (item) { when (item) {
is Artist -> musicMenu(anchor, R.menu.menu_artist_actions, item) is Artist -> openMusicMenu(anchor, R.menu.menu_artist_actions, item)
is Song -> musicMenu(anchor, R.menu.menu_song_actions, item) is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item)
else -> error("Unexpected datatype: ${item::class.simpleName}") else -> error("Unexpected datatype: ${item::class.simpleName}")
} }
} }
override fun onPlayParent() { private fun handlePlay() {
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value)) playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value))
} }
override fun onShuffleParent() { private fun handleShuffle() {
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value)) playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value))
} }
override fun onShowSortMenu(anchor: View) { private fun handleOpenSortMenu(anchor: View) {
menu(anchor, R.menu.menu_genre_sort) { openMenu(anchor, R.menu.menu_genre_sort) {
val sort = detailModel.genreSort val sort = detailModel.genreSort
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending 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.R
import org.oxycblt.auxio.databinding.DialogSongDetailBinding import org.oxycblt.auxio.databinding.DialogSongDetailBinding
import org.oxycblt.auxio.playback.formatDurationMs 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.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately 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.ItemDetailBinding
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
import org.oxycblt.auxio.detail.DiscHeader import org.oxycblt.auxio.detail.DiscHeader
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.formatDurationMs 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.context
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater 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. * An adapter for displaying [Album] information and it's children.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AlbumDetailAdapter(private val listener: Listener) : class AlbumDetailAdapter(private val callback: AlbumDetailAdapter.Callback) :
DetailAdapter<AlbumDetailAdapter.Listener>(listener, DIFFER) { DetailAdapter(callback, DIFFER) {
override fun getItemViewType(position: Int) = override fun getItemViewType(position: Int) =
when (differ.currentList[position]) { when (differ.currentList[position]) {
@ -70,9 +69,9 @@ class AlbumDetailAdapter(private val listener: Listener) :
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
when (val item = differ.currentList[position]) { 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 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 { class Callback(
fun onNavigateToArtist() 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) : private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(item: Album, listener: AlbumDetailAdapter.Listener) { fun bind(item: Album, callback: AlbumDetailAdapter.Callback) {
binding.detailCover.bind(item) binding.detailCover.bind(item)
binding.detailType.text = binding.context.getString(item.releaseType.stringRes) 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 { binding.detailSubhead.apply {
text = item.resolveArtistContents(context) text = item.resolveArtistContents(context)
setOnClickListener { listener.onNavigateToArtist() } setOnClickListener { callback.onNavigateToArtist() }
} }
binding.detailInfo.apply { 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) text = context.getString(R.string.fmt_three, date, songCount, duration)
} }
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() } binding.detailPlayButton.setOnClickListener { callback.onPlay() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() } binding.detailShuffleButton.setOnClickListener { callback.onShuffle() }
} }
companion object { companion object {
@ -174,7 +181,7 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) : private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) :
PlayingIndicatorAdapter.ViewHolder(binding.root) { 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. // Hide the track number view if the song does not have a track.
if (item.track != null) { if (item.track != null) {
binding.songTrack.apply { binding.songTrack.apply {
@ -193,11 +200,11 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
binding.songName.text = item.resolveName(binding.context) binding.songName.text = item.resolveName(binding.context)
binding.songDuration.text = item.durationMs.formatDurationMs(false) binding.songDuration.text = item.durationMs.formatDurationMs(false)
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) } binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) }
binding.root.apply { binding.root.apply {
setOnClickListener { listener.onItemClick(item) } setOnClickListener { callback.onClick(item) }
setOnLongClickListener { setOnLongClickListener {
listener.onSelect(item) callback.onSelect(item)
true true
} }
} }

View file

@ -26,13 +26,13 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemDetailBinding import org.oxycblt.auxio.databinding.ItemDetailBinding
import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.list.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.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Song 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.context
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater 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. * one actually contains both album information and song information.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class ArtistDetailAdapter(private val listener: Listener) : class ArtistDetailAdapter(private val callback: Callback) : DetailAdapter(callback, DIFFER) {
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
override fun getItemViewType(position: Int) = override fun getItemViewType(position: Int) =
when (differ.currentList[position]) { when (differ.currentList[position]) {
@ -70,9 +69,9 @@ class ArtistDetailAdapter(private val listener: Listener) :
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
when (val item = differ.currentList[position]) { when (val item = differ.currentList[position]) {
is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener) is Artist -> (holder as ArtistDetailViewHolder).bind(item, callback)
is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener) is Album -> (holder as ArtistAlbumViewHolder).bind(item, callback)
is Song -> (holder as ArtistSongViewHolder).bind(item, listener) 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) : private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(item: Artist, listener: DetailAdapter.Listener) { fun bind(item: Artist, callback: DetailAdapter.Callback) {
binding.detailCover.bind(item) binding.detailCover.bind(item)
binding.detailType.text = binding.context.getString(R.string.lbl_artist) binding.detailType.text = binding.context.getString(R.string.lbl_artist)
binding.detailName.text = item.resolveName(binding.context) 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.detailShuffleButton.isVisible = false
} }
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() } binding.detailPlayButton.setOnClickListener { callback.onPlay() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() } binding.detailShuffleButton.setOnClickListener { callback.onShuffle() }
} }
companion object { companion object {
@ -155,13 +154,13 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
private class ArtistAlbumViewHolder private constructor(private val binding: ItemParentBinding) : private class ArtistAlbumViewHolder private constructor(private val binding: ItemParentBinding) :
PlayingIndicatorAdapter.ViewHolder(binding.root) { PlayingIndicatorAdapter.ViewHolder(binding.root) {
fun bind(item: Album, listener: MenuItemListener) { fun bind(item: Album, callback: ItemMenuCallback) {
binding.parentImage.bind(item) binding.parentImage.bind(item)
binding.parentName.text = item.resolveName(binding.context) binding.parentName.text = item.resolveName(binding.context)
binding.parentInfo.text = binding.parentInfo.text =
item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date) item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) } binding.parentMenu.setOnClickListener { callback.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) } binding.root.setOnClickListener { callback.onClick(item) }
} }
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) { 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) : private class ArtistSongViewHolder private constructor(private val binding: ItemSongBinding) :
PlayingIndicatorAdapter.ViewHolder(binding.root) { PlayingIndicatorAdapter.ViewHolder(binding.root) {
fun bind(item: Song, listener: MenuItemListener) { fun bind(item: Song, callback: ItemMenuCallback) {
binding.songAlbumCover.bind(item) binding.songAlbumCover.bind(item)
binding.songName.text = item.resolveName(binding.context) binding.songName.text = item.resolveName(binding.context)
binding.songInfo.text = item.album.resolveName(binding.context) binding.songInfo.text = item.album.resolveName(binding.context)
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) } binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) }
binding.root.setOnClickListener { listener.onItemClick(item) } binding.root.setOnClickListener { callback.onClick(item) }
} }
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) { 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.IntegerTable
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
import org.oxycblt.auxio.detail.SortHeader import org.oxycblt.auxio.detail.SortHeader
import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.ui.recycler.Header import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.ui.recycler.HeaderViewHolder import org.oxycblt.auxio.list.ItemSelectCallback
import org.oxycblt.auxio.ui.recycler.Item import org.oxycblt.auxio.list.recycler.AuxioRecyclerView
import org.oxycblt.auxio.ui.recycler.MenuItemListener import org.oxycblt.auxio.list.recycler.HeaderViewHolder
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback import org.oxycblt.auxio.list.recycler.SimpleItemCallback
import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.context
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
abstract class DetailAdapter<L : DetailAdapter.Listener>( abstract class DetailAdapter(
private val listener: L, private val callback: Callback,
diffCallback: DiffUtil.ItemCallback<Item> diffCallback: DiffUtil.ItemCallback<Item>
) : PlayingIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup { ) : PlayingIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
private var isPlaying = false
@Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size @Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size
override fun getItemViewType(position: Int) = override fun getItemViewType(position: Int) =
@ -71,7 +69,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
when (item) { when (item) {
is Header -> (holder as HeaderViewHolder).bind(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 { open class Callback(
fun onPlayParent() onClick: (Item) -> Unit,
fun onShuffleParent() onOpenItemMenu: (Item, View) -> Unit,
fun onShowSortMenu(anchor: View) onSelect: (Item) -> Unit,
} val onPlay: () -> Unit,
val onShuffle: () -> Unit,
val onOpenSortMenu: (View) -> Unit
) : ItemSelectCallback(onClick, onOpenItemMenu, onSelect)
} }
class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) : class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
RecyclerView.ViewHolder(binding.root) { 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.headerTitle.text = binding.context.getString(item.string)
binding.headerButton.apply { binding.headerButton.apply {
TooltipCompat.setTooltipText(this, contentDescription) 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.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemDetailBinding 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.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song 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.context
import org.oxycblt.auxio.util.getPlural import org.oxycblt.auxio.util.getPlural
import org.oxycblt.auxio.util.inflater 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. * An adapter for displaying genre information and it's children.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class GenreDetailAdapter(private val listener: Listener) : class GenreDetailAdapter(private val callback: Callback) : DetailAdapter(callback, DIFFER) {
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
override fun getItemViewType(position: Int) = override fun getItemViewType(position: Int) =
when (differ.currentList[position]) { when (differ.currentList[position]) {
is Genre -> GenreDetailViewHolder.VIEW_TYPE is Genre -> GenreDetailViewHolder.VIEW_TYPE
@ -66,9 +65,9 @@ class GenreDetailAdapter(private val listener: Listener) :
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
when (val item = differ.currentList[position]) { when (val item = differ.currentList[position]) {
is Genre -> (holder as GenreDetailViewHolder).bind(item, listener) is Genre -> (holder as GenreDetailViewHolder).bind(item, callback)
is Artist -> (holder as ArtistViewHolder).bind(item, listener) is Artist -> (holder as ArtistViewHolder).bind(item, callback)
is Song -> (holder as SongViewHolder).bind(item, listener) 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) : private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(item: Genre, listener: DetailAdapter.Listener) { fun bind(item: Genre, callback: DetailAdapter.Callback) {
binding.detailCover.bind(item) binding.detailCover.bind(item)
binding.detailType.text = binding.context.getString(R.string.lbl_genre) binding.detailType.text = binding.context.getString(R.string.lbl_genre)
binding.detailName.text = item.resolveName(binding.context) 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_artist_count, item.artists.size),
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size)) binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() } binding.detailPlayButton.setOnClickListener { callback.onPlay() }
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() } binding.detailShuffleButton.setOnClickListener { callback.onShuffle() }
} }
companion object { companion object {

View file

@ -23,7 +23,6 @@ import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.Toolbar
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.iterator import androidx.core.view.iterator
import androidx.core.view.updatePadding 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.ArtistListFragment
import org.oxycblt.auxio.home.list.GenreListFragment import org.oxycblt.auxio.home.list.GenreListFragment
import org.oxycblt.auxio.home.list.SongListFragment import org.oxycblt.auxio.home.list.SongListFragment
import org.oxycblt.auxio.list.SelectionFragment
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -55,12 +55,7 @@ import org.oxycblt.auxio.music.MusicViewModel
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.music.system.Indexer import org.oxycblt.auxio.music.system.Indexer
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.shared.MainNavigationAction
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.util.* import org.oxycblt.auxio.util.*
/** /**
@ -68,12 +63,9 @@ import org.oxycblt.auxio.util.*
* respective item. * respective item.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener, SelectionToolbarOverlay.Callback { class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
private val homeModel: HomeViewModel by androidActivityViewModels() private val homeModel: HomeViewModel by androidActivityViewModels()
private val musicModel: MusicViewModel by activityViewModels() 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. // lifecycleObject builds this in the creation step, so doing this is okay.
private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject { 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 onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
binding.homeAppbar.apply { binding.homeAppbar.addOnOffsetChangedListener { _, it -> handleAppBarAnimation(it) }
addOnOffsetChangedListener { _, offset -> setupOverlay(binding.homeToolbarOverlay)
val range = binding.homeAppbar.totalScrollRange binding.homeToolbar.setOnMenuItemClickListener {
handleHomeMenuItem(it)
binding.homeToolbarOverlay.alpha = true
1f - (abs(offset.toFloat()) / (range.toFloat() / 2))
binding.homeContent.updatePadding(
bottom = binding.homeAppbar.totalScrollRange + offset)
}
} }
binding.homeToolbarOverlay.callback = this setupTabs(binding)
binding.homeToolbar.setOnMenuItemClickListener(this@HomeFragment)
updateTabConfiguration()
// Load the track color in manually as it's unclear whether the track actually supports // Load the track color in manually as it's unclear whether the track actually supports
// using a ColorStateList in the resources // using a ColorStateList in the resources
@ -128,17 +112,11 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
binding.homePager.apply { binding.homePager.apply {
adapter = HomePagerAdapter() 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( registerOnPageChangeCallback(
object : ViewPager2.OnPageChangeCallback() { object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) = override fun onPageSelected(position: Int) {
homeModel.updateCurrentTab(position) homeModel.setCurrentTab(position)
}
}) })
TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel)) 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 // insets applied to the indexing view before API 30. Fix this by overriding the
// callback with a non-consuming listener. // callback with a non-consuming listener.
setOnApplyWindowInsetsListener { _, insets -> insets } 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() } binding.homeFab.setOnClickListener { playbackModel.shuffleAll() }
@ -157,9 +151,11 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
collect(homeModel.recreateTabs, ::handleRecreateTabs) collect(homeModel.recreateTabs, ::handleRecreateTabs)
collectImmediately(homeModel.currentTab, ::updateCurrentTab) collectImmediately(homeModel.currentTab, ::updateCurrentTab)
collectImmediately(homeModel.songs, homeModel.isFastScrolling, ::updateFab) collectImmediately(homeModel.songs, homeModel.isFastScrolling, ::updateFab)
collectImmediately(musicModel.indexerState, ::handleIndexerState)
collectImmediately(selectionModel.selected, ::updateSelection) collectImmediately(musicModel.indexerState, ::updateIndexerState)
collect(navModel.exploreNavigationItem, ::handleNavigation) collect(navModel.exploreNavigationItem, ::handleNavigation)
collectImmediately(selectionModel.selected, ::updateSelection)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -171,13 +167,18 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
override fun onDestroyBinding(binding: FragmentHomeBinding) { private fun handleAppBarAnimation(verticalOffset: Int) {
super.onDestroyBinding(binding) val binding = requireBinding()
binding.homeToolbarOverlay.callback = null val range = binding.homeAppbar.totalScrollRange
binding.homeToolbar.setOnMenuItemClickListener(null)
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) { when (item.itemId) {
R.id.action_search -> { R.id.action_search -> {
logD("Navigating to search") logD("Navigating to search")
@ -206,7 +207,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
.getSortForTab(homeModel.currentTab.value) .getSortForTab(homeModel.currentTab.value)
.withAscending(item.isChecked)) .withAscending(item.isChecked))
} }
else -> { else -> {
// Sorting option was selected, mark it as selected and update the mode // Sorting option was selected, mark it as selected and update the mode
item.isChecked = true item.isChecked = true
@ -216,23 +216,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId)))) .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) { 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) { private fun updateSortMenu(mode: MusicMode, isVisible: (Int) -> Boolean) {
@ -285,34 +268,24 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
private fun handleRecreateTabs(recreate: Boolean) { private fun handleRecreateTabs(recreate: Boolean) {
if (recreate) { if (recreate) {
requireBinding().homePager.recreate() val binding = requireBinding()
updateTabConfiguration()
binding.homePager.apply {
currentItem = 0
adapter = HomePagerAdapter()
}
setupTabs(binding)
homeModel.finishRecreateTabs() homeModel.finishRecreateTabs()
} }
} }
private fun updateTabConfiguration() { private fun updateIndexerState(state: Indexer.State?) {
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?) {
val binding = requireBinding() val binding = requireBinding()
when (state) { when (state) {
is Indexer.State.Complete -> handleIndexerResponse(binding, state.response) is Indexer.State.Complete -> setupCompleteState(binding, state.response)
is Indexer.State.Indexing -> handleIndexingState(binding, state.indexing) is Indexer.State.Indexing -> setupIndexingState(binding, state.indexing)
null -> { null -> {
logD("Indexer is in indeterminate state") logD("Indexer is in indeterminate state")
binding.homeIndexingContainer.visibility = View.INVISIBLE 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) { if (response is Indexer.Response.Ok) {
binding.homeFab.show() binding.homeFab.show()
binding.homeIndexingContainer.visibility = View.INVISIBLE 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.homeIndexingContainer.visibility = View.VISIBLE
binding.homeIndexingProgress.visibility = View.VISIBLE binding.homeIndexingProgress.visibility = View.VISIBLE
binding.homeIndexingAction.visibility = View.INVISIBLE 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?) { private fun handleNavigation(item: Music?) {
val action = val action =
when (item) { when (item) {
@ -426,6 +388,42 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
findNavController().navigate(action) 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) { private fun initAxisTransitions(axis: Int) {
// Sanity check // Sanity check
check(axis == MaterialSharedAxis.X || axis == MaterialSharedAxis.Z) { check(axis == MaterialSharedAxis.X || axis == MaterialSharedAxis.Z) {
@ -437,35 +435,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
reenterTransition = MaterialSharedAxis(axis, false) 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 : private inner class HomePagerAdapter :
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) { 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. */ /** 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]}") logD("Updating current tab to ${tabs[pos]}")
_currentTab.value = 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 * Update the fast scroll state. This is used to control the FAB visibility whenever the user
* begins to fast scroll. * begins to fast scroll.
*/ */
fun updateFastScrolling(scrolling: Boolean) { fun setFastScrolling(fastScrolling: Boolean) {
logD("Updating fast scrolling state: $scrolling") logD("Updating fast scrolling state: $fastScrolling")
_isFastScrolling.value = scrolling _isFastScrolling.value = fastScrolling
} }
// --- OVERRIDES --- // --- OVERRIDES ---

View file

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

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.ui.fastscroll package org.oxycblt.auxio.home.fastscroll
import android.content.Context import android.content.Context
import android.graphics.Canvas import android.graphics.Canvas
@ -35,7 +35,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlin.math.abs import kotlin.math.abs
import org.oxycblt.auxio.R 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.getDimenSize
import org.oxycblt.auxio.util.getDrawableCompat import org.oxycblt.auxio.util.getDrawableCompat
import org.oxycblt.auxio.util.isRtl import org.oxycblt.auxio.util.isRtl
@ -137,33 +137,26 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
removeCallbacks(hideThumbRunnable) removeCallbacks(hideThumbRunnable)
showScrollbar() showScrollbar()
showPopup() showPopup()
listener?.onFastScrollStart()
} else { } else {
postAutoHideScrollbar() postAutoHideScrollbar()
hidePopup() hidePopup()
listener?.onFastScrollStop()
} }
fastScrollCallback?.invoke(field)
} }
private val tRect = Rect() 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 */ /** 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 { class FastScrollCallback(val onStart: () -> Unit, val onEnd: () -> Unit)
fun onFastScrollStart()
fun onFastScrollStop()
}
/** /**
* 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. * false if a drag ended.
*/ */
var listener: OnFastScrollListener? = null var fastScrollCallback: ((Boolean) -> Unit)? = null
init { init {
overlay.add(thumbView) overlay.add(thumbView)
@ -225,7 +218,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
if (firstAdapterPos != NO_POSITION && provider != null) { if (firstAdapterPos != NO_POSITION && provider != null) {
popupView.isInvisible = false popupView.isInvisible = false
// Get the popup text. If there is none, we default to "?". // Get the popup text. If there is none, we default to "?".
popupText = provider.getPopup(firstAdapterPos) ?: "?" popupText = provider.invoke(firstAdapterPos) ?: "?"
} else { } else {
// No valid position or provider, do not show the popup. // No valid position or provider, do not show the popup.
popupView.isInvisible = true popupView.isInvisible = true

View file

@ -19,11 +19,19 @@ package org.oxycblt.auxio.home.list
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import java.util.Formatter import java.util.Formatter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.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.Album
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode 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.music.Sort
import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.playback.formatDurationMs
import org.oxycblt.auxio.playback.secsToMs 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 import org.oxycblt.auxio.util.collectImmediately
/** /**
* A [HomeListFragment] for showing a list of [Album]s. * A [HomeListFragment] for showing a list of [Album]s.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AlbumListFragment : HomeListFragment<Album>() { class AlbumListFragment : SelectionFragment<FragmentHomeListBinding>() {
private val homeAdapter = AlbumAdapter(this) private val homeModel: HomeViewModel by activityViewModels()
private val homeAdapter =
AlbumAdapter(ItemSelectCallback(::handleClick, ::handleOpenMenu, ::handleClick))
private val formatterSb = StringBuilder(32) private val formatterSb = StringBuilder(32)
private val formatter = Formatter(formatterSb) private val formatter = Formatter(formatterSb)
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply { binding.homeRecycler.apply {
id = R.id.home_album_recycler id = R.id.home_album_recycler
adapter = homeAdapter adapter = homeAdapter
popupProvider = ::updatePopup
fastScrollCallback = { homeModel.setFastScrolling(it) }
} }
collectImmediately(homeModel.albums, homeAdapter::replaceList) collectImmediately(homeModel.albums, homeAdapter::replaceList)
collectImmediately(selectionModel.selected, homeAdapter::updateSelection) collectImmediately(selectionModel.selected, homeAdapter::setSelected)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handleParent) 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] val album = homeModel.albums.value[pos]
// Change how we display the popup depending on the mode. // Change how we display the popup depending on the mode.
@ -98,18 +126,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
else -> null else -> null
} }
} }
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
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) {
if (parent is Album) { if (parent is Album) {
homeAdapter.updateIndicator(parent, isPlaying) homeAdapter.updateIndicator(parent, isPlaying)
} else { } 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>() { SelectionIndicatorAdapter<AlbumViewHolder>() {
private val differ = SyncListDiffer(this, AlbumViewHolder.DIFFER) private val differ = SyncListDiffer(this, AlbumViewHolder.DIFFER)
@ -134,7 +151,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
super.onBindViewHolder(holder, position, payloads) super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) { 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 package org.oxycblt.auxio.home.list
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.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.Artist
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.playback.formatDurationMs 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.collectImmediately
import org.oxycblt.auxio.util.nonZeroOrNull 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. * A [HomeListFragment] for showing a list of [Artist]s.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class ArtistListFragment : HomeListFragment<Artist>() { class ArtistListFragment : SelectionFragment<FragmentHomeListBinding>() {
private val homeAdapter = ArtistAdapter(this) 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?) { override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
@ -49,14 +58,22 @@ class ArtistListFragment : HomeListFragment<Artist>() {
binding.homeRecycler.apply { binding.homeRecycler.apply {
id = R.id.home_artist_recycler id = R.id.home_artist_recycler
adapter = homeAdapter adapter = homeAdapter
popupProvider = ::updatePopup
fastScrollCallback = homeModel::setFastScrolling
} }
collectImmediately(homeModel.artists, homeAdapter::replaceList) collectImmediately(homeModel.artists, homeAdapter::replaceList)
collectImmediately(selectionModel.selected, homeAdapter::updateSelection) collectImmediately(selectionModel.selected, homeAdapter::setSelected)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handleParent) 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] val artist = homeModel.artists.value[pos]
// Change how we display the popup depending on the mode. // 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}" } check(music is Artist) { "Unexpected datatype: ${music::class.java}" }
navModel.exploreNavigateTo(music) 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}" } 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) { if (parent is Artist) {
homeAdapter.updateIndicator(parent, isPlaying) homeAdapter.updateIndicator(parent, isPlaying)
} else { } 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>() { SelectionIndicatorAdapter<ArtistViewHolder>() {
private val differ = SyncListDiffer(this, ArtistViewHolder.DIFFER) private val differ = SyncListDiffer(this, ArtistViewHolder.DIFFER)
@ -114,7 +131,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
super.onBindViewHolder(holder, position, payloads) super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) { 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 package org.oxycblt.auxio.home.list
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.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.Genre
import org.oxycblt.auxio.music.Music import org.oxycblt.auxio.music.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent import org.oxycblt.auxio.music.MusicParent
import org.oxycblt.auxio.music.Sort import org.oxycblt.auxio.music.Sort
import org.oxycblt.auxio.playback.formatDurationMs 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 import org.oxycblt.auxio.util.collectImmediately
/** /**
* A [HomeListFragment] for showing a list of [Genre]s. * A [HomeListFragment] for showing a list of [Genre]s.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class GenreListFragment : HomeListFragment<Genre>() { class GenreListFragment : SelectionFragment<FragmentHomeListBinding>() {
private val homeAdapter = GenreAdapter(this) 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?) { override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
@ -48,14 +57,22 @@ class GenreListFragment : HomeListFragment<Genre>() {
binding.homeRecycler.apply { binding.homeRecycler.apply {
id = R.id.home_genre_recycler id = R.id.home_genre_recycler
adapter = homeAdapter adapter = homeAdapter
popupProvider = ::updatePopup
fastScrollCallback = homeModel::setFastScrolling
} }
collectImmediately(homeModel.genres, homeAdapter::replaceList) collectImmediately(homeModel.genres, homeAdapter::replaceList)
collectImmediately(selectionModel.selected, homeAdapter::updateSelection) collectImmediately(selectionModel.selected, homeAdapter::setSelected)
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handlePlayback) 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] val genre = homeModel.genres.value[pos]
// Change how we display the popup depending on the mode. // 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}" } check(music is Genre) { "Unexpected datatype: ${music::class.java}" }
navModel.exploreNavigateTo(music) 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}" } 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) { if (parent is Genre) {
homeAdapter.updateIndicator(parent, isPlaying) homeAdapter.updateIndicator(parent, isPlaying)
} else { } 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>() { SelectionIndicatorAdapter<GenreViewHolder>() {
private val differ = SyncListDiffer(this, GenreViewHolder.DIFFER) private val differ = SyncListDiffer(this, GenreViewHolder.DIFFER)
@ -109,7 +126,7 @@ class GenreListFragment : HomeListFragment<Genre>() {
super.onBindViewHolder(holder, position, payloads) super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) { 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.os.Bundle
import android.text.format.DateUtils import android.text.format.DateUtils
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import java.util.Formatter import java.util.Formatter
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentHomeListBinding import org.oxycblt.auxio.databinding.FragmentHomeListBinding
import org.oxycblt.auxio.home.HomeViewModel
import org.oxycblt.auxio.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.Music
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.music.MusicParent 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.formatDurationMs
import org.oxycblt.auxio.playback.secsToMs import org.oxycblt.auxio.playback.secsToMs
import org.oxycblt.auxio.settings.Settings 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.collectImmediately
import org.oxycblt.auxio.util.context 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. * A [HomeListFragment] for showing a list of [Song]s.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class SongListFragment : HomeListFragment<Song>() { class SongListFragment : SelectionFragment<FragmentHomeListBinding>() {
private val homeAdapter = SongAdapter(this) 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 settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val formatterSb = StringBuilder(50) private val formatterSb = StringBuilder(50)
private val formatter = Formatter(formatterSb) private val formatter = Formatter(formatterSb)
override fun onCreateBinding(inflater: LayoutInflater) =
FragmentHomeListBinding.inflate(inflater)
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) { override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
super.onBindingCreated(binding, savedInstanceState) super.onBindingCreated(binding, savedInstanceState)
binding.homeRecycler.apply { binding.homeRecycler.apply {
id = R.id.home_song_recycler id = R.id.home_song_recycler
adapter = homeAdapter adapter = homeAdapter
popupProvider = ::updatePopup
fastScrollCallback = homeModel::setFastScrolling
} }
collectImmediately(homeModel.songs, homeAdapter::replaceList) collectImmediately(homeModel.songs, homeAdapter::replaceList)
collectImmediately(selectionModel.selected, homeAdapter::updateSelection) collectImmediately(selectionModel.selected, homeAdapter::setSelected)
collectImmediately( 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] val song = homeModel.songs.value[pos]
// Change how we display the popup depending on the mode. // 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}" } check(music is Song) { "Unexpected datatype: ${music::class.java}" }
when (settings.libPlaybackMode) { when (settings.libPlaybackMode) {
MusicMode.SONGS -> playbackModel.playFromAll(music) MusicMode.SONGS -> playbackModel.playFromAll(music)
@ -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}" } 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) { if (parent == null) {
homeAdapter.updateIndicator(song, isPlaying) homeAdapter.updateIndicator(song, isPlaying)
} else { } 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>() { SelectionIndicatorAdapter<SongViewHolder>() {
private val differ = SyncListDiffer(this, SongViewHolder.DIFFER) private val differ = SyncListDiffer(this, SongViewHolder.DIFFER)
@ -146,7 +165,7 @@ class SongListFragment : HomeListFragment<Song>() {
super.onBindViewHolder(holder, position, payloads) super.onBindViewHolder(holder, position, payloads)
if (payloads.isEmpty()) { 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 androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemTabBinding import org.oxycblt.auxio.databinding.ItemTabBinding
import org.oxycblt.auxio.list.recycler.DialogViewHolder
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.ui.recycler.DialogViewHolder
import org.oxycblt.auxio.util.inflater 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>() var tabs = arrayOf<Tab>()
private set 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 onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.new(parent)
override fun onBindViewHolder(holder: TabViewHolder, position: Int) { override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
holder.bind(tabs[position], listener) holder.bind(tabs[position], callback)
} }
@Suppress("NotifyDatasetChanged") @Suppress("NotifyDatasetChanged")
@ -59,10 +59,10 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
notifyItemMoved(from, to) notifyItemMoved(from, to)
} }
interface Listener { class Callback(
fun onVisibilityToggled(mode: MusicMode) val toggleVisibility: (MusicMode) -> Unit,
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) val pickUpTab: (RecyclerView.ViewHolder) -> Unit
} )
companion object { companion object {
val PAYLOAD_TAB_CHANGED = Any() 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) : class TabViewHolder private constructor(private val binding: ItemTabBinding) :
DialogViewHolder(binding.root) { DialogViewHolder(binding.root) {
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
fun bind(item: Tab, listener: TabAdapter.Listener) { fun bind(item: Tab, callback: TabAdapter.Callback) {
binding.root.setOnClickListener { listener.onVisibilityToggled(item.mode) } binding.root.setOnClickListener { callback.toggleVisibility(item.mode) }
binding.tabIcon.apply { binding.tabIcon.apply {
setText( setText(
@ -90,7 +90,7 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
binding.tabDragHandle.setOnTouchListener { _, motionEvent -> binding.tabDragHandle.setOnTouchListener { _, motionEvent ->
binding.tabDragHandle.performClick() binding.tabDragHandle.performClick()
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) { if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
listener.onPickUpTab(this) callback.pickUpTab(this)
true true
} else false } else false
} }

View file

@ -27,7 +27,7 @@ import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogTabsBinding import org.oxycblt.auxio.databinding.DialogTabsBinding
import org.oxycblt.auxio.music.MusicMode import org.oxycblt.auxio.music.MusicMode
import org.oxycblt.auxio.settings.Settings 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.context
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -35,8 +35,8 @@ import org.oxycblt.auxio.util.logD
* The dialog for customizing library tabs. * The dialog for customizing library tabs.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAdapter.Listener { class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>() {
private val tabAdapter = TabAdapter(this) private val tabAdapter = TabAdapter(TabAdapter.Callback(::toggleVisibility, ::pickUpTab))
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
private val touchHelper: ItemTouchHelper by lifecycleObject { private val touchHelper: ItemTouchHelper by lifecycleObject {
ItemTouchHelper(TabDragCallback(tabAdapter)) ItemTouchHelper(TabDragCallback(tabAdapter))
@ -79,7 +79,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
binding.tabRecycler.adapter = null binding.tabRecycler.adapter = null
} }
override fun onVisibilityToggled(mode: MusicMode) { private fun toggleVisibility(mode: MusicMode) {
val index = tabAdapter.tabs.indexOfFirst { it.mode == mode } val index = tabAdapter.tabs.indexOfFirst { it.mode == mode }
if (index > -1) { if (index > -1) {
val tab = tabAdapter.tabs[index] val tab = tabAdapter.tabs[index]
@ -95,7 +95,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
tabAdapter.tabs.filterIsInstance<Tab.Visible>().isNotEmpty() tabAdapter.tabs.filterIsInstance<Tab.Visible>().isNotEmpty()
} }
override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) { private fun pickUpTab(viewHolder: RecyclerView.ViewHolder) {
touchHelper.startDrag(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/>. * 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 android.view.View
import androidx.annotation.MenuRes import androidx.annotation.MenuRes
import androidx.appcompat.widget.PopupMenu 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.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.shared.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast import org.oxycblt.auxio.util.showToast
@ -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 * Opens the given menu in context of [song]. Assumes that the menu is only composed of common
* [Song] options. * [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}") logD("Launching new song menu: ${song.rawName}")
musicMenuImpl(anchor, menuRes) { id -> openMusicMenuImpl(anchor, menuRes) {
when (id) { when (it.itemId) {
R.id.action_play_next -> { R.id.action_play_next -> {
playbackModel.playNext(song) playbackModel.playNext(song)
requireContext().showToast(R.string.lng_queue_added) requireContext().showToast(R.string.lng_queue_added)
@ -78,8 +80,6 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
error("Unexpected menu item selected") 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 * Opens the given menu in context of [album]. Assumes that the menu is only composed of common
* [Album] options. * [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}") logD("Launching new album menu: ${album.rawName}")
musicMenuImpl(anchor, menuRes) { id -> openMusicMenuImpl(anchor, menuRes) {
when (id) { when (it.itemId) {
R.id.action_play -> { R.id.action_play -> {
playbackModel.play(album) playbackModel.play(album)
} }
@ -113,8 +113,6 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
error("Unexpected menu item selected") 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 * Opens the given menu in context of [artist]. Assumes that the menu is only composed of common
* [Artist] options. * [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}") logD("Launching new artist menu: ${artist.rawName}")
musicMenuImpl(anchor, menuRes) { id -> openMusicMenuImpl(anchor, menuRes) {
when (id) { when (it.itemId) {
R.id.action_play -> { R.id.action_play -> {
playbackModel.play(artist) playbackModel.play(artist)
} }
@ -145,8 +143,6 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
error("Unexpected menu item selected") 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 * Opens the given menu in context of [genre]. Assumes that the menu is only composed of common
* [Genre] options. * [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}") logD("Launching new genre menu: ${genre.rawName}")
musicMenuImpl(anchor, menuRes) { id -> openMusicMenuImpl(anchor, menuRes) {
when (id) { when (it.itemId) {
R.id.action_play -> { R.id.action_play -> {
playbackModel.play(genre) playbackModel.play(genre)
} }
@ -177,20 +173,27 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
error("Unexpected menu item selected") error("Unexpected menu item selected")
} }
} }
true
} }
} }
private fun musicMenuImpl(anchor: View, @MenuRes menuRes: Int, onSelect: (Int) -> Boolean) { private fun openMusicMenuImpl(
menu(anchor, menuRes) { setOnMenuItemClickListener { item -> onSelect(item.itemId) } } anchor: View,
@MenuRes menuRes: Int,
onClick: (MenuItem) -> Unit
) {
openMenu(anchor, menuRes) {
setOnMenuItemClickListener { item ->
onClick(item)
true
}
}
} }
/** /**
* Open a generic menu with configuration in [block]. If a menu is already opened, then this * Open a generic menu with configuration in [block]. If a menu is already opened, then this
* function is a no-op. * 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) { if (currentMenu != null) {
logD("Menu already present, not launching") logD("Menu already present, not launching")
return 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/>. * 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.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.MenuItem
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.annotation.AttrRes import androidx.annotation.AttrRes
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -38,29 +37,12 @@ class SelectionToolbarOverlay
@JvmOverloads @JvmOverloads
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) : constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
FrameLayout(context, attrs, defStyleAttr) { FrameLayout(context, attrs, defStyleAttr) {
var callback: Callback? = null
private lateinit var innerToolbar: MaterialToolbar private lateinit var innerToolbar: MaterialToolbar
private val selectionToolbar = private val selectionToolbar =
MaterialToolbar(context).apply { MaterialToolbar(context).apply {
setNavigationIcon(R.drawable.ic_close_24) setNavigationIcon(R.drawable.ic_close_24)
setNavigationOnClickListener {
callback?.onClearSelection()
}
inflateMenu(R.menu.menu_selection_actions) 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 private var fadeThroughAnimator: ValueAnimator? = null
@ -75,9 +57,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
addView(selectionToolbar) addView(selectionToolbar)
} }
override fun onDetachedFromWindow() { fun setOnSelectionCancelListener(listener: OnClickListener) {
super.onDetachedFromWindow() selectionToolbar.setNavigationOnClickListener(listener)
callback = null }
fun setOnMenuItemClickListener(listener: OnMenuItemClickListener) {
selectionToolbar.setOnMenuItemClickListener(listener)
} }
/** /**
@ -151,10 +136,4 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
isInvisible = innerAlpha == 1f 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/>. * 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 androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
@ -33,15 +33,15 @@ class SelectionViewModel : ViewModel() {
get() = _selected get() = _selected
/** Select a music item. */ /** Select a music item. */
fun select(item: Music) { fun select(music: Music) {
val items = _selected.value.toMutableList() val selected = _selected.value.toMutableList()
if (items.remove(item)) { if (selected.remove(music)) {
logD("Unselecting item $item") logD("Unselecting item $music")
_selected.value = items _selected.value = selected
} else { } else {
logD("Selecting item $item") logD("Selecting item $music")
items.add(item) selected.add(music)
_selected.value = items _selected.value = selected
} }
} }

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.ui.recycler package org.oxycblt.auxio.list.recycler
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView 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 oldSelectedItems = selectedItems
val newSelectedItems = items.toSet() val newSelectedItems = items.toSet()
if (newSelectedItems == oldSelectedItems) { 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/>. * 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.AdapterListUpdateCallback
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView 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 * 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 * 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 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/>. * 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 android.view.View
import androidx.recyclerview.widget.RecyclerView 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.ItemHeaderBinding
import org.oxycblt.auxio.databinding.ItemParentBinding import org.oxycblt.auxio.databinding.ItemParentBinding
import org.oxycblt.auxio.databinding.ItemSongBinding import org.oxycblt.auxio.databinding.ItemSongBinding
import org.oxycblt.auxio.list.Header
import org.oxycblt.auxio.list.ItemSelectCallback
import org.oxycblt.auxio.list.MenuItemListener
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
@ -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) { override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive binding.root.isSelected = isActive
binding.songAlbumCover.isPlaying = isPlaying 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) { override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive binding.root.isSelected = isActive
binding.parentImage.isPlaying = isPlaying 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) { override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive binding.root.isSelected = isActive
binding.parentImage.isPlaying = isPlaying 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) { override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
binding.root.isSelected = isActive binding.root.isSelected = isActive
binding.parentImage.isPlaying = isPlaying binding.parentImage.isPlaying = isPlaying

View file

@ -29,6 +29,7 @@ import kotlin.math.max
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.music.extractor.parseId3GenreNames import org.oxycblt.auxio.music.extractor.parseId3GenreNames
import org.oxycblt.auxio.music.extractor.parseMultiValue import org.oxycblt.auxio.music.extractor.parseMultiValue
import org.oxycblt.auxio.music.extractor.toUuidOrNull 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.albumCoverUri
import org.oxycblt.auxio.music.storage.audioUri import org.oxycblt.auxio.music.storage.audioUri
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.recycler.Item
import org.oxycblt.auxio.util.nonZeroOrNull import org.oxycblt.auxio.util.nonZeroOrNull
import org.oxycblt.auxio.util.unlikelyToBeNull 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 // 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. // song. Do what we can to hopefully find the song the user wanted to open.
val displayName = val displayName =
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE)) val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
songs.find { it.path.name == displayName && it.size == 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.R
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
import org.oxycblt.auxio.settings.Settings 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.context
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() { class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {

View file

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

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.ItemMusicDirBinding 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.context
import org.oxycblt.auxio.util.inflater 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.R
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
import org.oxycblt.auxio.settings.Settings 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.context
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -59,7 +59,8 @@ class MusicDirsDialog :
.setPositiveButton(R.string.lbl_save) { _, _ -> .setPositiveButton(R.string.lbl_save) { _, _ ->
val dirs = settings.getMusicDirs(storageManager) val dirs = settings.getMusicDirs(storageManager)
val newDirs = val newDirs =
MusicDirs(dirs = dirAdapter.dirs, shouldInclude = isInclude(requireBinding())) MusicDirs(
dirs = dirAdapter.dirs, shouldInclude = isUiModeInclude(requireBinding()))
if (dirs != newDirs) { if (dirs != newDirs) {
logD("Committing changes") logD("Committing changes")
settings.setMusicDirs(newDirs) settings.setMusicDirs(newDirs)
@ -122,7 +123,7 @@ class MusicDirsDialog :
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putStringArrayList( outState.putStringArrayList(
KEY_PENDING_DIRS, ArrayList(dirAdapter.dirs.map { it.toString() })) 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) { override fun onDestroyBinding(binding: DialogMusicDirsBinding) {
@ -166,14 +167,14 @@ class MusicDirsDialog :
private fun updateMode() { private fun updateMode() {
val binding = requireBinding() val binding = requireBinding()
if (isInclude(binding)) { if (isUiModeInclude(binding)) {
binding.dirsModeDesc.setText(R.string.set_dirs_mode_include_desc) binding.dirsModeDesc.setText(R.string.set_dirs_mode_include_desc)
} else { } else {
binding.dirsModeDesc.setText(R.string.set_dirs_mode_exclude_desc) 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 binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include
companion object { companion object {

View file

@ -23,7 +23,7 @@ import androidx.core.app.NotificationCompat
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R 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.logD
import org.oxycblt.auxio.util.newMainPendingIntent 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.music.MusicStore
import org.oxycblt.auxio.playback.state.PlaybackStateManager import org.oxycblt.auxio.playback.state.PlaybackStateManager
import org.oxycblt.auxio.settings.Settings 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.contentResolverSafe
import org.oxycblt.auxio.util.getSystemServiceCompat import org.oxycblt.auxio.util.getSystemServiceCompat
import org.oxycblt.auxio.util.logD 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.music.Song
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.ui.MainNavigationAction import org.oxycblt.auxio.shared.MainNavigationAction
import org.oxycblt.auxio.ui.NavigationViewModel import org.oxycblt.auxio.shared.NavigationViewModel
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment import org.oxycblt.auxio.shared.ViewBindingFragment
import org.oxycblt.auxio.util.androidActivityViewModels import org.oxycblt.auxio.util.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getAttrColorCompat
@ -63,16 +63,29 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
binding.playbackSong.isSelected = true binding.playbackSong.isSelected = true
binding.playbackInfo.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 // Load the track color in manually as it's unclear whether the track actually supports
// using a ColorStateList in the resources // using a ColorStateList in the resources
binding.playbackProgressBar.trackColor = binding.playbackProgressBar.trackColor =
context.getColorCompat(R.color.sel_track).defaultColor 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 -> { ActionMode.NEXT -> {
binding.playbackSecondaryAction.apply { binding.playbackSecondaryAction.apply {
setIconResource(R.drawable.ic_skip_next_24) 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?) { 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 requireBinding().playbackPlayPause.isActivated = isPlaying
} }

View file

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

View file

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

View file

@ -27,10 +27,10 @@ import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemQueueSongBinding 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.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.context
import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimen

View file

@ -24,7 +24,7 @@ import android.view.WindowInsets
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
import org.oxycblt.auxio.R 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.getAttrColorCompat
import org.oxycblt.auxio.util.getDimen import org.oxycblt.auxio.util.getDimen
import org.oxycblt.auxio.util.getDimenSize 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. * The bottom sheet behavior designed for the queue in particular.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) : class QueueBottomSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
AuxioSheetBehavior<V>(context, attributeSet) { AuxioBottomSheetBehavior<V>(context, attributeSet) {
private var barHeight = 0 private var barHeight = 0
private var barSpacing = context.getDimenSize(R.dimen.spacing_small) 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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.core.view.isInvisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
@ -28,7 +27,7 @@ import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.databinding.FragmentQueueBinding import org.oxycblt.auxio.databinding.FragmentQueueBinding
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel 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.androidActivityViewModels
import org.oxycblt.auxio.util.collectImmediately import org.oxycblt.auxio.util.collectImmediately
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
@ -52,16 +51,6 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
binding.queueRecycler.apply { binding.queueRecycler.apply {
adapter = queueAdapter adapter = queueAdapter
touchHelper.attachToRecyclerView(this) 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 ---- // --- VIEWMODEL SETUP ----
@ -95,10 +84,6 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
queueAdapter.submitList(queue) queueAdapter.submitList(queue)
} }
binding.queueDivider.isInvisible =
(binding.queueRecycler.layoutManager as LinearLayoutManager)
.findFirstCompletelyVisibleItemPosition() < 1
queueModel.finishReplace() queueModel.finishReplace()
val scrollTo = queueModel.scrollTo val scrollTo = queueModel.scrollTo
@ -117,11 +102,4 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
queueAdapter.updateIndicator(index, isPlaying) 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.R
import org.oxycblt.auxio.databinding.DialogPreAmpBinding import org.oxycblt.auxio.databinding.DialogPreAmpBinding
import org.oxycblt.auxio.settings.Settings 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.context
/** /**

View file

@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.IntegerTable import org.oxycblt.auxio.IntegerTable
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.playback.state.RepeatMode 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.newBroadcastPendingIntent
import org.oxycblt.auxio.util.newMainPendingIntent 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.PlaybackStateManager
import org.oxycblt.auxio.playback.state.RepeatMode import org.oxycblt.auxio.playback.state.RepeatMode
import org.oxycblt.auxio.settings.Settings 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.util.logD
import org.oxycblt.auxio.widgets.WidgetComponent import org.oxycblt.auxio.widgets.WidgetComponent
import org.oxycblt.auxio.widgets.WidgetProvider 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) 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 * 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") logD("Confirming seek")
// End of seek event, send off new value to callback. // End of seek event, send off new value to callback.
isActivated = false isActivated = false
callback?.seekTo(slider.value.toLong()) onSeekConfirmed?.invoke(slider.value.toLong())
} }
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
binding.seekBarPosition.text = value.toLong().formatDurationDs(true) 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 android.view.ViewGroup
import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.RecyclerView 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.Album
import org.oxycblt.auxio.music.Artist import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song 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 { SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
private val differ = AsyncListDiffer(this, DIFFER) private val differ = AsyncListDiffer(this, DIFFER)
@ -61,10 +62,10 @@ class SearchAdapter(private val listener: MenuItemListener) :
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
when (val item = differ.currentList[position]) { when (val item = differ.currentList[position]) {
is Song -> (holder as SongViewHolder).bind(item, listener) is Song -> (holder as SongViewHolder).bind(item, callback)
is Album -> (holder as AlbumViewHolder).bind(item, listener) is Album -> (holder as AlbumViewHolder).bind(item, callback)
is Artist -> (holder as ArtistViewHolder).bind(item, listener) is Artist -> (holder as ArtistViewHolder).bind(item, callback)
is Genre -> (holder as GenreViewHolder).bind(item, listener) is Genre -> (holder as GenreViewHolder).bind(item, callback)
is Header -> (holder as HeaderViewHolder).bind(item) is Header -> (holder as HeaderViewHolder).bind(item)
} }
} }

View file

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

View file

@ -23,7 +23,7 @@ import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.transition.MaterialFadeThrough import com.google.android.material.transition.MaterialFadeThrough
import org.oxycblt.auxio.databinding.FragmentSettingsBinding 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. * 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/>. * 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 android.os.Build
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.list.Item
import org.oxycblt.auxio.util.logW import org.oxycblt.auxio.util.logW
private val ACCENT_NAMES = private val ACCENT_NAMES =
@ -114,7 +115,7 @@ private val ACCENT_PRIMARY_COLORS =
* @property primary The primary color resource for this accent * @property primary The primary color resource for this accent
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class Accent private constructor(val index: Int) { class Accent private constructor(val index: Int) : Item {
val name: Int val name: Int
get() = ACCENT_NAMES[index] get() = ACCENT_NAMES[index]
val theme: Int val theme: Int

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.ui.accent package org.oxycblt.auxio.settings.accent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -23,6 +23,7 @@ import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.ItemAccentBinding import org.oxycblt.auxio.databinding.ItemAccentBinding
import org.oxycblt.auxio.list.ItemClickCallback
import org.oxycblt.auxio.util.getAttrColorCompat import org.oxycblt.auxio.util.getAttrColorCompat
import org.oxycblt.auxio.util.getColorCompat import org.oxycblt.auxio.util.getColorCompat
import org.oxycblt.auxio.util.inflater import org.oxycblt.auxio.util.inflater
@ -31,7 +32,8 @@ import org.oxycblt.auxio.util.inflater
* An adapter that displays the accent palette. * An adapter that displays the accent palette.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<AccentViewHolder>() { class AccentAdapter(private val callback: ItemClickCallback) :
RecyclerView.Adapter<AccentViewHolder>() {
var selectedAccent: Accent? = null var selectedAccent: Accent? = null
private set private set
@ -50,7 +52,7 @@ class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<Accen
val item = Accent.from(position) val item = Accent.from(position)
if (payloads.isEmpty()) { if (payloads.isEmpty()) {
holder.bind(item, listener) holder.bind(item, callback)
} }
holder.setSelected(item == selectedAccent) holder.setSelected(item == selectedAccent)
@ -63,10 +65,6 @@ class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<Accen
notifyItemChanged(accent.index, PAYLOAD_SELECTION_CHANGED) notifyItemChanged(accent.index, PAYLOAD_SELECTION_CHANGED)
} }
interface Listener {
fun onAccentSelected(accent: Accent)
}
companion object { companion object {
val PAYLOAD_SELECTION_CHANGED = Any() 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) : class AccentViewHolder private constructor(private val binding: ItemAccentBinding) :
RecyclerView.ViewHolder(binding.root) { RecyclerView.ViewHolder(binding.root) {
fun bind(item: Accent, listener: AccentAdapter.Listener) { fun bind(item: Accent, callback: ItemClickCallback) {
setSelected(false) setSelected(false)
binding.accent.apply { binding.accent.apply {
backgroundTintList = context.getColorCompat(item.primary) backgroundTintList = context.getColorCompat(item.primary)
contentDescription = context.getString(item.name) contentDescription = context.getString(item.name)
TooltipCompat.setTooltipText(this, contentDescription) 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/>. * 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.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -23,8 +23,10 @@ import androidx.appcompat.app.AlertDialog
import org.oxycblt.auxio.BuildConfig import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogAccentBinding 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.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.context
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull 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. * Dialog responsible for showing the list of accents to select.
* @author OxygenCobalt * @author OxygenCobalt
*/ */
class AccentCustomizeDialog : class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>() {
ViewBindingDialogFragment<DialogAccentBinding>(), AccentAdapter.Listener { private var accentAdapter = AccentAdapter(ItemClickCallback(::handleClick))
private var accentAdapter = AccentAdapter(this)
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) } private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater) override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater)
@ -74,8 +75,9 @@ class AccentCustomizeDialog :
binding.accentRecycler.adapter = null binding.accentRecycler.adapter = null
} }
override fun onAccentSelected(accent: Accent) { private fun handleClick(item: Item) {
accentAdapter.setSelectedAccent(accent) check(item is Accent) { "Unexpected datatype: ${item::class.java}" }
accentAdapter.setSelectedAccent(item)
} }
companion object { companion object {

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.ui.accent package org.oxycblt.auxio.settings.accent
import android.content.Context import android.content.Context
import android.util.AttributeSet 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.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.Settings import org.oxycblt.auxio.settings.Settings
import org.oxycblt.auxio.settings.SettingsFragmentDirections 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.androidActivityViewModels
import org.oxycblt.auxio.util.isNight import org.oxycblt.auxio.util.isNight
import org.oxycblt.auxio.util.logD import org.oxycblt.auxio.util.logD

View file

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

View file

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

View file

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

View file

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

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.oxycblt.auxio.ui package org.oxycblt.auxio.shared
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.navigation.NavDirections 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 * 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 * 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 { sealed class MainNavigationAction {
/** Expand the playback panel. */ /** Expand the playback panel. */

View file

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

View file

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

View file

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

View file

@ -12,7 +12,7 @@
android:name="androidx.navigation.fragment.NavHostFragment" android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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" app:navGraph="@navigation/nav_explore"
tools:layout="@layout/fragment_home" /> tools:layout="@layout/fragment_home" />
@ -20,7 +20,7 @@
android:id="@+id/playback_sheet" android:id="@+id/playback_sheet"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <androidx.fragment.app.FragmentContainerView
android:id="@+id/playback_bar_fragment" android:id="@+id/playback_bar_fragment"

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/accent_recycler" android:id="@+id/accent_recycler"
@ -7,6 +7,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/spacing_medium" android:paddingStart="@dimen/spacing_medium"
android:paddingEnd="@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:itemCount="16"
tools:listitem="@layout/item_accent" /> tools:listitem="@layout/item_accent" />

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/picker_recycler" android:id="@+id/picker_recycler"

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/tab_recycler" android:id="@+id/tab_recycler"
style="@style/Widget.Auxio.RecyclerView.Linear" style="@style/Widget.Auxio.RecyclerView.Linear"

View file

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

View file

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

View file

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

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?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" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/home_recycler" android:id="@+id/home_recycler"
style="@style/Widget.Auxio.RecyclerView.Grid.WithAdaptiveFab" style="@style/Widget.Auxio.RecyclerView.Grid.WithAdaptiveFab"

View file

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

View file

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

View file

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

View file

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

View file

@ -78,7 +78,7 @@
</fragment> </fragment>
<dialog <dialog
android:id="@+id/accent_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" android:label="accent_dialog"
tools:layout="@layout/dialog_accent" /> tools:layout="@layout/dialog_accent" />
<dialog <dialog