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:
parent
8aeb6d092e
commit
32d01f2027
87 changed files with 989 additions and 911 deletions
|
@ -26,8 +26,6 @@ import androidx.core.view.ViewCompat
|
|||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavDestination
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.bottomsheet.NeoBottomSheetBehavior
|
||||
|
@ -39,13 +37,12 @@ import org.oxycblt.auxio.databinding.FragmentMainBinding
|
|||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackSheetBehavior
|
||||
import org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.playback.queue.QueueSheetBehavior
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||
import org.oxycblt.auxio.ui.selection.SelectionViewModel
|
||||
import org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior
|
||||
import org.oxycblt.auxio.shared.MainNavigationAction
|
||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
||||
/**
|
||||
|
@ -83,17 +80,17 @@ class MainFragment :
|
|||
insets
|
||||
}
|
||||
|
||||
|
||||
// Send meaningful accessibility events for bottom sheets
|
||||
ViewCompat.setAccessibilityPaneTitle(
|
||||
binding.playbackSheet, context.getString(R.string.lbl_playback))
|
||||
ViewCompat.setAccessibilityPaneTitle(
|
||||
binding.queueSheet, context.getString(R.string.lbl_queue))
|
||||
|
||||
val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
|
||||
val queueSheetBehavior =
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
if (queueSheetBehavior != null) {
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
|
||||
unlikelyToBeNull(binding.handleWrapper).setOnClickListener {
|
||||
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED &&
|
||||
|
@ -146,7 +143,7 @@ class MainFragment :
|
|||
val binding = requireBinding()
|
||||
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
|
||||
val playbackRatio = max(playbackSheetBehavior.calculateSlideOffset(), 0f)
|
||||
|
||||
|
@ -154,7 +151,8 @@ class MainFragment :
|
|||
val halfOutRatio = min(playbackRatio * 2, 1f)
|
||||
val halfInPlaybackRatio = max(playbackRatio - 0.5f, 0f) * 2
|
||||
|
||||
val queueSheetBehavior = binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
|
||||
val queueSheetBehavior =
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
|
||||
if (queueSheetBehavior != null) {
|
||||
// Queue sheet, take queue into account so the playback bar is shown and the playback
|
||||
|
@ -262,7 +260,7 @@ class MainFragment :
|
|||
private fun tryExpandAll() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
|
||||
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_COLLAPSED) {
|
||||
// State is collapsed and non-hidden, expand
|
||||
|
@ -273,12 +271,12 @@ class MainFragment :
|
|||
private fun tryCollapseAll() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
|
||||
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_EXPANDED) {
|
||||
// Make sure the queue is also collapsed here.
|
||||
val queueSheetBehavior =
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
|
||||
playbackSheetBehavior.state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
||||
queueSheetBehavior?.state = NeoBottomSheetBehavior.STATE_COLLAPSED
|
||||
|
@ -288,11 +286,11 @@ class MainFragment :
|
|||
private fun tryUnhideAll() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
|
||||
if (playbackSheetBehavior.state == NeoBottomSheetBehavior.STATE_HIDDEN) {
|
||||
val queueSheetBehavior =
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
|
||||
// Queue sheet behavior is either collapsed or expanded, no hiding needed
|
||||
queueSheetBehavior?.isDraggable = true
|
||||
|
@ -308,11 +306,11 @@ class MainFragment :
|
|||
private fun tryHideAll() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
|
||||
if (playbackSheetBehavior.state != NeoBottomSheetBehavior.STATE_HIDDEN) {
|
||||
val queueSheetBehavior =
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
|
||||
// Make these views non-draggable so the user can't halt the hiding event.
|
||||
|
||||
|
@ -336,9 +334,9 @@ class MainFragment :
|
|||
override fun handleOnBackPressed() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
val queueSheetBehavior =
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
|
||||
if (queueSheetBehavior != null &&
|
||||
queueSheetBehavior.state != NeoBottomSheetBehavior.STATE_COLLAPSED &&
|
||||
|
@ -361,9 +359,9 @@ class MainFragment :
|
|||
fun updateEnabledState() {
|
||||
val binding = requireBinding()
|
||||
val playbackSheetBehavior =
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackSheetBehavior
|
||||
binding.playbackSheet.coordinatorLayoutBehavior as PlaybackBottomSheetBehavior
|
||||
val queueSheetBehavior =
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueSheetBehavior?
|
||||
binding.queueSheet.coordinatorLayoutBehavior as QueueBottomSheetBehavior?
|
||||
|
||||
val exploreNavController = binding.exploreNavHost.findNavController()
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.children
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
|
@ -32,6 +31,8 @@ import com.google.android.material.transition.MaterialSharedAxis
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.detail.recycler.AlbumDetailAdapter
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.MenuFragment
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
|
@ -40,8 +41,6 @@ import org.oxycblt.auxio.music.MusicParent
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.canScroll
|
||||
import org.oxycblt.auxio.util.collect
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
@ -54,16 +53,23 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* A fragment that shows information for a particular [Album].
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumDetailFragment :
|
||||
MenuFragment<FragmentDetailBinding>(),
|
||||
Toolbar.OnMenuItemClickListener,
|
||||
AlbumDetailAdapter.Listener {
|
||||
class AlbumDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
private val args: AlbumDetailFragmentArgs by navArgs()
|
||||
private val detailAdapter = AlbumDetailAdapter(this)
|
||||
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
|
||||
|
||||
private val detailAdapter =
|
||||
AlbumDetailAdapter(
|
||||
AlbumDetailAdapter.Callback(
|
||||
::handleClick,
|
||||
::handleOpenItemMenu,
|
||||
{},
|
||||
::handlePlay,
|
||||
::handleShuffle,
|
||||
::handleOpenSortMenu,
|
||||
::handleArtistNavigation))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
|
@ -80,14 +86,17 @@ class AlbumDetailFragment :
|
|||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_album_detail)
|
||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
setOnMenuItemClickListener(this@AlbumDetailFragment)
|
||||
setOnMenuItemClickListener {
|
||||
handleDetailMenuItem(it)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
binding.detailRecycler.adapter = detailAdapter
|
||||
|
||||
// -- VIEWMODEL SETUP ---
|
||||
|
||||
collectImmediately(detailModel.currentAlbum, ::handleItemChange)
|
||||
collectImmediately(detailModel.currentAlbum, ::updateItem)
|
||||
collectImmediately(detailModel.albumData, detailAdapter::submitList)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
|
@ -96,35 +105,10 @@ class AlbumDetailFragment :
|
|||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.detailToolbar.apply {
|
||||
setNavigationOnClickListener(null)
|
||||
setOnMenuItemClickListener(null)
|
||||
}
|
||||
|
||||
binding.detailRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_play_next -> {
|
||||
playbackModel.playNext(unlikelyToBeNull(detailModel.currentAlbum.value))
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
true
|
||||
}
|
||||
R.id.action_queue_add -> {
|
||||
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentAlbum.value))
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
true
|
||||
}
|
||||
R.id.action_go_artist -> {
|
||||
onNavigateToArtist()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
private fun handleClick(item: Item) {
|
||||
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
when (settings.detailPlaybackMode) {
|
||||
null,
|
||||
|
@ -135,21 +119,21 @@ class AlbumDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
private fun handleOpenItemMenu(item: Item, anchor: View) {
|
||||
check(item is Song) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
musicMenu(anchor, R.menu.menu_album_song_actions, item)
|
||||
openMusicMenu(anchor, R.menu.menu_album_song_actions, item)
|
||||
}
|
||||
|
||||
override fun onPlayParent() {
|
||||
private fun handlePlay() {
|
||||
playbackModel.play(unlikelyToBeNull(detailModel.currentAlbum.value))
|
||||
}
|
||||
|
||||
override fun onShuffleParent() {
|
||||
private fun handleShuffle() {
|
||||
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentAlbum.value))
|
||||
}
|
||||
|
||||
override fun onShowSortMenu(anchor: View) {
|
||||
menu(anchor, R.menu.menu_album_sort) {
|
||||
private fun handleOpenSortMenu(anchor: View) {
|
||||
openMenu(anchor, R.menu.menu_album_sort) {
|
||||
val sort = detailModel.albumSort
|
||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||
|
@ -166,11 +150,27 @@ class AlbumDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onNavigateToArtist() {
|
||||
private fun handleArtistNavigation() {
|
||||
navModel.exploreNavigateTo(unlikelyToBeNull(detailModel.currentAlbum.value).artists)
|
||||
}
|
||||
|
||||
private fun handleItemChange(album: Album?) {
|
||||
private fun handleDetailMenuItem(item: MenuItem) {
|
||||
when (item.itemId) {
|
||||
R.id.action_play_next -> {
|
||||
playbackModel.playNext(unlikelyToBeNull(detailModel.currentAlbum.value))
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
R.id.action_queue_add -> {
|
||||
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentAlbum.value))
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
R.id.action_go_artist -> {
|
||||
handleArtistNavigation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateItem(album: Album?) {
|
||||
if (album == null) {
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
|
|
|
@ -21,7 +21,6 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
|
@ -30,6 +29,8 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.detail.recycler.ArtistDetailAdapter
|
||||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.MenuFragment
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
|
@ -38,8 +39,6 @@ import org.oxycblt.auxio.music.MusicParent
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.collect
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.context
|
||||
|
@ -51,14 +50,22 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* A fragment that shows information for a particular [Artist].
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistDetailFragment :
|
||||
MenuFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
|
||||
class ArtistDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
private val args: ArtistDetailFragmentArgs by navArgs()
|
||||
private val detailAdapter = ArtistDetailAdapter(this)
|
||||
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
|
||||
|
||||
private val detailAdapter =
|
||||
ArtistDetailAdapter(
|
||||
DetailAdapter.Callback(
|
||||
::handleClick,
|
||||
::handleOpenItemMenu,
|
||||
{},
|
||||
::handlePlay,
|
||||
::handleShuffle,
|
||||
::handleOpenSortMenu))
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
|
@ -75,14 +82,17 @@ class ArtistDetailFragment :
|
|||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
||||
setOnMenuItemClickListener {
|
||||
handleDetailMenuItem(it)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
binding.detailRecycler.adapter = detailAdapter
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
collectImmediately(detailModel.currentArtist, ::handleItemChange)
|
||||
collectImmediately(detailModel.currentArtist, ::updateItem)
|
||||
collectImmediately(detailModel.artistData, detailAdapter::submitList)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
|
@ -91,31 +101,10 @@ class ArtistDetailFragment :
|
|||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.detailToolbar.apply {
|
||||
setNavigationOnClickListener(null)
|
||||
setOnMenuItemClickListener(null)
|
||||
}
|
||||
|
||||
binding.detailRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_play_next -> {
|
||||
playbackModel.playNext(unlikelyToBeNull(detailModel.currentArtist.value))
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
true
|
||||
}
|
||||
R.id.action_queue_add -> {
|
||||
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentArtist.value))
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
private fun handleClick(item: Item) {
|
||||
when (item) {
|
||||
is Song -> {
|
||||
when (settings.detailPlaybackMode) {
|
||||
|
@ -133,24 +122,24 @@ class ArtistDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
private fun handleOpenItemMenu(item: Item, anchor: View) {
|
||||
when (item) {
|
||||
is Song -> musicMenu(anchor, R.menu.menu_artist_song_actions, item)
|
||||
is Album -> musicMenu(anchor, R.menu.menu_artist_album_actions, item)
|
||||
is Song -> openMusicMenu(anchor, R.menu.menu_artist_song_actions, item)
|
||||
is Album -> openMusicMenu(anchor, R.menu.menu_artist_album_actions, item)
|
||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayParent() {
|
||||
private fun handlePlay() {
|
||||
playbackModel.play(unlikelyToBeNull(detailModel.currentArtist.value))
|
||||
}
|
||||
|
||||
override fun onShuffleParent() {
|
||||
private fun handleShuffle() {
|
||||
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentArtist.value))
|
||||
}
|
||||
|
||||
override fun onShowSortMenu(anchor: View) {
|
||||
menu(anchor, R.menu.menu_artist_sort) {
|
||||
private fun handleOpenSortMenu(anchor: View) {
|
||||
openMenu(anchor, R.menu.menu_artist_sort) {
|
||||
val sort = detailModel.artistSort
|
||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||
|
@ -169,7 +158,20 @@ class ArtistDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleItemChange(artist: Artist?) {
|
||||
private fun handleDetailMenuItem(item: MenuItem) {
|
||||
when (item.itemId) {
|
||||
R.id.action_play_next -> {
|
||||
playbackModel.playNext(unlikelyToBeNull(detailModel.currentArtist.value))
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
R.id.action_queue_add -> {
|
||||
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentArtist.value))
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateItem(artist: Artist?) {
|
||||
if (artist == null) {
|
||||
findNavController().navigateUp()
|
||||
return
|
||||
|
|
|
@ -31,7 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.google.android.material.appbar.AppBarLayout
|
||||
import java.lang.reflect.Field
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.ui.AuxioAppBarLayout
|
||||
import org.oxycblt.auxio.shared.AuxioAppBarLayout
|
||||
import org.oxycblt.auxio.util.lazyReflectedField
|
||||
|
||||
/**
|
||||
|
|
|
@ -30,6 +30,8 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.list.Header
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -40,8 +42,6 @@ import org.oxycblt.auxio.music.Song
|
|||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.music.storage.MimeType
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.recycler.Header
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.application
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
|
|
@ -21,7 +21,6 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
|
@ -30,6 +29,8 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentDetailBinding
|
||||
import org.oxycblt.auxio.detail.recycler.DetailAdapter
|
||||
import org.oxycblt.auxio.detail.recycler.GenreDetailAdapter
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.MenuFragment
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -39,8 +40,6 @@ import org.oxycblt.auxio.music.MusicParent
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.collect
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.context
|
||||
|
@ -52,14 +51,21 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* A fragment that shows information for a particular [Genre].
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class GenreDetailFragment :
|
||||
MenuFragment<FragmentDetailBinding>(), Toolbar.OnMenuItemClickListener, DetailAdapter.Listener {
|
||||
class GenreDetailFragment : MenuFragment<FragmentDetailBinding>() {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
private val args: GenreDetailFragmentArgs by navArgs()
|
||||
private val detailAdapter = GenreDetailAdapter(this)
|
||||
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
|
||||
|
||||
private val detailAdapter =
|
||||
GenreDetailAdapter(
|
||||
DetailAdapter.Callback(
|
||||
::handleClick,
|
||||
::handleOpenItemMenu,
|
||||
{},
|
||||
::handlePlay,
|
||||
::handleShuffle,
|
||||
::handleOpenSortMenu))
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
||||
|
@ -76,7 +82,10 @@ class GenreDetailFragment :
|
|||
binding.detailToolbar.apply {
|
||||
inflateMenu(R.menu.menu_genre_artist_detail)
|
||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
setOnMenuItemClickListener(this@GenreDetailFragment)
|
||||
setOnMenuItemClickListener {
|
||||
handleDetailMenuItem(it)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
binding.detailRecycler.adapter = detailAdapter
|
||||
|
@ -92,31 +101,23 @@ class GenreDetailFragment :
|
|||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.detailToolbar.apply {
|
||||
setNavigationOnClickListener(null)
|
||||
setOnMenuItemClickListener(null)
|
||||
}
|
||||
|
||||
binding.detailRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
private fun handleDetailMenuItem(item: MenuItem) {
|
||||
when (item.itemId) {
|
||||
R.id.action_play_next -> {
|
||||
playbackModel.playNext(unlikelyToBeNull(detailModel.currentGenre.value))
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
true
|
||||
}
|
||||
R.id.action_queue_add -> {
|
||||
playbackModel.addToQueue(unlikelyToBeNull(detailModel.currentGenre.value))
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
private fun handleClick(item: Item) {
|
||||
when (item) {
|
||||
is Artist -> navModel.exploreNavigateTo(item)
|
||||
is Song ->
|
||||
|
@ -133,24 +134,24 @@ class GenreDetailFragment :
|
|||
}
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
private fun handleOpenItemMenu(item: Item, anchor: View) {
|
||||
when (item) {
|
||||
is Artist -> musicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
is Song -> musicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
is Artist -> openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
else -> error("Unexpected datatype: ${item::class.simpleName}")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPlayParent() {
|
||||
private fun handlePlay() {
|
||||
playbackModel.play(unlikelyToBeNull(detailModel.currentGenre.value))
|
||||
}
|
||||
|
||||
override fun onShuffleParent() {
|
||||
private fun handleShuffle() {
|
||||
playbackModel.shuffle(unlikelyToBeNull(detailModel.currentGenre.value))
|
||||
}
|
||||
|
||||
override fun onShowSortMenu(anchor: View) {
|
||||
menu(anchor, R.menu.menu_genre_sort) {
|
||||
private fun handleOpenSortMenu(anchor: View) {
|
||||
openMenu(anchor, R.menu.menu_genre_sort) {
|
||||
val sort = detailModel.genreSort
|
||||
unlikelyToBeNull(menu.findItem(sort.mode.itemId)).isChecked = true
|
||||
unlikelyToBeNull(menu.findItem(R.id.option_sort_asc)).isChecked = sort.isAscending
|
||||
|
|
|
@ -27,7 +27,7 @@ import androidx.navigation.fragment.navArgs
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
||||
|
|
|
@ -27,13 +27,12 @@ import org.oxycblt.auxio.databinding.ItemAlbumSongBinding
|
|||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||
import org.oxycblt.auxio.databinding.ItemDiscHeaderBinding
|
||||
import org.oxycblt.auxio.detail.DiscHeader
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
@ -42,8 +41,8 @@ import org.oxycblt.auxio.util.inflater
|
|||
* An adapter for displaying [Album] information and it's children.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumDetailAdapter(private val listener: Listener) :
|
||||
DetailAdapter<AlbumDetailAdapter.Listener>(listener, DIFFER) {
|
||||
class AlbumDetailAdapter(private val callback: AlbumDetailAdapter.Callback) :
|
||||
DetailAdapter(callback, DIFFER) {
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
|
@ -70,9 +69,9 @@ class AlbumDetailAdapter(private val listener: Listener) :
|
|||
|
||||
if (payloads.isEmpty()) {
|
||||
when (val item = differ.currentList[position]) {
|
||||
is Album -> (holder as AlbumDetailViewHolder).bind(item, listener)
|
||||
is Album -> (holder as AlbumDetailViewHolder).bind(item, callback)
|
||||
is DiscHeader -> (holder as DiscHeaderViewHolder).bind(item)
|
||||
is Song -> (holder as AlbumSongViewHolder).bind(item, listener)
|
||||
is Song -> (holder as AlbumSongViewHolder).bind(item, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -99,15 +98,23 @@ class AlbumDetailAdapter(private val listener: Listener) :
|
|||
}
|
||||
}
|
||||
|
||||
interface Listener : DetailAdapter.Listener {
|
||||
fun onNavigateToArtist()
|
||||
}
|
||||
class Callback(
|
||||
onClick: (Item) -> Unit,
|
||||
onOpenItemMenu: (Item, View) -> Unit,
|
||||
onSelect: (Item) -> Unit,
|
||||
onPlay: () -> Unit,
|
||||
onShuffle: () -> Unit,
|
||||
onOpenSortMenu: (View) -> Unit,
|
||||
val onNavigateToArtist: () -> Unit
|
||||
) :
|
||||
DetailAdapter.Callback(
|
||||
onClick, onOpenItemMenu, onSelect, onPlay, onShuffle, onOpenSortMenu)
|
||||
}
|
||||
|
||||
private class AlbumDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: Album, listener: AlbumDetailAdapter.Listener) {
|
||||
fun bind(item: Album, callback: AlbumDetailAdapter.Callback) {
|
||||
binding.detailCover.bind(item)
|
||||
binding.detailType.text = binding.context.getString(item.releaseType.stringRes)
|
||||
|
||||
|
@ -115,7 +122,7 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
|
||||
binding.detailSubhead.apply {
|
||||
text = item.resolveArtistContents(context)
|
||||
setOnClickListener { listener.onNavigateToArtist() }
|
||||
setOnClickListener { callback.onNavigateToArtist() }
|
||||
}
|
||||
|
||||
binding.detailInfo.apply {
|
||||
|
@ -128,8 +135,8 @@ private class AlbumDetailViewHolder private constructor(private val binding: Ite
|
|||
text = context.getString(R.string.fmt_three, date, songCount, duration)
|
||||
}
|
||||
|
||||
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||
binding.detailPlayButton.setOnClickListener { callback.onPlay() }
|
||||
binding.detailShuffleButton.setOnClickListener { callback.onShuffle() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -174,7 +181,7 @@ class DiscHeaderViewHolder(private val binding: ItemDiscHeaderBinding) :
|
|||
|
||||
private class AlbumSongViewHolder private constructor(private val binding: ItemAlbumSongBinding) :
|
||||
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
||||
fun bind(item: Song, listener: MenuItemListener) {
|
||||
fun bind(item: Song, callback: AlbumDetailAdapter.Callback) {
|
||||
// Hide the track number view if the song does not have a track.
|
||||
if (item.track != null) {
|
||||
binding.songTrack.apply {
|
||||
|
@ -193,11 +200,11 @@ private class AlbumSongViewHolder private constructor(private val binding: ItemA
|
|||
binding.songName.text = item.resolveName(binding.context)
|
||||
binding.songDuration.text = item.durationMs.formatDurationMs(false)
|
||||
|
||||
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||
binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
||||
binding.root.apply {
|
||||
setOnClickListener { listener.onItemClick(item) }
|
||||
setOnClickListener { callback.onClick(item) }
|
||||
setOnLongClickListener {
|
||||
listener.onSelect(item)
|
||||
callback.onSelect(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,13 +26,13 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||
import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.ItemMenuCallback
|
||||
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
@ -42,8 +42,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
* one actually contains both album information and song information.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistDetailAdapter(private val listener: Listener) :
|
||||
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
|
||||
class ArtistDetailAdapter(private val callback: Callback) : DetailAdapter(callback, DIFFER) {
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
|
@ -70,9 +69,9 @@ class ArtistDetailAdapter(private val listener: Listener) :
|
|||
|
||||
if (payloads.isEmpty()) {
|
||||
when (val item = differ.currentList[position]) {
|
||||
is Artist -> (holder as ArtistDetailViewHolder).bind(item, listener)
|
||||
is Album -> (holder as ArtistAlbumViewHolder).bind(item, listener)
|
||||
is Song -> (holder as ArtistSongViewHolder).bind(item, listener)
|
||||
is Artist -> (holder as ArtistDetailViewHolder).bind(item, callback)
|
||||
is Album -> (holder as ArtistAlbumViewHolder).bind(item, callback)
|
||||
is Song -> (holder as ArtistSongViewHolder).bind(item, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +102,7 @@ class ArtistDetailAdapter(private val listener: Listener) :
|
|||
private class ArtistDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: Artist, listener: DetailAdapter.Listener) {
|
||||
fun bind(item: Artist, callback: DetailAdapter.Callback) {
|
||||
binding.detailCover.bind(item)
|
||||
binding.detailType.text = binding.context.getString(R.string.lbl_artist)
|
||||
binding.detailName.text = item.resolveName(binding.context)
|
||||
|
@ -132,8 +131,8 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
|
|||
binding.detailShuffleButton.isVisible = false
|
||||
}
|
||||
|
||||
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||
binding.detailPlayButton.setOnClickListener { callback.onPlay() }
|
||||
binding.detailShuffleButton.setOnClickListener { callback.onShuffle() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -155,13 +154,13 @@ private class ArtistDetailViewHolder private constructor(private val binding: It
|
|||
|
||||
private class ArtistAlbumViewHolder private constructor(private val binding: ItemParentBinding) :
|
||||
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
||||
fun bind(item: Album, listener: MenuItemListener) {
|
||||
fun bind(item: Album, callback: ItemMenuCallback) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
binding.parentInfo.text =
|
||||
item.date?.resolveDate(binding.context) ?: binding.context.getString(R.string.def_date)
|
||||
binding.parentMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||
binding.parentMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
||||
binding.root.setOnClickListener { callback.onClick(item) }
|
||||
}
|
||||
|
||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
|
@ -185,12 +184,12 @@ private class ArtistAlbumViewHolder private constructor(private val binding: Ite
|
|||
|
||||
private class ArtistSongViewHolder private constructor(private val binding: ItemSongBinding) :
|
||||
PlayingIndicatorAdapter.ViewHolder(binding.root) {
|
||||
fun bind(item: Song, listener: MenuItemListener) {
|
||||
fun bind(item: Song, callback: ItemMenuCallback) {
|
||||
binding.songAlbumCover.bind(item)
|
||||
binding.songName.text = item.resolveName(binding.context)
|
||||
binding.songInfo.text = item.album.resolveName(binding.context)
|
||||
binding.songMenu.setOnClickListener { listener.onOpenMenu(item, it) }
|
||||
binding.root.setOnClickListener { listener.onItemClick(item) }
|
||||
binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
||||
binding.root.setOnClickListener { callback.onClick(item) }
|
||||
}
|
||||
|
||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
|
|
|
@ -26,22 +26,20 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.databinding.ItemSortHeaderBinding
|
||||
import org.oxycblt.auxio.detail.SortHeader
|
||||
import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
|
||||
import org.oxycblt.auxio.ui.recycler.Header
|
||||
import org.oxycblt.auxio.ui.recycler.HeaderViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.list.Header
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.ItemSelectCallback
|
||||
import org.oxycblt.auxio.list.recycler.AuxioRecyclerView
|
||||
import org.oxycblt.auxio.list.recycler.HeaderViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
||||
private val listener: L,
|
||||
abstract class DetailAdapter(
|
||||
private val callback: Callback,
|
||||
diffCallback: DiffUtil.ItemCallback<Item>
|
||||
) : PlayingIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
||||
private var isPlaying = false
|
||||
|
||||
@Suppress("LeakingThis") override fun getItemCount() = differ.currentList.size
|
||||
|
||||
override fun getItemViewType(position: Int) =
|
||||
|
@ -71,7 +69,7 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
|||
if (payloads.isEmpty()) {
|
||||
when (item) {
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, listener)
|
||||
is SortHeader -> (holder as SortHeaderViewHolder).bind(item, callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,20 +105,23 @@ abstract class DetailAdapter<L : DetailAdapter.Listener>(
|
|||
}
|
||||
}
|
||||
|
||||
interface Listener : MenuItemListener {
|
||||
fun onPlayParent()
|
||||
fun onShuffleParent()
|
||||
fun onShowSortMenu(anchor: View)
|
||||
}
|
||||
open class Callback(
|
||||
onClick: (Item) -> Unit,
|
||||
onOpenItemMenu: (Item, View) -> Unit,
|
||||
onSelect: (Item) -> Unit,
|
||||
val onPlay: () -> Unit,
|
||||
val onShuffle: () -> Unit,
|
||||
val onOpenSortMenu: (View) -> Unit
|
||||
) : ItemSelectCallback(onClick, onOpenItemMenu, onSelect)
|
||||
}
|
||||
|
||||
class SortHeaderViewHolder(private val binding: ItemSortHeaderBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: SortHeader, listener: DetailAdapter.Listener) {
|
||||
fun bind(item: SortHeader, callback: DetailAdapter.Callback) {
|
||||
binding.headerTitle.text = binding.context.getString(item.string)
|
||||
binding.headerButton.apply {
|
||||
TooltipCompat.setTooltipText(this, contentDescription)
|
||||
setOnClickListener(listener::onShowSortMenu)
|
||||
setOnClickListener(callback.onOpenSortMenu)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,13 +24,13 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemDetailBinding
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.SimpleItemCallback
|
||||
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getPlural
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
@ -39,8 +39,7 @@ import org.oxycblt.auxio.util.inflater
|
|||
* An adapter for displaying genre information and it's children.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class GenreDetailAdapter(private val listener: Listener) :
|
||||
DetailAdapter<DetailAdapter.Listener>(listener, DIFFER) {
|
||||
class GenreDetailAdapter(private val callback: Callback) : DetailAdapter(callback, DIFFER) {
|
||||
override fun getItemViewType(position: Int) =
|
||||
when (differ.currentList[position]) {
|
||||
is Genre -> GenreDetailViewHolder.VIEW_TYPE
|
||||
|
@ -66,9 +65,9 @@ class GenreDetailAdapter(private val listener: Listener) :
|
|||
|
||||
if (payloads.isEmpty()) {
|
||||
when (val item = differ.currentList[position]) {
|
||||
is Genre -> (holder as GenreDetailViewHolder).bind(item, listener)
|
||||
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
|
||||
is Song -> (holder as SongViewHolder).bind(item, listener)
|
||||
is Genre -> (holder as GenreDetailViewHolder).bind(item, callback)
|
||||
is Artist -> (holder as ArtistViewHolder).bind(item, callback)
|
||||
is Song -> (holder as SongViewHolder).bind(item, callback)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,7 +97,7 @@ class GenreDetailAdapter(private val listener: Listener) :
|
|||
|
||||
private class GenreDetailViewHolder private constructor(private val binding: ItemDetailBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun bind(item: Genre, listener: DetailAdapter.Listener) {
|
||||
fun bind(item: Genre, callback: DetailAdapter.Callback) {
|
||||
binding.detailCover.bind(item)
|
||||
binding.detailType.text = binding.context.getString(R.string.lbl_genre)
|
||||
binding.detailName.text = item.resolveName(binding.context)
|
||||
|
@ -109,8 +108,8 @@ private class GenreDetailViewHolder private constructor(private val binding: Ite
|
|||
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
|
||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
||||
|
||||
binding.detailPlayButton.setOnClickListener { listener.onPlayParent() }
|
||||
binding.detailShuffleButton.setOnClickListener { listener.onShuffleParent() }
|
||||
binding.detailPlayButton.setOnClickListener { callback.onPlay() }
|
||||
binding.detailShuffleButton.setOnClickListener { callback.onShuffle() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -23,7 +23,6 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.iterator
|
||||
import androidx.core.view.updatePadding
|
||||
|
@ -46,6 +45,7 @@ import org.oxycblt.auxio.home.list.AlbumListFragment
|
|||
import org.oxycblt.auxio.home.list.ArtistListFragment
|
||||
import org.oxycblt.auxio.home.list.GenreListFragment
|
||||
import org.oxycblt.auxio.home.list.SongListFragment
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -55,12 +55,7 @@ import org.oxycblt.auxio.music.MusicViewModel
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.music.system.Indexer
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||
import org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
|
||||
import org.oxycblt.auxio.ui.selection.SelectionViewModel
|
||||
import org.oxycblt.auxio.shared.MainNavigationAction
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
||||
/**
|
||||
|
@ -68,12 +63,9 @@ import org.oxycblt.auxio.util.*
|
|||
* respective item.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuItemClickListener, SelectionToolbarOverlay.Callback {
|
||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||
class HomeFragment : SelectionFragment<FragmentHomeBinding>() {
|
||||
private val homeModel: HomeViewModel by androidActivityViewModels()
|
||||
private val musicModel: MusicViewModel by activityViewModels()
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
private val selectionModel: SelectionViewModel by activityViewModels()
|
||||
|
||||
// lifecycleObject builds this in the creation step, so doing this is okay.
|
||||
private val storagePermissionLauncher: ActivityResultLauncher<String> by lifecycleObject {
|
||||
|
@ -103,22 +95,14 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentHomeBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentHomeBinding, savedInstanceState: Bundle?) {
|
||||
binding.homeAppbar.apply {
|
||||
addOnOffsetChangedListener { _, offset ->
|
||||
val range = binding.homeAppbar.totalScrollRange
|
||||
|
||||
binding.homeToolbarOverlay.alpha =
|
||||
1f - (abs(offset.toFloat()) / (range.toFloat() / 2))
|
||||
|
||||
binding.homeContent.updatePadding(
|
||||
bottom = binding.homeAppbar.totalScrollRange + offset)
|
||||
}
|
||||
binding.homeAppbar.addOnOffsetChangedListener { _, it -> handleAppBarAnimation(it) }
|
||||
setupOverlay(binding.homeToolbarOverlay)
|
||||
binding.homeToolbar.setOnMenuItemClickListener {
|
||||
handleHomeMenuItem(it)
|
||||
true
|
||||
}
|
||||
|
||||
binding.homeToolbarOverlay.callback = this
|
||||
binding.homeToolbar.setOnMenuItemClickListener(this@HomeFragment)
|
||||
|
||||
updateTabConfiguration()
|
||||
setupTabs(binding)
|
||||
|
||||
// Load the track color in manually as it's unclear whether the track actually supports
|
||||
// using a ColorStateList in the resources
|
||||
|
@ -128,17 +112,11 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
binding.homePager.apply {
|
||||
adapter = HomePagerAdapter()
|
||||
|
||||
// We know that there will only be a fixed amount of tabs, so we manually set this
|
||||
// limit to that. This also prevents the appbar lift state from being confused during
|
||||
// page transitions.
|
||||
offscreenPageLimit = homeModel.tabs.size
|
||||
|
||||
reduceSensitivity(3)
|
||||
|
||||
registerOnPageChangeCallback(
|
||||
object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) =
|
||||
homeModel.updateCurrentTab(position)
|
||||
override fun onPageSelected(position: Int) {
|
||||
homeModel.setCurrentTab(position)
|
||||
}
|
||||
})
|
||||
|
||||
TabLayoutMediator(binding.homeTabs, this, AdaptiveTabStrategy(context, homeModel))
|
||||
|
@ -148,6 +126,22 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
// insets applied to the indexing view before API 30. Fix this by overriding the
|
||||
// callback with a non-consuming listener.
|
||||
setOnApplyWindowInsetsListener { _, insets -> insets }
|
||||
|
||||
// We know that there will only be a fixed amount of tabs, so we manually set this
|
||||
// limit to that. This also prevents the appbar lift state from being confused during
|
||||
// page transitions.
|
||||
offscreenPageLimit = homeModel.tabs.size
|
||||
|
||||
// By default, ViewPager2's sensitivity is high enough to result in vertical scroll
|
||||
// events being
|
||||
// registered as horizontal scroll events. Reflect into the internal recyclerview and
|
||||
// change the
|
||||
// touch slope so that touch actions will act more as a scroll than as a swipe. Derived
|
||||
// from:
|
||||
// https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414
|
||||
val recycler = VP_RECYCLER_FIELD.get(this@apply)
|
||||
val slop = RV_TOUCH_SLOP_FIELD.get(recycler) as Int
|
||||
RV_TOUCH_SLOP_FIELD.set(recycler, slop * 3)
|
||||
}
|
||||
|
||||
binding.homeFab.setOnClickListener { playbackModel.shuffleAll() }
|
||||
|
@ -157,9 +151,11 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
collect(homeModel.recreateTabs, ::handleRecreateTabs)
|
||||
collectImmediately(homeModel.currentTab, ::updateCurrentTab)
|
||||
collectImmediately(homeModel.songs, homeModel.isFastScrolling, ::updateFab)
|
||||
collectImmediately(musicModel.indexerState, ::handleIndexerState)
|
||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||
|
||||
collectImmediately(musicModel.indexerState, ::updateIndexerState)
|
||||
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
|
@ -171,13 +167,18 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentHomeBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.homeToolbarOverlay.callback = null
|
||||
binding.homeToolbar.setOnMenuItemClickListener(null)
|
||||
private fun handleAppBarAnimation(verticalOffset: Int) {
|
||||
val binding = requireBinding()
|
||||
val range = binding.homeAppbar.totalScrollRange
|
||||
|
||||
binding.homeToolbarOverlay.alpha =
|
||||
1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2))
|
||||
|
||||
binding.homeContent.updatePadding(
|
||||
bottom = binding.homeAppbar.totalScrollRange + verticalOffset)
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
private fun handleHomeMenuItem(item: MenuItem) {
|
||||
when (item.itemId) {
|
||||
R.id.action_search -> {
|
||||
logD("Navigating to search")
|
||||
|
@ -206,7 +207,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
.getSortForTab(homeModel.currentTab.value)
|
||||
.withAscending(item.isChecked))
|
||||
}
|
||||
|
||||
else -> {
|
||||
// Sorting option was selected, mark it as selected and update the mode
|
||||
item.isChecked = true
|
||||
|
@ -216,23 +216,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
.withMode(requireNotNull(Sort.Mode.fromItemId(item.itemId))))
|
||||
}
|
||||
}
|
||||
|
||||
// Always handling an item
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onClearSelection() {
|
||||
selectionModel.consume()
|
||||
}
|
||||
|
||||
override fun onPlaySelectionNext() {
|
||||
playbackModel.playNext(selectionModel.consume())
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
|
||||
override fun onAddSelectionToQueue() {
|
||||
playbackModel.addToQueue(selectionModel.consume())
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
}
|
||||
|
||||
private fun updateCurrentTab(tab: MusicMode) {
|
||||
|
@ -263,7 +246,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
}
|
||||
}
|
||||
|
||||
requireBinding().homeAppbar.liftOnScrollTargetViewId = getRecyclerId(tab)
|
||||
requireBinding().homeAppbar.liftOnScrollTargetViewId = getTabRecyclerId(tab)
|
||||
}
|
||||
|
||||
private fun updateSortMenu(mode: MusicMode, isVisible: (Int) -> Boolean) {
|
||||
|
@ -285,34 +268,24 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
|
||||
private fun handleRecreateTabs(recreate: Boolean) {
|
||||
if (recreate) {
|
||||
requireBinding().homePager.recreate()
|
||||
updateTabConfiguration()
|
||||
val binding = requireBinding()
|
||||
|
||||
binding.homePager.apply {
|
||||
currentItem = 0
|
||||
adapter = HomePagerAdapter()
|
||||
}
|
||||
|
||||
setupTabs(binding)
|
||||
|
||||
homeModel.finishRecreateTabs()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateTabConfiguration() {
|
||||
val binding = requireBinding()
|
||||
val toolbarParams = binding.homeToolbarOverlay.layoutParams as AppBarLayout.LayoutParams
|
||||
if (homeModel.tabs.size == 1) {
|
||||
// A single tab makes the tab layout redundant, hide it and disable the collapsing
|
||||
// behavior.
|
||||
binding.homeTabs.isVisible = false
|
||||
binding.homeAppbar.setExpanded(true, false)
|
||||
toolbarParams.scrollFlags = 0
|
||||
} else {
|
||||
binding.homeTabs.isVisible = true
|
||||
toolbarParams.scrollFlags =
|
||||
AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or
|
||||
AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleIndexerState(state: Indexer.State?) {
|
||||
private fun updateIndexerState(state: Indexer.State?) {
|
||||
val binding = requireBinding()
|
||||
when (state) {
|
||||
is Indexer.State.Complete -> handleIndexerResponse(binding, state.response)
|
||||
is Indexer.State.Indexing -> handleIndexingState(binding, state.indexing)
|
||||
is Indexer.State.Complete -> setupCompleteState(binding, state.response)
|
||||
is Indexer.State.Indexing -> setupIndexingState(binding, state.indexing)
|
||||
null -> {
|
||||
logD("Indexer is in indeterminate state")
|
||||
binding.homeIndexingContainer.visibility = View.INVISIBLE
|
||||
|
@ -320,7 +293,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleIndexerResponse(binding: FragmentHomeBinding, response: Indexer.Response) {
|
||||
private fun setupCompleteState(binding: FragmentHomeBinding, response: Indexer.Response) {
|
||||
if (response is Indexer.Response.Ok) {
|
||||
binding.homeFab.show()
|
||||
binding.homeIndexingContainer.visibility = View.INVISIBLE
|
||||
|
@ -366,7 +339,7 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) {
|
||||
private fun setupIndexingState(binding: FragmentHomeBinding, indexing: Indexer.Indexing) {
|
||||
binding.homeIndexingContainer.visibility = View.VISIBLE
|
||||
binding.homeIndexingProgress.visibility = View.VISIBLE
|
||||
binding.homeIndexingAction.visibility = View.INVISIBLE
|
||||
|
@ -399,17 +372,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
val binding = requireBinding()
|
||||
if (binding.homeToolbarOverlay.updateSelectionAmount(selected.size) &&
|
||||
selected.isNotEmpty()) {
|
||||
logD("Significant selection occurred, expanding AppBar")
|
||||
// Significant enough change where we want to expand the RecyclerView
|
||||
binding.homeAppbar.expandWithRecycler(
|
||||
binding.homePager.findViewById(getRecyclerId(homeModel.currentTab.value)))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
val action =
|
||||
when (item) {
|
||||
|
@ -426,6 +388,42 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
findNavController().navigate(action)
|
||||
}
|
||||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
val binding = requireBinding()
|
||||
if (binding.homeToolbarOverlay.updateSelectionAmount(selected.size) &&
|
||||
selected.isNotEmpty()) {
|
||||
logD("Significant selection occurred, expanding AppBar")
|
||||
// Significant enough change where we want to expand the RecyclerView
|
||||
binding.homeAppbar.expandWithRecycler(
|
||||
binding.homePager.findViewById(getTabRecyclerId(homeModel.currentTab.value)))
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the ID of a RecyclerView that the given [tab] contains */
|
||||
private fun getTabRecyclerId(tab: MusicMode) =
|
||||
when (tab) {
|
||||
MusicMode.SONGS -> R.id.home_song_recycler
|
||||
MusicMode.ALBUMS -> R.id.home_album_recycler
|
||||
MusicMode.ARTISTS -> R.id.home_artist_recycler
|
||||
MusicMode.GENRES -> R.id.home_genre_recycler
|
||||
}
|
||||
|
||||
private fun setupTabs(binding: FragmentHomeBinding) {
|
||||
val toolbarParams = binding.homeToolbarOverlay.layoutParams as AppBarLayout.LayoutParams
|
||||
if (homeModel.tabs.size == 1) {
|
||||
// A single tab makes the tab layout redundant, hide it and disable the collapsing
|
||||
// behavior.
|
||||
binding.homeTabs.isVisible = false
|
||||
binding.homeAppbar.setExpanded(true, false)
|
||||
toolbarParams.scrollFlags = 0
|
||||
} else {
|
||||
binding.homeTabs.isVisible = true
|
||||
toolbarParams.scrollFlags =
|
||||
AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL or
|
||||
AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAxisTransitions(axis: Int) {
|
||||
// Sanity check
|
||||
check(axis == MaterialSharedAxis.X || axis == MaterialSharedAxis.Z) {
|
||||
|
@ -437,35 +435,6 @@ class HomeFragment : ViewBindingFragment<FragmentHomeBinding>(), Toolbar.OnMenuI
|
|||
reenterTransition = MaterialSharedAxis(axis, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of a RecyclerView that the given [tab] contains
|
||||
*/
|
||||
private fun getRecyclerId(tab: MusicMode) =
|
||||
when (tab) {
|
||||
MusicMode.SONGS -> R.id.home_song_recycler
|
||||
MusicMode.ALBUMS -> R.id.home_album_recycler
|
||||
MusicMode.ARTISTS -> R.id.home_artist_recycler
|
||||
MusicMode.GENRES -> R.id.home_genre_recycler
|
||||
}
|
||||
|
||||
/**
|
||||
* By default, ViewPager2's sensitivity is high enough to result in vertical scroll events being
|
||||
* registered as horizontal scroll events. Reflect into the internal recyclerview and change the
|
||||
* touch slope so that touch actions will act more as a scroll than as a swipe. Derived from:
|
||||
* https://al-e-shevelev.medium.com/how-to-reduce-scroll-sensitivity-of-viewpager2-widget-87797ad02414
|
||||
*/
|
||||
private fun ViewPager2.reduceSensitivity(by: Int) {
|
||||
val recycler = VP_RECYCLER_FIELD.get(this@reduceSensitivity)
|
||||
val slop = RV_TOUCH_SLOP_FIELD.get(recycler) as Int
|
||||
RV_TOUCH_SLOP_FIELD.set(recycler, slop * by)
|
||||
}
|
||||
|
||||
/** Forces the view to recreate all fragments contained within it. */
|
||||
private fun ViewPager2.recreate() {
|
||||
currentItem = 0
|
||||
adapter = HomePagerAdapter()
|
||||
}
|
||||
|
||||
private inner class HomePagerAdapter :
|
||||
FragmentStateAdapter(childFragmentManager, viewLifecycleOwner.lifecycle) {
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ class HomeViewModel(application: Application) :
|
|||
}
|
||||
|
||||
/** Update the current tab based off of the new ViewPager position. */
|
||||
fun updateCurrentTab(pos: Int) {
|
||||
fun setCurrentTab(pos: Int) {
|
||||
logD("Updating current tab to ${tabs[pos]}")
|
||||
_currentTab.value = tabs[pos]
|
||||
}
|
||||
|
@ -129,9 +129,9 @@ class HomeViewModel(application: Application) :
|
|||
* Update the fast scroll state. This is used to control the FAB visibility whenever the user
|
||||
* begins to fast scroll.
|
||||
*/
|
||||
fun updateFastScrolling(scrolling: Boolean) {
|
||||
logD("Updating fast scrolling state: $scrolling")
|
||||
_isFastScrolling.value = scrolling
|
||||
fun setFastScrolling(fastScrolling: Boolean) {
|
||||
logD("Updating fast scrolling state: $fastScrolling")
|
||||
_isFastScrolling.value = fastScrolling
|
||||
}
|
||||
|
||||
// --- OVERRIDES ---
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.fastscroll
|
||||
package org.oxycblt.auxio.home.fastscroll
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.fastscroll
|
||||
package org.oxycblt.auxio.home.fastscroll
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
|
@ -35,7 +35,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlin.math.abs
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
|
||||
import org.oxycblt.auxio.list.recycler.AuxioRecyclerView
|
||||
import org.oxycblt.auxio.util.getDimenSize
|
||||
import org.oxycblt.auxio.util.getDrawableCompat
|
||||
import org.oxycblt.auxio.util.isRtl
|
||||
|
@ -137,33 +137,26 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
removeCallbacks(hideThumbRunnable)
|
||||
showScrollbar()
|
||||
showPopup()
|
||||
listener?.onFastScrollStart()
|
||||
} else {
|
||||
postAutoHideScrollbar()
|
||||
hidePopup()
|
||||
listener?.onFastScrollStop()
|
||||
}
|
||||
|
||||
fastScrollCallback?.invoke(field)
|
||||
}
|
||||
|
||||
private val tRect = Rect()
|
||||
|
||||
interface PopupProvider {
|
||||
fun getPopup(pos: Int): String?
|
||||
}
|
||||
|
||||
/** Callback to provide a string to be shown on the popup when an item is passed */
|
||||
var popupProvider: PopupProvider? = null
|
||||
var popupProvider: ((Int) -> String?)? = null
|
||||
|
||||
interface OnFastScrollListener {
|
||||
fun onFastScrollStart()
|
||||
fun onFastScrollStop()
|
||||
}
|
||||
class FastScrollCallback(val onStart: () -> Unit, val onEnd: () -> Unit)
|
||||
|
||||
/**
|
||||
* A listener for when a drag event occurs. The value will be true if a drag has begun, and
|
||||
* A callback for when a drag event occurs. The value will be true if a drag has begun, and
|
||||
* false if a drag ended.
|
||||
*/
|
||||
var listener: OnFastScrollListener? = null
|
||||
var fastScrollCallback: ((Boolean) -> Unit)? = null
|
||||
|
||||
init {
|
||||
overlay.add(thumbView)
|
||||
|
@ -225,7 +218,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
if (firstAdapterPos != NO_POSITION && provider != null) {
|
||||
popupView.isInvisible = false
|
||||
// Get the popup text. If there is none, we default to "?".
|
||||
popupText = provider.getPopup(firstAdapterPos) ?: "?"
|
||||
popupText = provider.invoke(firstAdapterPos) ?: "?"
|
||||
} else {
|
||||
// No valid position or provider, do not show the popup.
|
||||
popupView.isInvisible = true
|
|
@ -19,11 +19,19 @@ package org.oxycblt.auxio.home.list
|
|||
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import java.util.Formatter
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.list.recycler.AlbumViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
|
@ -31,36 +39,56 @@ import org.oxycblt.auxio.music.MusicParent
|
|||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.playback.secsToMs
|
||||
import org.oxycblt.auxio.ui.recycler.AlbumViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
||||
/**
|
||||
* A [HomeListFragment] for showing a list of [Album]s.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AlbumListFragment : HomeListFragment<Album>() {
|
||||
private val homeAdapter = AlbumAdapter(this)
|
||||
class AlbumListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
|
||||
private val homeAdapter =
|
||||
AlbumAdapter(ItemSelectCallback(::handleClick, ::handleOpenMenu, ::handleClick))
|
||||
|
||||
private val formatterSb = StringBuilder(32)
|
||||
private val formatter = Formatter(formatterSb)
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
FragmentHomeListBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
||||
binding.homeRecycler.apply {
|
||||
id = R.id.home_album_recycler
|
||||
adapter = homeAdapter
|
||||
|
||||
popupProvider = ::updatePopup
|
||||
fastScrollCallback = { homeModel.setFastScrolling(it) }
|
||||
}
|
||||
|
||||
collectImmediately(homeModel.albums, homeAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::updateSelection)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handleParent)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int): String? {
|
||||
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.homeRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onClick(music: Music) {
|
||||
check(music is Album) { "Unexpected datatype: ${music::class.java}" }
|
||||
navModel.exploreNavigateTo(music)
|
||||
}
|
||||
|
||||
private fun handleOpenMenu(item: Item, anchor: View) {
|
||||
check(item is Album) { "Unexpected datatype: ${item::class.java}" }
|
||||
openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
||||
}
|
||||
|
||||
private fun updatePopup(pos: Int): String? {
|
||||
val album = homeModel.albums.value[pos]
|
||||
|
||||
// Change how we display the popup depending on the mode.
|
||||
|
@ -98,18 +126,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
|||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(music: Music) {
|
||||
check(music is Album) { "Unexpected datatype: ${music::class.java}" }
|
||||
navModel.exploreNavigateTo(music)
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
check(item is Album) { "Unexpected datatype: ${item::class.java}" }
|
||||
musicMenu(anchor, R.menu.menu_album_actions, item)
|
||||
}
|
||||
|
||||
private fun handleParent(parent: MusicParent?, isPlaying: Boolean) {
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Album) {
|
||||
homeAdapter.updateIndicator(parent, isPlaying)
|
||||
} else {
|
||||
|
@ -118,7 +135,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
|||
}
|
||||
}
|
||||
|
||||
private class AlbumAdapter(private val listener: MenuItemListener) :
|
||||
private class AlbumAdapter(private val callback: ItemSelectCallback) :
|
||||
SelectionIndicatorAdapter<AlbumViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, AlbumViewHolder.DIFFER)
|
||||
|
||||
|
@ -134,7 +151,7 @@ class AlbumListFragment : HomeListFragment<Album>() {
|
|||
super.onBindViewHolder(holder, position, payloads)
|
||||
|
||||
if (payloads.isEmpty()) {
|
||||
holder.bind(differ.currentList[position], listener)
|
||||
holder.bind(differ.currentList[position], callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,21 +18,24 @@
|
|||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.list.recycler.ArtistViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.ui.recycler.ArtistViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
|
||||
|
@ -40,8 +43,14 @@ import org.oxycblt.auxio.util.nonZeroOrNull
|
|||
* A [HomeListFragment] for showing a list of [Artist]s.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class ArtistListFragment : HomeListFragment<Artist>() {
|
||||
private val homeAdapter = ArtistAdapter(this)
|
||||
class ArtistListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
|
||||
private val homeAdapter =
|
||||
ArtistAdapter(ItemSelectCallback(::handleClick, ::handleOpenMenu, ::handleSelect))
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
FragmentHomeListBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
@ -49,14 +58,22 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
binding.homeRecycler.apply {
|
||||
id = R.id.home_artist_recycler
|
||||
adapter = homeAdapter
|
||||
|
||||
popupProvider = ::updatePopup
|
||||
fastScrollCallback = homeModel::setFastScrolling
|
||||
}
|
||||
|
||||
collectImmediately(homeModel.artists, homeAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::updateSelection)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handleParent)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int): String? {
|
||||
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.homeRecycler.adapter = null
|
||||
}
|
||||
|
||||
private fun updatePopup(pos: Int): String? {
|
||||
val artist = homeModel.artists.value[pos]
|
||||
|
||||
// Change how we display the popup depending on the mode.
|
||||
|
@ -75,17 +92,17 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(music: Music) {
|
||||
override fun onClick(music: Music) {
|
||||
check(music is Artist) { "Unexpected datatype: ${music::class.java}" }
|
||||
navModel.exploreNavigateTo(music)
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
private fun handleOpenMenu(item: Item, anchor: View) {
|
||||
check(item is Artist) { "Unexpected datatype: ${item::class.java}" }
|
||||
musicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
}
|
||||
|
||||
private fun handleParent(parent: MusicParent?, isPlaying: Boolean) {
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Artist) {
|
||||
homeAdapter.updateIndicator(parent, isPlaying)
|
||||
} else {
|
||||
|
@ -94,7 +111,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
}
|
||||
}
|
||||
|
||||
private class ArtistAdapter(private val listener: MenuItemListener) :
|
||||
private class ArtistAdapter(private val callback: ItemSelectCallback) :
|
||||
SelectionIndicatorAdapter<ArtistViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, ArtistViewHolder.DIFFER)
|
||||
|
||||
|
@ -114,7 +131,7 @@ class ArtistListFragment : HomeListFragment<Artist>() {
|
|||
super.onBindViewHolder(holder, position, payloads)
|
||||
|
||||
if (payloads.isEmpty()) {
|
||||
holder.bind(differ.currentList[position], listener)
|
||||
holder.bind(differ.currentList[position], callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,29 +18,38 @@
|
|||
package org.oxycblt.auxio.home.list
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.list.recycler.GenreViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.ui.recycler.GenreViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
||||
/**
|
||||
* A [HomeListFragment] for showing a list of [Genre]s.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class GenreListFragment : HomeListFragment<Genre>() {
|
||||
private val homeAdapter = GenreAdapter(this)
|
||||
class GenreListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
|
||||
private val homeAdapter =
|
||||
GenreAdapter(ItemSelectCallback(::handleClick, ::handleOpenMenu, ::handleSelect))
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
FragmentHomeListBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
@ -48,14 +57,22 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
binding.homeRecycler.apply {
|
||||
id = R.id.home_genre_recycler
|
||||
adapter = homeAdapter
|
||||
|
||||
popupProvider = ::updatePopup
|
||||
fastScrollCallback = homeModel::setFastScrolling
|
||||
}
|
||||
|
||||
collectImmediately(homeModel.genres, homeAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::updateSelection)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
||||
collectImmediately(playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int): String? {
|
||||
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.homeRecycler.adapter = null
|
||||
}
|
||||
|
||||
private fun updatePopup(pos: Int): String? {
|
||||
val genre = homeModel.genres.value[pos]
|
||||
|
||||
// Change how we display the popup depending on the mode.
|
||||
|
@ -74,17 +91,17 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(music: Music) {
|
||||
override fun onClick(music: Music) {
|
||||
check(music is Genre) { "Unexpected datatype: ${music::class.java}" }
|
||||
navModel.exploreNavigateTo(music)
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
private fun handleOpenMenu(item: Item, anchor: View) {
|
||||
check(item is Genre) { "Unexpected datatype: ${item::class.java}" }
|
||||
musicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
}
|
||||
|
||||
private fun handlePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
private fun updatePlayback(parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent is Genre) {
|
||||
homeAdapter.updateIndicator(parent, isPlaying)
|
||||
} else {
|
||||
|
@ -93,7 +110,7 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
}
|
||||
}
|
||||
|
||||
private class GenreAdapter(private val listener: MenuItemListener) :
|
||||
private class GenreAdapter(private val callback: ItemSelectCallback) :
|
||||
SelectionIndicatorAdapter<GenreViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, GenreViewHolder.DIFFER)
|
||||
|
||||
|
@ -109,7 +126,7 @@ class GenreListFragment : HomeListFragment<Genre>() {
|
|||
super.onBindViewHolder(holder, position, payloads)
|
||||
|
||||
if (payloads.isEmpty()) {
|
||||
holder.bind(differ.currentList[position], listener)
|
||||
holder.bind(differ.currentList[position], callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -19,11 +19,19 @@ package org.oxycblt.auxio.home.list
|
|||
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import java.util.Formatter
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentHomeListBinding
|
||||
import org.oxycblt.auxio.home.HomeViewModel
|
||||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.list.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
|
@ -32,11 +40,6 @@ import org.oxycblt.auxio.music.Sort
|
|||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.playback.secsToMs
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.recycler.SelectionIndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.context
|
||||
|
||||
|
@ -44,27 +47,43 @@ import org.oxycblt.auxio.util.context
|
|||
* A [HomeListFragment] for showing a list of [Song]s.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SongListFragment : HomeListFragment<Song>() {
|
||||
private val homeAdapter = SongAdapter(this)
|
||||
class SongListFragment : SelectionFragment<FragmentHomeListBinding>() {
|
||||
private val homeModel: HomeViewModel by activityViewModels()
|
||||
|
||||
private val homeAdapter =
|
||||
SongAdapter(ItemSelectCallback(::handleClick, ::handleOpenMenu, ::handleSelect))
|
||||
|
||||
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
|
||||
|
||||
private val formatterSb = StringBuilder(50)
|
||||
private val formatter = Formatter(formatterSb)
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
FragmentHomeListBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentHomeListBinding, savedInstanceState: Bundle?) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
||||
binding.homeRecycler.apply {
|
||||
id = R.id.home_song_recycler
|
||||
adapter = homeAdapter
|
||||
|
||||
popupProvider = ::updatePopup
|
||||
fastScrollCallback = homeModel::setFastScrolling
|
||||
}
|
||||
|
||||
collectImmediately(homeModel.songs, homeAdapter::replaceList)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::updateSelection)
|
||||
collectImmediately(selectionModel.selected, homeAdapter::setSelected)
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
}
|
||||
|
||||
override fun getPopup(pos: Int): String? {
|
||||
override fun onDestroyBinding(binding: FragmentHomeListBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.homeRecycler.adapter = null
|
||||
}
|
||||
|
||||
private fun updatePopup(pos: Int): String? {
|
||||
val song = homeModel.songs.value[pos]
|
||||
|
||||
// Change how we display the popup depending on the mode.
|
||||
|
@ -106,7 +125,7 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(music: Music) {
|
||||
override fun onClick(music: Music) {
|
||||
check(music is Song) { "Unexpected datatype: ${music::class.java}" }
|
||||
when (settings.libPlaybackMode) {
|
||||
MusicMode.SONGS -> playbackModel.playFromAll(music)
|
||||
|
@ -116,12 +135,12 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
private fun handleOpenMenu(item: Item, anchor: View) {
|
||||
check(item is Song) { "Unexpected datatype: ${item::class.java}" }
|
||||
musicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
}
|
||||
|
||||
private fun handlePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
if (parent == null) {
|
||||
homeAdapter.updateIndicator(song, isPlaying)
|
||||
} else {
|
||||
|
@ -130,7 +149,7 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
}
|
||||
}
|
||||
|
||||
private class SongAdapter(private val listener: MenuItemListener) :
|
||||
private class SongAdapter(private val callback: ItemSelectCallback) :
|
||||
SelectionIndicatorAdapter<SongViewHolder>() {
|
||||
private val differ = SyncListDiffer(this, SongViewHolder.DIFFER)
|
||||
|
||||
|
@ -146,7 +165,7 @@ class SongListFragment : HomeListFragment<Song>() {
|
|||
super.onBindViewHolder(holder, position, payloads)
|
||||
|
||||
if (payloads.isEmpty()) {
|
||||
holder.bind(differ.currentList[position], listener)
|
||||
holder.bind(differ.currentList[position], callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,11 +24,11 @@ import android.view.ViewGroup
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemTabBinding
|
||||
import org.oxycblt.auxio.list.recycler.DialogViewHolder
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.ui.recycler.DialogViewHolder
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewHolder>() {
|
||||
class TabAdapter(private val callback: Callback) : RecyclerView.Adapter<TabViewHolder>() {
|
||||
var tabs = arrayOf<Tab>()
|
||||
private set
|
||||
|
||||
|
@ -37,7 +37,7 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = TabViewHolder.new(parent)
|
||||
|
||||
override fun onBindViewHolder(holder: TabViewHolder, position: Int) {
|
||||
holder.bind(tabs[position], listener)
|
||||
holder.bind(tabs[position], callback)
|
||||
}
|
||||
|
||||
@Suppress("NotifyDatasetChanged")
|
||||
|
@ -59,10 +59,10 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
|
|||
notifyItemMoved(from, to)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onVisibilityToggled(mode: MusicMode)
|
||||
fun onPickUpTab(viewHolder: RecyclerView.ViewHolder)
|
||||
}
|
||||
class Callback(
|
||||
val toggleVisibility: (MusicMode) -> Unit,
|
||||
val pickUpTab: (RecyclerView.ViewHolder) -> Unit
|
||||
)
|
||||
|
||||
companion object {
|
||||
val PAYLOAD_TAB_CHANGED = Any()
|
||||
|
@ -72,8 +72,8 @@ class TabAdapter(private val listener: Listener) : RecyclerView.Adapter<TabViewH
|
|||
class TabViewHolder private constructor(private val binding: ItemTabBinding) :
|
||||
DialogViewHolder(binding.root) {
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun bind(item: Tab, listener: TabAdapter.Listener) {
|
||||
binding.root.setOnClickListener { listener.onVisibilityToggled(item.mode) }
|
||||
fun bind(item: Tab, callback: TabAdapter.Callback) {
|
||||
binding.root.setOnClickListener { callback.toggleVisibility(item.mode) }
|
||||
|
||||
binding.tabIcon.apply {
|
||||
setText(
|
||||
|
@ -90,7 +90,7 @@ class TabViewHolder private constructor(private val binding: ItemTabBinding) :
|
|||
binding.tabDragHandle.setOnTouchListener { _, motionEvent ->
|
||||
binding.tabDragHandle.performClick()
|
||||
if (motionEvent.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
listener.onPickUpTab(this)
|
||||
callback.pickUpTab(this)
|
||||
true
|
||||
} else false
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.DialogTabsBinding
|
||||
import org.oxycblt.auxio.music.MusicMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
@ -35,8 +35,8 @@ import org.oxycblt.auxio.util.logD
|
|||
* The dialog for customizing library tabs.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAdapter.Listener {
|
||||
private val tabAdapter = TabAdapter(this)
|
||||
class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>() {
|
||||
private val tabAdapter = TabAdapter(TabAdapter.Callback(::toggleVisibility, ::pickUpTab))
|
||||
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
|
||||
private val touchHelper: ItemTouchHelper by lifecycleObject {
|
||||
ItemTouchHelper(TabDragCallback(tabAdapter))
|
||||
|
@ -79,7 +79,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
|
|||
binding.tabRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onVisibilityToggled(mode: MusicMode) {
|
||||
private fun toggleVisibility(mode: MusicMode) {
|
||||
val index = tabAdapter.tabs.indexOfFirst { it.mode == mode }
|
||||
if (index > -1) {
|
||||
val tab = tabAdapter.tabs[index]
|
||||
|
@ -95,7 +95,7 @@ class TabCustomizeDialog : ViewBindingDialogFragment<DialogTabsBinding>(), TabAd
|
|||
tabAdapter.tabs.filterIsInstance<Tab.Visible>().isNotEmpty()
|
||||
}
|
||||
|
||||
override fun onPickUpTab(viewHolder: RecyclerView.ViewHolder) {
|
||||
private fun pickUpTab(viewHolder: RecyclerView.ViewHolder) {
|
||||
touchHelper.startDrag(viewHolder)
|
||||
}
|
||||
|
||||
|
|
56
app/src/main/java/org/oxycblt/auxio/list/List.kt
Normal file
56
app/src/main/java/org/oxycblt/auxio/list/List.kt
Normal 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)
|
||||
}
|
|
@ -15,8 +15,9 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.fragment
|
||||
package org.oxycblt.auxio.list
|
||||
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.annotation.MenuRes
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
|
@ -29,8 +30,9 @@ import org.oxycblt.auxio.music.Artist
|
|||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.shared.MainNavigationAction
|
||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
|
@ -50,11 +52,11 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|||
* Opens the given menu in context of [song]. Assumes that the menu is only composed of common
|
||||
* [Song] options.
|
||||
*/
|
||||
protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, song: Song) {
|
||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, song: Song) {
|
||||
logD("Launching new song menu: ${song.rawName}")
|
||||
|
||||
musicMenuImpl(anchor, menuRes) { id ->
|
||||
when (id) {
|
||||
openMusicMenuImpl(anchor, menuRes) {
|
||||
when (it.itemId) {
|
||||
R.id.action_play_next -> {
|
||||
playbackModel.playNext(song)
|
||||
requireContext().showToast(R.string.lng_queue_added)
|
||||
|
@ -78,8 +80,6 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|||
error("Unexpected menu item selected")
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,11 +87,11 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|||
* Opens the given menu in context of [album]. Assumes that the menu is only composed of common
|
||||
* [Album] options.
|
||||
*/
|
||||
protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) {
|
||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, album: Album) {
|
||||
logD("Launching new album menu: ${album.rawName}")
|
||||
|
||||
musicMenuImpl(anchor, menuRes) { id ->
|
||||
when (id) {
|
||||
openMusicMenuImpl(anchor, menuRes) {
|
||||
when (it.itemId) {
|
||||
R.id.action_play -> {
|
||||
playbackModel.play(album)
|
||||
}
|
||||
|
@ -113,8 +113,6 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|||
error("Unexpected menu item selected")
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,11 +120,11 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|||
* Opens the given menu in context of [artist]. Assumes that the menu is only composed of common
|
||||
* [Artist] options.
|
||||
*/
|
||||
protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) {
|
||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, artist: Artist) {
|
||||
logD("Launching new artist menu: ${artist.rawName}")
|
||||
|
||||
musicMenuImpl(anchor, menuRes) { id ->
|
||||
when (id) {
|
||||
openMusicMenuImpl(anchor, menuRes) {
|
||||
when (it.itemId) {
|
||||
R.id.action_play -> {
|
||||
playbackModel.play(artist)
|
||||
}
|
||||
|
@ -145,8 +143,6 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|||
error("Unexpected menu item selected")
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,11 +150,11 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|||
* Opens the given menu in context of [genre]. Assumes that the menu is only composed of common
|
||||
* [Genre] options.
|
||||
*/
|
||||
protected fun musicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) {
|
||||
protected fun openMusicMenu(anchor: View, @MenuRes menuRes: Int, genre: Genre) {
|
||||
logD("Launching new genre menu: ${genre.rawName}")
|
||||
|
||||
musicMenuImpl(anchor, menuRes) { id ->
|
||||
when (id) {
|
||||
openMusicMenuImpl(anchor, menuRes) {
|
||||
when (it.itemId) {
|
||||
R.id.action_play -> {
|
||||
playbackModel.play(genre)
|
||||
}
|
||||
|
@ -177,20 +173,27 @@ abstract class MenuFragment<T : ViewBinding> : ViewBindingFragment<T>() {
|
|||
error("Unexpected menu item selected")
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun musicMenuImpl(anchor: View, @MenuRes menuRes: Int, onSelect: (Int) -> Boolean) {
|
||||
menu(anchor, menuRes) { setOnMenuItemClickListener { item -> onSelect(item.itemId) } }
|
||||
private fun openMusicMenuImpl(
|
||||
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
|
||||
* function is a no-op.
|
||||
*/
|
||||
protected fun menu(anchor: View, @MenuRes menuRes: Int, block: PopupMenu.() -> Unit) {
|
||||
protected fun openMenu(anchor: View, @MenuRes menuRes: Int, block: PopupMenu.() -> Unit) {
|
||||
if (currentMenu != null) {
|
||||
logD("Menu already present, not launching")
|
||||
return
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -15,15 +15,14 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.selection
|
||||
package org.oxycblt.auxio.list
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MenuItem
|
||||
import android.widget.FrameLayout
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener
|
||||
import androidx.core.view.isInvisible
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import org.oxycblt.auxio.R
|
||||
|
@ -38,29 +37,12 @@ class SelectionToolbarOverlay
|
|||
@JvmOverloads
|
||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||
FrameLayout(context, attrs, defStyleAttr) {
|
||||
var callback: Callback? = null
|
||||
|
||||
private lateinit var innerToolbar: MaterialToolbar
|
||||
private val selectionToolbar =
|
||||
MaterialToolbar(context).apply {
|
||||
setNavigationIcon(R.drawable.ic_close_24)
|
||||
setNavigationOnClickListener {
|
||||
callback?.onClearSelection()
|
||||
}
|
||||
|
||||
inflateMenu(R.menu.menu_selection_actions)
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.action_play_next -> {
|
||||
callback?.onPlaySelectionNext()
|
||||
}
|
||||
R.id.action_queue_add -> {
|
||||
callback?.onAddSelectionToQueue()
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private var fadeThroughAnimator: ValueAnimator? = null
|
||||
|
@ -75,9 +57,12 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
addView(selectionToolbar)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
callback = null
|
||||
fun setOnSelectionCancelListener(listener: OnClickListener) {
|
||||
selectionToolbar.setNavigationOnClickListener(listener)
|
||||
}
|
||||
|
||||
fun setOnMenuItemClickListener(listener: OnMenuItemClickListener) {
|
||||
selectionToolbar.setOnMenuItemClickListener(listener)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -151,10 +136,4 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
|||
isInvisible = innerAlpha == 1f
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onClearSelection()
|
||||
fun onPlaySelectionNext()
|
||||
fun onAddSelectionToQueue()
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.selection
|
||||
package org.oxycblt.auxio.list
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -33,15 +33,15 @@ class SelectionViewModel : ViewModel() {
|
|||
get() = _selected
|
||||
|
||||
/** Select a music item. */
|
||||
fun select(item: Music) {
|
||||
val items = _selected.value.toMutableList()
|
||||
if (items.remove(item)) {
|
||||
logD("Unselecting item $item")
|
||||
_selected.value = items
|
||||
fun select(music: Music) {
|
||||
val selected = _selected.value.toMutableList()
|
||||
if (selected.remove(music)) {
|
||||
logD("Unselecting item $music")
|
||||
_selected.value = selected
|
||||
} else {
|
||||
logD("Selecting item $item")
|
||||
items.add(item)
|
||||
_selected.value = items
|
||||
logD("Selecting item $music")
|
||||
selected.add(music)
|
||||
_selected.value = selected
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.recycler
|
||||
package org.oxycblt.auxio.list.recycler
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.recycler
|
||||
package org.oxycblt.auxio.list.recycler
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
|
@ -15,10 +15,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.recycler
|
||||
package org.oxycblt.auxio.list.recycler
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
/**
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.recycler
|
||||
package org.oxycblt.auxio.list.recycler
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -37,7 +37,7 @@ abstract class SelectionIndicatorAdapter<VH : RecyclerView.ViewHolder> :
|
|||
}
|
||||
}
|
||||
|
||||
fun updateSelection(items: List<Music>) {
|
||||
fun setSelected(items: List<Music>) {
|
||||
val oldSelectedItems = selectedItems
|
||||
val newSelectedItems = items.toSet()
|
||||
if (newSelectedItems == oldSelectedItems) {
|
|
@ -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
|
||||
}
|
|
@ -15,38 +15,12 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.recycler
|
||||
package org.oxycblt.auxio.list.recycler
|
||||
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.recyclerview.widget.AdapterListUpdateCallback
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
/** A marker for something that is a RecyclerView item. Has no functionality on it's own. */
|
||||
interface Item
|
||||
|
||||
/** A data object used solely for the "Header" UI element. */
|
||||
data class Header(
|
||||
/** The string resource used for the header. */
|
||||
@StringRes val string: Int
|
||||
) : Item
|
||||
|
||||
/** An interface for detecting if an item has been clicked once. */
|
||||
interface ItemClickListener {
|
||||
/** Called when an item is clicked once. */
|
||||
fun onItemClick(item: Item)
|
||||
}
|
||||
|
||||
/** An interface for detecting if an item has had it's menu opened. */
|
||||
interface MenuItemListener : ItemClickListener {
|
||||
/** Called when an item is long-clicked. */
|
||||
fun onSelect(item: Item) {}
|
||||
|
||||
/** Called when an item desires to open a menu relating to it. */
|
||||
fun onOpenMenu(item: Item, anchor: View)
|
||||
}
|
||||
|
||||
/**
|
||||
* Like AsyncListDiffer, but synchronous. This may seem like it would be inefficient, but in
|
||||
* practice Auxio's lists tend to be small enough to the point where this does not matter, and
|
||||
|
@ -153,11 +127,3 @@ class SyncListDiffer<T>(
|
|||
currentList = newList
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A base [DiffUtil.ItemCallback] that automatically provides an implementation of
|
||||
* [areContentsTheSame] any object that is derived from [Item].
|
||||
*/
|
||||
abstract class SimpleItemCallback<T : Item> : DiffUtil.ItemCallback<T>() {
|
||||
final override fun areItemsTheSame(oldItem: T, newItem: T) = oldItem == newItem
|
||||
}
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.recycler
|
||||
package org.oxycblt.auxio.list.recycler
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
@ -24,6 +24,9 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.ItemHeaderBinding
|
||||
import org.oxycblt.auxio.databinding.ItemParentBinding
|
||||
import org.oxycblt.auxio.databinding.ItemSongBinding
|
||||
import org.oxycblt.auxio.list.Header
|
||||
import org.oxycblt.auxio.list.ItemSelectCallback
|
||||
import org.oxycblt.auxio.list.MenuItemListener
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -53,6 +56,21 @@ class SongViewHolder private constructor(private val binding: ItemSongBinding) :
|
|||
}
|
||||
}
|
||||
|
||||
fun bind(item: Song, callback: ItemSelectCallback) {
|
||||
binding.songAlbumCover.bind(item)
|
||||
binding.songName.text = item.resolveName(binding.context)
|
||||
binding.songInfo.text = item.resolveArtistContents(binding.context)
|
||||
|
||||
binding.songMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
||||
binding.root.apply {
|
||||
setOnClickListener { callback.onClick(item) }
|
||||
setOnLongClickListener {
|
||||
callback.onSelect(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.root.isSelected = isActive
|
||||
binding.songAlbumCover.isPlaying = isPlaying
|
||||
|
@ -97,6 +115,21 @@ class AlbumViewHolder private constructor(private val binding: ItemParentBinding
|
|||
}
|
||||
}
|
||||
|
||||
fun bind(item: Album, callback: ItemSelectCallback) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
binding.parentInfo.text = item.resolveArtistContents(binding.context)
|
||||
|
||||
binding.parentMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
||||
binding.root.apply {
|
||||
setOnClickListener { callback.onClick(item) }
|
||||
setOnLongClickListener {
|
||||
callback.onSelect(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.root.isSelected = isActive
|
||||
binding.parentImage.isPlaying = isPlaying
|
||||
|
@ -153,6 +186,31 @@ class ArtistViewHolder private constructor(private val binding: ItemParentBindin
|
|||
}
|
||||
}
|
||||
|
||||
fun bind(item: Artist, callback: ItemSelectCallback) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
|
||||
binding.parentInfo.text =
|
||||
if (item.songs.isNotEmpty()) {
|
||||
binding.context.getString(
|
||||
R.string.fmt_two,
|
||||
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size),
|
||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
||||
} else {
|
||||
// Artist has no songs, only display an album count.
|
||||
binding.context.getPlural(R.plurals.fmt_album_count, item.albums.size)
|
||||
}
|
||||
|
||||
binding.parentMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
||||
binding.root.apply {
|
||||
setOnClickListener { callback.onClick(item) }
|
||||
setOnLongClickListener {
|
||||
callback.onSelect(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.root.isSelected = isActive
|
||||
binding.parentImage.isPlaying = isPlaying
|
||||
|
@ -203,6 +261,25 @@ class GenreViewHolder private constructor(private val binding: ItemParentBinding
|
|||
}
|
||||
}
|
||||
|
||||
fun bind(item: Genre, callback: ItemSelectCallback) {
|
||||
binding.parentImage.bind(item)
|
||||
binding.parentName.text = item.resolveName(binding.context)
|
||||
binding.parentInfo.text =
|
||||
binding.context.getString(
|
||||
R.string.fmt_two,
|
||||
binding.context.getPlural(R.plurals.fmt_artist_count, item.artists.size),
|
||||
binding.context.getPlural(R.plurals.fmt_song_count, item.songs.size))
|
||||
|
||||
binding.parentMenu.setOnClickListener { callback.onOpenMenu(item, it) }
|
||||
binding.root.apply {
|
||||
setOnClickListener { callback.onClick(item) }
|
||||
setOnLongClickListener {
|
||||
callback.onSelect(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updatePlayingIndicator(isActive: Boolean, isPlaying: Boolean) {
|
||||
binding.root.isSelected = isActive
|
||||
binding.parentImage.isPlaying = isPlaying
|
|
@ -29,6 +29,7 @@ import kotlin.math.max
|
|||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.music.extractor.parseId3GenreNames
|
||||
import org.oxycblt.auxio.music.extractor.parseMultiValue
|
||||
import org.oxycblt.auxio.music.extractor.toUuidOrNull
|
||||
|
@ -38,7 +39,6 @@ import org.oxycblt.auxio.music.storage.Path
|
|||
import org.oxycblt.auxio.music.storage.albumCoverUri
|
||||
import org.oxycblt.auxio.music.storage.audioUri
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.nonZeroOrNull
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
|
|
|
@ -124,12 +124,9 @@ class MusicStore private constructor() {
|
|||
|
||||
// We are weirdly limited to DISPLAY_NAME and SIZE when trying to locate a
|
||||
// song. Do what we can to hopefully find the song the user wanted to open.
|
||||
|
||||
val displayName =
|
||||
cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||
|
||||
val size = cursor.getLong(cursor.getColumnIndexOrThrow(OpenableColumns.SIZE))
|
||||
|
||||
songs.find { it.path.name == displayName && it.size == size }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import com.google.android.material.checkbox.MaterialCheckBox
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogSeparatorsBinding
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.context
|
||||
|
||||
class SeparatorsDialog : ViewBindingDialogFragment<DialogSeparatorsBinding>() {
|
||||
|
|
|
@ -21,14 +21,14 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.databinding.ItemPickerChoiceBinding
|
||||
import org.oxycblt.auxio.list.ItemClickCallback
|
||||
import org.oxycblt.auxio.list.recycler.DialogViewHolder
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.ui.recycler.DialogViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.ItemClickListener
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
/** The adapter that displays a list of artist choices in the picker UI. */
|
||||
class ArtistChoiceAdapter(private val listener: ItemClickListener) :
|
||||
class ArtistChoiceAdapter(private val callback: ItemClickCallback) :
|
||||
RecyclerView.Adapter<ArtistChoiceViewHolder>() {
|
||||
private var artists = listOf<Artist>()
|
||||
|
||||
|
@ -38,7 +38,7 @@ class ArtistChoiceAdapter(private val listener: ItemClickListener) :
|
|||
ArtistChoiceViewHolder.new(parent)
|
||||
|
||||
override fun onBindViewHolder(holder: ArtistChoiceViewHolder, position: Int) =
|
||||
holder.bind(artists[position], listener)
|
||||
holder.bind(artists[position], callback)
|
||||
|
||||
fun submitList(newArtists: List<Artist>) {
|
||||
if (newArtists != artists) {
|
||||
|
@ -55,10 +55,10 @@ class ArtistChoiceAdapter(private val listener: ItemClickListener) :
|
|||
*/
|
||||
class ArtistChoiceViewHolder(private val binding: ItemPickerChoiceBinding) :
|
||||
DialogViewHolder(binding.root) {
|
||||
fun bind(artist: Artist, listener: ItemClickListener) {
|
||||
fun bind(artist: Artist, callback: ItemClickCallback) {
|
||||
binding.pickerImage.bind(artist)
|
||||
binding.pickerName.text = artist.resolveName(binding.context)
|
||||
binding.root.setOnClickListener { listener.onItemClick(artist) }
|
||||
binding.root.setOnClickListener { callback.onClick(artist) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -21,9 +21,9 @@ import android.os.Bundle
|
|||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
||||
|
||||
/**
|
||||
* The [ArtistPickerDialog] for ambiguous artist navigation operations.
|
||||
|
@ -31,6 +31,7 @@ import org.oxycblt.auxio.ui.recycler.Item
|
|||
*/
|
||||
class ArtistNavigationPickerDialog : ArtistPickerDialog() {
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
|
||||
private val args: ArtistNavigationPickerDialogArgs by navArgs()
|
||||
|
||||
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
|
||||
|
@ -38,9 +39,9 @@ class ArtistNavigationPickerDialog : ArtistPickerDialog() {
|
|||
super.onBindingCreated(binding, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
super.onItemClick(item)
|
||||
check(item is Music) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
override fun onChoiceConfirmed(item: Item) {
|
||||
super.onChoiceConfirmed(item)
|
||||
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
navModel.exploreNavigateTo(item)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,15 +24,15 @@ import androidx.fragment.app.viewModels
|
|||
import androidx.navigation.fragment.findNavController
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.ItemClickListener
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.ItemClickCallback
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
|
||||
abstract class ArtistPickerDialog :
|
||||
ViewBindingDialogFragment<DialogMusicPickerBinding>(), ItemClickListener {
|
||||
abstract class ArtistPickerDialog : ViewBindingDialogFragment<DialogMusicPickerBinding>() {
|
||||
protected val pickerModel: MusicPickerViewModel by viewModels()
|
||||
private val artistAdapter = ArtistChoiceAdapter(this)
|
||||
private val artistAdapter = ArtistChoiceAdapter(ItemClickCallback(::onChoiceConfirmed))
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
DialogMusicPickerBinding.inflate(inflater)
|
||||
|
@ -43,6 +43,7 @@ abstract class ArtistPickerDialog :
|
|||
|
||||
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
|
||||
binding.pickerRecycler.adapter = artistAdapter
|
||||
|
||||
collectImmediately(pickerModel.currentArtists) { artists ->
|
||||
if (!artists.isNullOrEmpty()) {
|
||||
artistAdapter.submitList(artists)
|
||||
|
@ -56,7 +57,8 @@ abstract class ArtistPickerDialog :
|
|||
binding.pickerRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
open fun onChoiceConfirmed(item: Item) {
|
||||
check(item is Artist) { "Unexpected datatype: ${item::class.java}" }
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ package org.oxycblt.auxio.music.picker
|
|||
import android.os.Bundle
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.databinding.DialogMusicPickerBinding
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
|
||||
/**
|
||||
|
@ -31,6 +31,7 @@ import org.oxycblt.auxio.util.androidActivityViewModels
|
|||
*/
|
||||
class ArtistPlaybackPickerDialog : ArtistPickerDialog() {
|
||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||
|
||||
private val args: ArtistPlaybackPickerDialogArgs by navArgs()
|
||||
|
||||
override fun onBindingCreated(binding: DialogMusicPickerBinding, savedInstanceState: Bundle?) {
|
||||
|
@ -38,8 +39,8 @@ class ArtistPlaybackPickerDialog : ArtistPickerDialog() {
|
|||
super.onBindingCreated(binding, savedInstanceState)
|
||||
}
|
||||
|
||||
override fun onItemClick(item: Item) {
|
||||
super.onItemClick(item)
|
||||
override fun onChoiceConfirmed(item: Item) {
|
||||
super.onChoiceConfirmed(item)
|
||||
check(item is Artist) { "Unexpected datatype: ${item::class.simpleName}" }
|
||||
pickerModel.currentSong.value?.let { song -> playbackModel.playFromArtist(song, item) }
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.databinding.ItemMusicDirBinding
|
||||
import org.oxycblt.auxio.ui.recycler.DialogViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.DialogViewHolder
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -59,7 +59,8 @@ class MusicDirsDialog :
|
|||
.setPositiveButton(R.string.lbl_save) { _, _ ->
|
||||
val dirs = settings.getMusicDirs(storageManager)
|
||||
val newDirs =
|
||||
MusicDirs(dirs = dirAdapter.dirs, shouldInclude = isInclude(requireBinding()))
|
||||
MusicDirs(
|
||||
dirs = dirAdapter.dirs, shouldInclude = isUiModeInclude(requireBinding()))
|
||||
if (dirs != newDirs) {
|
||||
logD("Committing changes")
|
||||
settings.setMusicDirs(newDirs)
|
||||
|
@ -122,7 +123,7 @@ class MusicDirsDialog :
|
|||
super.onSaveInstanceState(outState)
|
||||
outState.putStringArrayList(
|
||||
KEY_PENDING_DIRS, ArrayList(dirAdapter.dirs.map { it.toString() }))
|
||||
outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding()))
|
||||
outState.putBoolean(KEY_PENDING_MODE, isUiModeInclude(requireBinding()))
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: DialogMusicDirsBinding) {
|
||||
|
@ -166,14 +167,14 @@ class MusicDirsDialog :
|
|||
|
||||
private fun updateMode() {
|
||||
val binding = requireBinding()
|
||||
if (isInclude(binding)) {
|
||||
if (isUiModeInclude(binding)) {
|
||||
binding.dirsModeDesc.setText(R.string.set_dirs_mode_include_desc)
|
||||
} else {
|
||||
binding.dirsModeDesc.setText(R.string.set_dirs_mode_exclude_desc)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isInclude(binding: DialogMusicDirsBinding) =
|
||||
private fun isUiModeInclude(binding: DialogMusicDirsBinding) =
|
||||
binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -23,7 +23,7 @@ import androidx.core.app.NotificationCompat
|
|||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.ui.system.ServiceNotification
|
||||
import org.oxycblt.auxio.shared.ServiceNotification
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.system.ForegroundManager
|
||||
import org.oxycblt.auxio.shared.ForegroundManager
|
||||
import org.oxycblt.auxio.util.contentResolverSafe
|
||||
import org.oxycblt.auxio.util.getSystemServiceCompat
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
|
|
@ -25,9 +25,9 @@ import org.oxycblt.auxio.databinding.FragmentPlaybackBarBinding
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||
import org.oxycblt.auxio.shared.MainNavigationAction
|
||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||
|
@ -63,16 +63,29 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
|||
binding.playbackSong.isSelected = true
|
||||
binding.playbackInfo.isSelected = true
|
||||
|
||||
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }
|
||||
setupSecondaryActions(binding, Settings(context))
|
||||
|
||||
// Load the track color in manually as it's unclear whether the track actually supports
|
||||
// using a ColorStateList in the resources
|
||||
binding.playbackProgressBar.trackColor =
|
||||
context.getColorCompat(R.color.sel_track).defaultColor
|
||||
|
||||
binding.playbackPlayPause.setOnClickListener { playbackModel.invertPlaying() }
|
||||
// -- VIEWMODEL SETUP ---
|
||||
|
||||
// Update the secondary action to match the setting.
|
||||
collectImmediately(playbackModel.song, ::updateSong)
|
||||
collectImmediately(playbackModel.isPlaying, ::updatePlaying)
|
||||
collectImmediately(playbackModel.positionDs, ::updatePosition)
|
||||
}
|
||||
|
||||
when (Settings(context).actionMode) {
|
||||
override fun onDestroyBinding(binding: FragmentPlaybackBarBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.playbackSong.isSelected = false
|
||||
binding.playbackInfo.isSelected = false
|
||||
}
|
||||
|
||||
private fun setupSecondaryActions(binding: FragmentPlaybackBarBinding, settings: Settings) {
|
||||
when (settings.actionMode) {
|
||||
ActionMode.NEXT -> {
|
||||
binding.playbackSecondaryAction.apply {
|
||||
setIconResource(R.drawable.ic_skip_next_24)
|
||||
|
@ -99,18 +112,6 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- VIEWMODEL SETUP ---
|
||||
|
||||
collectImmediately(playbackModel.song, ::updateSong)
|
||||
collectImmediately(playbackModel.isPlaying, ::updateIsPlaying)
|
||||
collectImmediately(playbackModel.positionDs, ::updatePosition)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentPlaybackBarBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.playbackSong.isSelected = false
|
||||
binding.playbackInfo.isSelected = false
|
||||
}
|
||||
|
||||
private fun updateSong(song: Song?) {
|
||||
|
@ -124,7 +125,7 @@ class PlaybackBarFragment : ViewBindingFragment<FragmentPlaybackBarBinding>() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateIsPlaying(isPlaying: Boolean) {
|
||||
private fun updatePlaying(isPlaying: Boolean) {
|
||||
requireBinding().playbackPlayPause.isActivated = isPlaying
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ import android.view.View
|
|||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.ui.AuxioSheetBehavior
|
||||
import org.oxycblt.auxio.shared.AuxioBottomSheetBehavior
|
||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||
import org.oxycblt.auxio.util.getDimen
|
||||
|
||||
|
@ -34,8 +34,8 @@ import org.oxycblt.auxio.util.getDimen
|
|||
* to make bottom sheets like this work.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class PlaybackSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
|
||||
AuxioSheetBehavior<V>(context, attributeSet) {
|
||||
class PlaybackBottomSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
|
||||
AuxioBottomSheetBehavior<V>(context, attributeSet) {
|
||||
val sheetBackgroundDrawable =
|
||||
MaterialShapeDrawable.createWithElevationOverlay(context).apply {
|
||||
fillColor = context.getAttrColorCompat(R.attr.colorSurface)
|
|
@ -24,18 +24,19 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import org.oxycblt.auxio.MainFragmentDirections
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentPlaybackPanelBinding
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.playback.ui.StyledSeekBar
|
||||
import org.oxycblt.auxio.ui.MainNavigationAction
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.shared.MainNavigationAction
|
||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
import org.oxycblt.auxio.util.systemBarInsetsCompat
|
||||
|
@ -47,10 +48,10 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
|||
*
|
||||
* TODO: Make seek thumb grow when selected
|
||||
*/
|
||||
class PlaybackPanelFragment :
|
||||
MenuFragment<FragmentPlaybackPanelBinding>(),
|
||||
StyledSeekBar.Callback,
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
class PlaybackPanelFragment : ViewBindingFragment<FragmentPlaybackPanelBinding>() {
|
||||
private val playbackModel: PlaybackViewModel by androidActivityViewModels()
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
|
||||
// AudioEffect expects you to use startActivityForResult with the panel intent. Use
|
||||
// the contract analogue for this since there is no built-in contract for AudioEffect.
|
||||
private val activityLauncher by lifecycleObject {
|
||||
|
@ -76,7 +77,10 @@ class PlaybackPanelFragment :
|
|||
|
||||
binding.playbackToolbar.apply {
|
||||
setNavigationOnClickListener { navModel.mainNavigateTo(MainNavigationAction.Collapse) }
|
||||
setOnMenuItemClickListener(this@PlaybackPanelFragment)
|
||||
setOnMenuItemClickListener {
|
||||
handleMenuItem(it)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure we enable marquee on the song info
|
||||
|
@ -96,7 +100,7 @@ class PlaybackPanelFragment :
|
|||
setOnClickListener { playbackModel.song.value?.let { showCurrentAlbum() } }
|
||||
}
|
||||
|
||||
binding.playbackSeekBar.callback = this
|
||||
binding.playbackSeekBar.onSeekConfirmed = playbackModel::seekTo
|
||||
|
||||
binding.playbackRepeat.setOnClickListener { playbackModel.incrementRepeatMode() }
|
||||
binding.playbackSkipPrev.setOnClickListener { playbackModel.prev() }
|
||||
|
@ -115,18 +119,14 @@ class PlaybackPanelFragment :
|
|||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentPlaybackPanelBinding) {
|
||||
binding.playbackToolbar.setOnMenuItemClickListener(null)
|
||||
|
||||
// Leaving marquee on will cause a leak
|
||||
binding.playbackSong.isSelected = false
|
||||
binding.playbackArtist.isSelected = false
|
||||
binding.playbackAlbum.isSelected = false
|
||||
|
||||
binding.playbackSeekBar.callback = null
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
private fun handleMenuItem(item: MenuItem) {
|
||||
when (item.itemId) {
|
||||
R.id.action_open_equalizer -> {
|
||||
val equalizerIntent =
|
||||
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL)
|
||||
|
@ -139,16 +139,12 @@ class PlaybackPanelFragment :
|
|||
} catch (e: ActivityNotFoundException) {
|
||||
requireContext().showToast(R.string.err_no_app)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
R.id.action_go_artist -> {
|
||||
showCurrentArtist()
|
||||
true
|
||||
}
|
||||
R.id.action_go_album -> {
|
||||
showCurrentAlbum()
|
||||
true
|
||||
}
|
||||
R.id.action_song_detail -> {
|
||||
playbackModel.song.value?.let { song ->
|
||||
|
@ -156,17 +152,10 @@ class PlaybackPanelFragment :
|
|||
MainNavigationAction.Directions(
|
||||
MainFragmentDirections.actionShowDetails(song.uid)))
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun seekTo(positionDs: Long) {
|
||||
playbackModel.seekTo(positionDs)
|
||||
}
|
||||
|
||||
private fun updateSong(song: Song?) {
|
||||
if (song == null) return
|
||||
val binding = requireBinding()
|
||||
|
@ -208,6 +197,7 @@ class PlaybackPanelFragment :
|
|||
val song = playbackModel.song.value ?: return
|
||||
navModel.exploreNavigateTo(song.artists)
|
||||
}
|
||||
|
||||
private fun showCurrentAlbum() {
|
||||
val song = playbackModel.song.value ?: return
|
||||
navModel.exploreNavigateTo(song.album)
|
||||
|
|
|
@ -27,10 +27,10 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemQueueSongBinding
|
||||
import org.oxycblt.auxio.list.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.list.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.list.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.recycler.PlayingIndicatorAdapter
|
||||
import org.oxycblt.auxio.ui.recycler.SongViewHolder
|
||||
import org.oxycblt.auxio.ui.recycler.SyncListDiffer
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||
import org.oxycblt.auxio.util.getDimen
|
||||
|
|
|
@ -24,7 +24,7 @@ import android.view.WindowInsets
|
|||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.ui.AuxioSheetBehavior
|
||||
import org.oxycblt.auxio.shared.AuxioBottomSheetBehavior
|
||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||
import org.oxycblt.auxio.util.getDimen
|
||||
import org.oxycblt.auxio.util.getDimenSize
|
||||
|
@ -35,8 +35,8 @@ import org.oxycblt.auxio.util.systemBarInsetsCompat
|
|||
* The bottom sheet behavior designed for the queue in particular.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class QueueSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
|
||||
AuxioSheetBehavior<V>(context, attributeSet) {
|
||||
class QueueBottomSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
|
||||
AuxioBottomSheetBehavior<V>(context, attributeSet) {
|
||||
private var barHeight = 0
|
||||
private var barSpacing = context.getDimenSize(R.dimen.spacing_small)
|
||||
|
|
@ -19,7 +19,6 @@ package org.oxycblt.auxio.playback.queue
|
|||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
|
@ -28,7 +27,7 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import org.oxycblt.auxio.databinding.FragmentQueueBinding
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -52,16 +51,6 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
|||
binding.queueRecycler.apply {
|
||||
adapter = queueAdapter
|
||||
touchHelper.attachToRecyclerView(this)
|
||||
|
||||
// Sometimes the scroll can change without the listener being updated, so we also
|
||||
// check for relayout events.
|
||||
addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> invalidateDivider() }
|
||||
addOnScrollListener(
|
||||
object : RecyclerView.OnScrollListener() {
|
||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||
invalidateDivider()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- VIEWMODEL SETUP ----
|
||||
|
@ -95,10 +84,6 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
|||
queueAdapter.submitList(queue)
|
||||
}
|
||||
|
||||
binding.queueDivider.isInvisible =
|
||||
(binding.queueRecycler.layoutManager as LinearLayoutManager)
|
||||
.findFirstCompletelyVisibleItemPosition() < 1
|
||||
|
||||
queueModel.finishReplace()
|
||||
|
||||
val scrollTo = queueModel.scrollTo
|
||||
|
@ -117,11 +102,4 @@ class QueueFragment : ViewBindingFragment<FragmentQueueBinding>(), QueueItemList
|
|||
|
||||
queueAdapter.updateIndicator(index, isPlaying)
|
||||
}
|
||||
|
||||
private fun invalidateDivider() {
|
||||
val binding = requireBinding()
|
||||
binding.queueDivider.isInvisible =
|
||||
(binding.queueRecycler.layoutManager as LinearLayoutManager)
|
||||
.findFirstCompletelyVisibleItemPosition() < 1
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@ import kotlin.math.abs
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogPreAmpBinding
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.context
|
||||
|
||||
/**
|
||||
|
|
|
@ -29,7 +29,7 @@ import org.oxycblt.auxio.BuildConfig
|
|||
import org.oxycblt.auxio.IntegerTable
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.ui.system.ServiceNotification
|
||||
import org.oxycblt.auxio.shared.ServiceNotification
|
||||
import org.oxycblt.auxio.util.newBroadcastPendingIntent
|
||||
import org.oxycblt.auxio.util.newMainPendingIntent
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ import org.oxycblt.auxio.playback.state.PlaybackStateDatabase
|
|||
import org.oxycblt.auxio.playback.state.PlaybackStateManager
|
||||
import org.oxycblt.auxio.playback.state.RepeatMode
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.system.ForegroundManager
|
||||
import org.oxycblt.auxio.shared.ForegroundManager
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.widgets.WidgetComponent
|
||||
import org.oxycblt.auxio.widgets.WidgetProvider
|
||||
|
|
|
@ -45,7 +45,7 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
|||
binding.seekBarSlider.addOnChangeListener(this)
|
||||
}
|
||||
|
||||
var callback: Callback? = null
|
||||
var onSeekConfirmed: ((Long) -> Unit)? = null
|
||||
|
||||
/**
|
||||
* The current position, in seconds. This is the current value of the SeekBar and is indicated
|
||||
|
@ -104,17 +104,10 @@ constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
|||
logD("Confirming seek")
|
||||
// End of seek event, send off new value to callback.
|
||||
isActivated = false
|
||||
callback?.seekTo(slider.value.toLong())
|
||||
onSeekConfirmed?.invoke(slider.value.toLong())
|
||||
}
|
||||
|
||||
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
|
||||
binding.seekBarPosition.text = value.toLong().formatDurationDs(true)
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
/**
|
||||
* Called when a seek event was completed and the new position must be seeked to by the app.
|
||||
*/
|
||||
fun seekTo(positionDs: Long)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,13 +20,14 @@ package org.oxycblt.auxio.search
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.list.*
|
||||
import org.oxycblt.auxio.list.recycler.*
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.recycler.*
|
||||
|
||||
class SearchAdapter(private val listener: MenuItemListener) :
|
||||
class SearchAdapter(private val callback: ItemSelectCallback) :
|
||||
SelectionIndicatorAdapter<RecyclerView.ViewHolder>(), AuxioRecyclerView.SpanSizeLookup {
|
||||
private val differ = AsyncListDiffer(this, DIFFER)
|
||||
|
||||
|
@ -61,10 +62,10 @@ class SearchAdapter(private val listener: MenuItemListener) :
|
|||
|
||||
if (payloads.isEmpty()) {
|
||||
when (val item = differ.currentList[position]) {
|
||||
is Song -> (holder as SongViewHolder).bind(item, listener)
|
||||
is Album -> (holder as AlbumViewHolder).bind(item, listener)
|
||||
is Artist -> (holder as ArtistViewHolder).bind(item, listener)
|
||||
is Genre -> (holder as GenreViewHolder).bind(item, listener)
|
||||
is Song -> (holder as SongViewHolder).bind(item, callback)
|
||||
is Album -> (holder as AlbumViewHolder).bind(item, callback)
|
||||
is Artist -> (holder as ArtistViewHolder).bind(item, callback)
|
||||
is Genre -> (holder as GenreViewHolder).bind(item, callback)
|
||||
is Header -> (holder as HeaderViewHolder).bind(item)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,16 +22,17 @@ import android.view.LayoutInflater
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.view.isInvisible
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.transition.MaterialSharedAxis
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.ItemSelectCallback
|
||||
import org.oxycblt.auxio.list.SelectionFragment
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -40,26 +41,20 @@ import org.oxycblt.auxio.music.MusicMode
|
|||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.MenuFragment
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.ui.recycler.MenuItemListener
|
||||
import org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
|
||||
import org.oxycblt.auxio.ui.selection.SelectionViewModel
|
||||
import org.oxycblt.auxio.util.*
|
||||
|
||||
/**
|
||||
* A [Fragment] that allows for the searching of the entire music library.
|
||||
* FIXME: Keyboard logic is really wonky
|
||||
* A [Fragment] that allows for the searching of the entire music library. TODO: Minor rework with
|
||||
* better keyboard logic, recycler updating, and chips
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class SearchFragment :
|
||||
MenuFragment<FragmentSearchBinding>(), MenuItemListener, Toolbar.OnMenuItemClickListener, SelectionToolbarOverlay.Callback {
|
||||
class SearchFragment : SelectionFragment<FragmentSearchBinding>() {
|
||||
|
||||
// SearchViewModel is only scoped to this Fragment
|
||||
private val searchModel: SearchViewModel by androidViewModels()
|
||||
private val selectionModel: SelectionViewModel by activityViewModels()
|
||||
|
||||
private val searchAdapter = SearchAdapter(this)
|
||||
private val searchAdapter =
|
||||
SearchAdapter(ItemSelectCallback(::handleClick, ::handleOpenMenu, ::handleSelect))
|
||||
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
|
||||
private val imm: InputMethodManager by lifecycleObject { binding ->
|
||||
binding.context.getSystemServiceCompat(InputMethodManager::class)
|
||||
|
@ -78,7 +73,7 @@ class SearchFragment :
|
|||
override fun onCreateBinding(inflater: LayoutInflater) = FragmentSearchBinding.inflate(inflater)
|
||||
|
||||
override fun onBindingCreated(binding: FragmentSearchBinding, savedInstanceState: Bundle?) {
|
||||
binding.searchToolbarOverlay.callback = this
|
||||
setupOverlay(binding.searchToolbarOverlay)
|
||||
|
||||
binding.searchToolbar.apply {
|
||||
val itemIdToSelect =
|
||||
|
@ -92,17 +87,11 @@ class SearchFragment :
|
|||
|
||||
menu.findItem(itemIdToSelect).isChecked = true
|
||||
|
||||
setNavigationOnClickListener {
|
||||
// Reset selection (navigating to another selectable screen)
|
||||
selectionModel.consume()
|
||||
|
||||
// Drop keyboard as it's no longer needed
|
||||
imm.hide()
|
||||
|
||||
findNavController().navigateUp()
|
||||
setNavigationOnClickListener { handleSearchNavigateUp() }
|
||||
setOnMenuItemClickListener {
|
||||
handleSearchMenuItem(it)
|
||||
true
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener(this@SearchFragment)
|
||||
}
|
||||
|
||||
binding.searchEditText.apply {
|
||||
|
@ -113,9 +102,7 @@ class SearchFragment :
|
|||
|
||||
if (!launchedKeyboard) {
|
||||
// Auto-open the keyboard when this view is shown
|
||||
requestFocus()
|
||||
postDelayed(200) { imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) }
|
||||
|
||||
imm.show(this)
|
||||
launchedKeyboard = true
|
||||
}
|
||||
}
|
||||
|
@ -124,76 +111,58 @@ class SearchFragment :
|
|||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
collectImmediately(searchModel.searchResults, ::handleResults)
|
||||
collectImmediately(searchModel.searchResults, ::updateResults)
|
||||
|
||||
collectImmediately(
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::handlePlayback)
|
||||
playbackModel.song, playbackModel.parent, playbackModel.isPlaying, ::updatePlayback)
|
||||
|
||||
collect(navModel.exploreNavigationItem, ::handleNavigation)
|
||||
collectImmediately(selectionModel.selected, ::handleSelection)
|
||||
collectImmediately(selectionModel.selected, ::updateSelection)
|
||||
}
|
||||
|
||||
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
||||
binding.searchToolbarOverlay.callback = null
|
||||
binding.searchToolbar.setOnMenuItemClickListener(null)
|
||||
binding.searchRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
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) {
|
||||
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) {
|
||||
check(item is Music) { "Unexpected datatype ${item::class.simpleName}"}
|
||||
selectionModel.select(item)
|
||||
}
|
||||
|
||||
override fun onOpenMenu(item: Item, anchor: View) {
|
||||
private fun handleOpenMenu(item: Item, anchor: View) {
|
||||
when (item) {
|
||||
is Song -> musicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
is Album -> musicMenu(anchor, R.menu.menu_album_actions, item)
|
||||
is Artist -> musicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
is Genre -> musicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
is Song -> openMusicMenu(anchor, R.menu.menu_song_actions, item)
|
||||
is Album -> openMusicMenu(anchor, R.menu.menu_album_actions, item)
|
||||
is Artist -> openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
is Genre -> openMusicMenu(anchor, R.menu.menu_artist_actions, item)
|
||||
else -> logW("Unexpected datatype when opening menu: ${item::class.java}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleResults(results: List<Item>) {
|
||||
private fun updateResults(results: List<Item>) {
|
||||
val binding = requireBinding()
|
||||
|
||||
searchAdapter.submitList(results.toMutableList()) {
|
||||
|
@ -206,20 +175,21 @@ class SearchFragment :
|
|||
binding.searchRecycler.isInvisible = results.isEmpty()
|
||||
}
|
||||
|
||||
private fun handlePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
private fun updatePlayback(song: Song?, parent: MusicParent?, isPlaying: Boolean) {
|
||||
searchAdapter.updateIndicator(parent ?: song, isPlaying)
|
||||
}
|
||||
|
||||
private fun handleNavigation(item: Music?) {
|
||||
findNavController()
|
||||
.navigate(
|
||||
when (item) {
|
||||
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.uid)
|
||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.uid)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.uid)
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.uid)
|
||||
else -> return
|
||||
})
|
||||
val action =
|
||||
when (item) {
|
||||
is Song -> SearchFragmentDirections.actionShowAlbum(item.album.uid)
|
||||
is Album -> SearchFragmentDirections.actionShowAlbum(item.uid)
|
||||
is Artist -> SearchFragmentDirections.actionShowArtist(item.uid)
|
||||
is Genre -> SearchFragmentDirections.actionShowGenre(item.uid)
|
||||
else -> return
|
||||
}
|
||||
|
||||
findNavController().navigate(action)
|
||||
|
||||
// Reset selection (navigating to another selectable screen)
|
||||
selectionModel.consume()
|
||||
|
@ -228,13 +198,21 @@ class SearchFragment :
|
|||
imm.hide()
|
||||
}
|
||||
|
||||
private fun handleSelection(selected: List<Music>) {
|
||||
searchAdapter.updateSelection(selected)
|
||||
if (requireBinding().searchToolbarOverlay.updateSelectionAmount(selected.size) && selected.isNotEmpty()) {
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
searchAdapter.setSelected(selected)
|
||||
if (requireBinding().searchToolbarOverlay.updateSelectionAmount(selected.size) &&
|
||||
selected.isNotEmpty()) {
|
||||
imm.hide()
|
||||
}
|
||||
}
|
||||
|
||||
private fun InputMethodManager.show(view: View) {
|
||||
view.apply {
|
||||
requestFocus()
|
||||
postDelayed(200) { showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun InputMethodManager.hide() {
|
||||
hideSoftInputFromWindow(requireView().windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
}
|
||||
|
|
|
@ -30,6 +30,8 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.list.Header
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -39,8 +41,6 @@ import org.oxycblt.auxio.music.MusicStore
|
|||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.Sort
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.recycler.Header
|
||||
import org.oxycblt.auxio.ui.recycler.Item
|
||||
import org.oxycblt.auxio.util.application
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ import org.oxycblt.auxio.R
|
|||
import org.oxycblt.auxio.databinding.FragmentAboutBinding
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
||||
import org.oxycblt.auxio.util.collectImmediately
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.showToast
|
||||
|
|
|
@ -35,7 +35,7 @@ import org.oxycblt.auxio.music.storage.MusicDirs
|
|||
import org.oxycblt.auxio.playback.ActionMode
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
|
||||
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
|
||||
import org.oxycblt.auxio.ui.accent.Accent
|
||||
import org.oxycblt.auxio.settings.accent.Accent
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.navigation.fragment.findNavController
|
||||
import com.google.android.material.transition.MaterialFadeThrough
|
||||
import org.oxycblt.auxio.databinding.FragmentSettingsBinding
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingFragment
|
||||
import org.oxycblt.auxio.shared.ViewBindingFragment
|
||||
|
||||
/**
|
||||
* A container [Fragment] for the settings menu.
|
||||
|
|
|
@ -15,10 +15,11 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.accent
|
||||
package org.oxycblt.auxio.settings.accent
|
||||
|
||||
import android.os.Build
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.util.logW
|
||||
|
||||
private val ACCENT_NAMES =
|
||||
|
@ -114,7 +115,7 @@ private val ACCENT_PRIMARY_COLORS =
|
|||
* @property primary The primary color resource for this accent
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class Accent private constructor(val index: Int) {
|
||||
class Accent private constructor(val index: Int) : Item {
|
||||
val name: Int
|
||||
get() = ACCENT_NAMES[index]
|
||||
val theme: Int
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.accent
|
||||
package org.oxycblt.auxio.settings.accent
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -23,6 +23,7 @@ import androidx.appcompat.widget.TooltipCompat
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.ItemAccentBinding
|
||||
import org.oxycblt.auxio.list.ItemClickCallback
|
||||
import org.oxycblt.auxio.util.getAttrColorCompat
|
||||
import org.oxycblt.auxio.util.getColorCompat
|
||||
import org.oxycblt.auxio.util.inflater
|
||||
|
@ -31,7 +32,8 @@ import org.oxycblt.auxio.util.inflater
|
|||
* An adapter that displays the accent palette.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<AccentViewHolder>() {
|
||||
class AccentAdapter(private val callback: ItemClickCallback) :
|
||||
RecyclerView.Adapter<AccentViewHolder>() {
|
||||
var selectedAccent: Accent? = null
|
||||
private set
|
||||
|
||||
|
@ -50,7 +52,7 @@ class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<Accen
|
|||
val item = Accent.from(position)
|
||||
|
||||
if (payloads.isEmpty()) {
|
||||
holder.bind(item, listener)
|
||||
holder.bind(item, callback)
|
||||
}
|
||||
|
||||
holder.setSelected(item == selectedAccent)
|
||||
|
@ -63,10 +65,6 @@ class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<Accen
|
|||
notifyItemChanged(accent.index, PAYLOAD_SELECTION_CHANGED)
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onAccentSelected(accent: Accent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
val PAYLOAD_SELECTION_CHANGED = Any()
|
||||
}
|
||||
|
@ -75,14 +73,14 @@ class AccentAdapter(private val listener: Listener) : RecyclerView.Adapter<Accen
|
|||
class AccentViewHolder private constructor(private val binding: ItemAccentBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
fun bind(item: Accent, listener: AccentAdapter.Listener) {
|
||||
fun bind(item: Accent, callback: ItemClickCallback) {
|
||||
setSelected(false)
|
||||
|
||||
binding.accent.apply {
|
||||
backgroundTintList = context.getColorCompat(item.primary)
|
||||
contentDescription = context.getString(item.name)
|
||||
TooltipCompat.setTooltipText(this, contentDescription)
|
||||
setOnClickListener { listener.onAccentSelected(item) }
|
||||
setOnClickListener { callback.onClick(item) }
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.accent
|
||||
package org.oxycblt.auxio.settings.accent
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
@ -23,8 +23,10 @@ import androidx.appcompat.app.AlertDialog
|
|||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogAccentBinding
|
||||
import org.oxycblt.auxio.list.Item
|
||||
import org.oxycblt.auxio.list.ItemClickCallback
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.ui.fragment.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.shared.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.context
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
@ -33,9 +35,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* Dialog responsible for showing the list of accents to select.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class AccentCustomizeDialog :
|
||||
ViewBindingDialogFragment<DialogAccentBinding>(), AccentAdapter.Listener {
|
||||
private var accentAdapter = AccentAdapter(this)
|
||||
class AccentCustomizeDialog : ViewBindingDialogFragment<DialogAccentBinding>() {
|
||||
private var accentAdapter = AccentAdapter(ItemClickCallback(::handleClick))
|
||||
private val settings: Settings by lifecycleObject { binding -> Settings(binding.context) }
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) = DialogAccentBinding.inflate(inflater)
|
||||
|
@ -74,8 +75,9 @@ class AccentCustomizeDialog :
|
|||
binding.accentRecycler.adapter = null
|
||||
}
|
||||
|
||||
override fun onAccentSelected(accent: Accent) {
|
||||
accentAdapter.setSelectedAccent(accent)
|
||||
private fun handleClick(item: Item) {
|
||||
check(item is Accent) { "Unexpected datatype: ${item::class.java}" }
|
||||
accentAdapter.setSelectedAccent(item)
|
||||
}
|
||||
|
||||
companion object {
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.accent
|
||||
package org.oxycblt.auxio.settings.accent
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
|
@ -35,7 +35,7 @@ import org.oxycblt.auxio.music.MusicViewModel
|
|||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.settings.Settings
|
||||
import org.oxycblt.auxio.settings.SettingsFragmentDirections
|
||||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.shared.NavigationViewModel
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
import org.oxycblt.auxio.util.isNight
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui
|
||||
package org.oxycblt.auxio.shared
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui
|
||||
package org.oxycblt.auxio.shared
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
|
@ -34,7 +34,7 @@ import org.oxycblt.auxio.util.systemGestureInsetsCompat
|
|||
* the vendored code because of course I have to) for normal use without absurd bugs.
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
abstract class AuxioSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
|
||||
abstract class AuxioBottomSheetBehavior<V : View>(context: Context, attributeSet: AttributeSet?) :
|
||||
NeoBottomSheetBehavior<V>(context, attributeSet) {
|
||||
private var setup = false
|
||||
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui
|
||||
package org.oxycblt.auxio.shared
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.system
|
||||
package org.oxycblt.auxio.shared
|
||||
|
||||
import android.app.Service
|
||||
import androidx.core.app.ServiceCompat
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui
|
||||
package org.oxycblt.auxio.shared
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.navigation.NavDirections
|
||||
|
@ -108,7 +108,7 @@ class NavigationViewModel : ViewModel() {
|
|||
/**
|
||||
* Represents the navigation options for the Main Fragment, which tends to be multiple layers above
|
||||
* normal fragments. This can be passed to [NavigationViewModel.mainNavigateTo] in order to
|
||||
* facilitate navigation without stupid fragment hacks.
|
||||
* facilitate navigation without workarounds..
|
||||
*/
|
||||
sealed class MainNavigationAction {
|
||||
/** Expand the playback panel. */
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.system
|
||||
package org.oxycblt.auxio.shared
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.StringRes
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.fragment
|
||||
package org.oxycblt.auxio.shared
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
|
@ -15,7 +15,7 @@
|
|||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.ui.fragment
|
||||
package org.oxycblt.auxio.shared
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
|
@ -12,7 +12,7 @@
|
|||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"
|
||||
app:layout_behavior="org.oxycblt.auxio.shared.BottomSheetContentBehavior"
|
||||
app:navGraph="@navigation/nav_explore"
|
||||
tools:layout="@layout/fragment_home" />
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
|||
android:id="@+id/playback_sheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="org.oxycblt.auxio.playback.PlaybackSheetBehavior">
|
||||
app:layout_behavior="org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/playback_bar_fragment"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.oxycblt.auxio.ui.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.oxycblt.auxio.list.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/accent_recycler"
|
||||
|
@ -7,6 +7,6 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/spacing_medium"
|
||||
android:paddingEnd="@dimen/spacing_medium"
|
||||
app:layoutManager="org.oxycblt.auxio.ui.accent.AccentGridLayoutManager"
|
||||
app:layoutManager="org.oxycblt.auxio.settings.accent.AccentGridLayoutManager"
|
||||
tools:itemCount="16"
|
||||
tools:listitem="@layout/item_accent" />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.oxycblt.auxio.ui.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.oxycblt.auxio.list.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/picker_recycler"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.oxycblt.auxio.ui.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.oxycblt.auxio.list.recycler.DialogRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/tab_recycler"
|
||||
style="@style/Widget.Auxio.RecyclerView.Linear"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
android:transitionGroup="true"
|
||||
tools:context=".settings.AboutFragment">
|
||||
|
||||
<org.oxycblt.auxio.ui.AuxioAppBarLayout
|
||||
<org.oxycblt.auxio.shared.AuxioAppBarLayout
|
||||
android:id="@+id/about_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
app:liftOnScroll="true">
|
||||
|
@ -21,7 +21,7 @@
|
|||
app:navigationIcon="@drawable/ic_back_24"
|
||||
app:title="@string/lbl_about" />
|
||||
|
||||
</org.oxycblt.auxio.ui.AuxioAppBarLayout>
|
||||
</org.oxycblt.auxio.shared.AuxioAppBarLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:id="@+id/about_contents"
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
</org.oxycblt.auxio.detail.DetailAppBarLayout>
|
||||
|
||||
<org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
|
||||
<org.oxycblt.auxio.list.recycler.AuxioRecyclerView
|
||||
android:id="@+id/detail_recycler"
|
||||
style="@style/Widget.Auxio.RecyclerView.Grid"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
android:background="?attr/colorSurface"
|
||||
android:transitionGroup="true">
|
||||
|
||||
<org.oxycblt.auxio.ui.AuxioAppBarLayout
|
||||
<org.oxycblt.auxio.shared.AuxioAppBarLayout
|
||||
android:id="@+id/home_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
|
||||
<org.oxycblt.auxio.list.SelectionToolbarOverlay
|
||||
android:id="@+id/home_toolbar_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
@ -26,7 +26,7 @@
|
|||
app:menu="@menu/menu_home"
|
||||
app:title="@string/info_app_name" />
|
||||
|
||||
</org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay>
|
||||
</org.oxycblt.auxio.list.SelectionToolbarOverlay>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/home_tabs"
|
||||
|
@ -37,7 +37,7 @@
|
|||
app:tabGravity="start"
|
||||
app:tabMode="scrollable" />
|
||||
|
||||
</org.oxycblt.auxio.ui.AuxioAppBarLayout>
|
||||
</org.oxycblt.auxio.shared.AuxioAppBarLayout>
|
||||
|
||||
|
||||
<FrameLayout
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.oxycblt.auxio.ui.fastscroll.FastScrollRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<org.oxycblt.auxio.home.fastscroll.FastScrollRecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/home_recycler"
|
||||
style="@style/Widget.Auxio.RecyclerView.Grid.WithAdaptiveFab"
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior"
|
||||
app:layout_behavior="org.oxycblt.auxio.shared.BottomSheetContentBehavior"
|
||||
app:navGraph="@navigation/nav_explore"
|
||||
tools:layout="@layout/fragment_home" />
|
||||
|
||||
|
@ -22,7 +22,7 @@
|
|||
style="@style/Widget.Auxio.DisableDropShadows"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="org.oxycblt.auxio.playback.PlaybackSheetBehavior">
|
||||
app:layout_behavior="org.oxycblt.auxio.playback.PlaybackBottomSheetBehavior">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/playback_bar_fragment"
|
||||
|
@ -35,7 +35,7 @@
|
|||
android:name="org.oxycblt.auxio.playback.PlaybackPanelFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="org.oxycblt.auxio.ui.BottomSheetContentBehavior" />
|
||||
app:layout_behavior="org.oxycblt.auxio.shared.BottomSheetContentBehavior" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/queue_sheet"
|
||||
|
@ -43,7 +43,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
app:layout_behavior="org.oxycblt.auxio.playback.queue.QueueSheetBehavior">
|
||||
app:layout_behavior="org.oxycblt.auxio.playback.queue.QueueBottomSheetBehavior">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/handle_wrapper"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
|
||||
<org.oxycblt.auxio.list.recycler.AuxioRecyclerView
|
||||
android:id="@+id/queue_recycler"
|
||||
style="@style/Widget.Auxio.RecyclerView.Linear"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -7,12 +7,12 @@
|
|||
android:background="?attr/colorSurface"
|
||||
android:transitionGroup="true">
|
||||
|
||||
<org.oxycblt.auxio.ui.AuxioAppBarLayout
|
||||
<org.oxycblt.auxio.shared.AuxioAppBarLayout
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/search_recycler">
|
||||
|
||||
<org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay
|
||||
<org.oxycblt.auxio.list.SelectionToolbarOverlay
|
||||
android:id="@+id/search_toolbar_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
@ -49,11 +49,11 @@
|
|||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
</org.oxycblt.auxio.ui.selection.SelectionToolbarOverlay>
|
||||
</org.oxycblt.auxio.list.SelectionToolbarOverlay>
|
||||
|
||||
</org.oxycblt.auxio.ui.AuxioAppBarLayout>
|
||||
</org.oxycblt.auxio.shared.AuxioAppBarLayout>
|
||||
|
||||
<org.oxycblt.auxio.ui.recycler.AuxioRecyclerView
|
||||
<org.oxycblt.auxio.list.recycler.AuxioRecyclerView
|
||||
android:id="@+id/search_recycler"
|
||||
style="@style/Widget.Auxio.RecyclerView.Grid"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
android:orientation="vertical"
|
||||
android:transitionGroup="true">
|
||||
|
||||
<org.oxycblt.auxio.ui.AuxioAppBarLayout
|
||||
<org.oxycblt.auxio.shared.AuxioAppBarLayout
|
||||
android:id="@+id/settings_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout"
|
||||
android:clickable="true"
|
||||
|
@ -22,7 +22,7 @@
|
|||
app:navigationIcon="@drawable/ic_back_24"
|
||||
app:title="@string/set_title" />
|
||||
|
||||
</org.oxycblt.auxio.ui.AuxioAppBarLayout>
|
||||
</org.oxycblt.auxio.shared.AuxioAppBarLayout>
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/settings_list_fragment"
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
</fragment>
|
||||
<dialog
|
||||
android:id="@+id/accent_dialog"
|
||||
android:name="org.oxycblt.auxio.ui.accent.AccentCustomizeDialog"
|
||||
android:name="org.oxycblt.auxio.settings.accent.AccentCustomizeDialog"
|
||||
android:label="accent_dialog"
|
||||
tools:layout="@layout/dialog_accent" />
|
||||
<dialog
|
||||
|
|
Loading…
Reference in a new issue