detail: move editing state to toolbar
Move the music editing state to the toolbar. This should be signifigantly clearer than prior, at the cost of it's "universality" implying that renaming should be available when it actually won't be.
This commit is contained in:
parent
cee92c8087
commit
1fd6795b0d
20 changed files with 317 additions and 286 deletions
|
@ -38,6 +38,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||||
|
import org.oxycblt.auxio.detail.DetailViewModel
|
||||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||||
import org.oxycblt.auxio.music.Music
|
import org.oxycblt.auxio.music.Music
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
|
@ -66,6 +67,7 @@ class MainFragment :
|
||||||
private val musicModel: MusicViewModel by activityViewModels()
|
private val musicModel: MusicViewModel by activityViewModels()
|
||||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
private val selectionModel: SelectionViewModel by activityViewModels()
|
private val selectionModel: SelectionViewModel by activityViewModels()
|
||||||
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
private val callback = DynamicBackPressedCallback()
|
private val callback = DynamicBackPressedCallback()
|
||||||
private var lastInsets: WindowInsets? = null
|
private var lastInsets: WindowInsets? = null
|
||||||
private var elevationNormal = 0f
|
private var elevationNormal = 0f
|
||||||
|
@ -458,6 +460,11 @@ class MainFragment :
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear out pending playlist edits.
|
||||||
|
if (detailModel.dropPlaylistEdit()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Clear out any prior selections.
|
// Clear out any prior selections.
|
||||||
if (selectionModel.drop()) {
|
if (selectionModel.drop()) {
|
||||||
return
|
return
|
||||||
|
@ -487,6 +494,7 @@ class MainFragment :
|
||||||
isEnabled =
|
isEnabled =
|
||||||
queueSheetBehavior?.state == BackportBottomSheetBehavior.STATE_EXPANDED ||
|
queueSheetBehavior?.state == BackportBottomSheetBehavior.STATE_EXPANDED ||
|
||||||
playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED ||
|
playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
detailModel.editedPlaylist.value != null ||
|
||||||
selectionModel.selected.value.isNotEmpty() ||
|
selectionModel.selected.value.isNotEmpty() ||
|
||||||
exploreNavController.currentDestination?.id !=
|
exploreNavController.currentDestination?.id !=
|
||||||
exploreNavController.graph.startDestinationId
|
exploreNavController.graph.startDestinationId
|
||||||
|
|
|
@ -93,7 +93,7 @@ class AlbumDetailFragment :
|
||||||
super.onBindingCreated(binding, savedInstanceState)
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
// --- UI SETUP --
|
// --- UI SETUP --
|
||||||
binding.detailToolbar.apply {
|
binding.detailNormalToolbar.apply {
|
||||||
inflateMenu(R.menu.menu_album_detail)
|
inflateMenu(R.menu.menu_album_detail)
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
setOnMenuItemClickListener(this@AlbumDetailFragment)
|
setOnMenuItemClickListener(this@AlbumDetailFragment)
|
||||||
|
@ -124,7 +124,7 @@ class AlbumDetailFragment :
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||||
// during list initialization and crash the app. Could happen if the user is fast enough.
|
// during list initialization and crash the app. Could happen if the user is fast enough.
|
||||||
|
@ -214,7 +214,7 @@ class AlbumDetailFragment :
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requireBinding().detailToolbar.title = album.name.resolve(requireContext())
|
requireBinding().detailNormalToolbar.title = album.name.resolve(requireContext())
|
||||||
albumHeaderAdapter.setParent(album)
|
albumHeaderAdapter.setParent(album)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,6 +313,13 @@ class AlbumDetailFragment :
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
albumListAdapter.setSelected(selected.toSet())
|
albumListAdapter.setSelected(selected.toSet())
|
||||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
|
||||||
|
val binding = requireBinding()
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
|
||||||
|
binding.detailToolbar.setVisible(R.id.detail_selection_toolbar)
|
||||||
|
} else {
|
||||||
|
binding.detailToolbar.setVisible(R.id.detail_normal_toolbar)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -91,7 +91,7 @@ class ArtistDetailFragment :
|
||||||
super.onBindingCreated(binding, savedInstanceState)
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
binding.detailToolbar.apply {
|
binding.detailNormalToolbar.apply {
|
||||||
inflateMenu(R.menu.menu_parent_detail)
|
inflateMenu(R.menu.menu_parent_detail)
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
||||||
|
@ -122,7 +122,7 @@ class ArtistDetailFragment :
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||||
// during list initialization and crash the app. Could happen if the user is fast enough.
|
// during list initialization and crash the app. Could happen if the user is fast enough.
|
||||||
|
@ -223,7 +223,7 @@ class ArtistDetailFragment :
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requireBinding().detailToolbar.title = artist.name.resolve(requireContext())
|
requireBinding().detailNormalToolbar.title = artist.name.resolve(requireContext())
|
||||||
artistHeaderAdapter.setParent(artist)
|
artistHeaderAdapter.setParent(artist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,6 +283,13 @@ class ArtistDetailFragment :
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
artistListAdapter.setSelected(selected.toSet())
|
artistListAdapter.setSelected(selected.toSet())
|
||||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
|
||||||
|
val binding = requireBinding()
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
|
||||||
|
binding.detailToolbar.setVisible(R.id.detail_selection_toolbar)
|
||||||
|
} else {
|
||||||
|
binding.detailToolbar.setVisible(R.id.detail_normal_toolbar)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr
|
||||||
|
|
||||||
// Assume that we have a Toolbar with a detail_toolbar ID, as this view is only
|
// Assume that we have a Toolbar with a detail_toolbar ID, as this view is only
|
||||||
// used within the detail layouts.
|
// used within the detail layouts.
|
||||||
val toolbar = findViewById<Toolbar>(R.id.detail_toolbar)
|
val toolbar = findViewById<Toolbar>(R.id.detail_normal_toolbar)
|
||||||
|
|
||||||
// The Toolbar's title view is actually hidden. To avoid having to create our own
|
// The Toolbar's title view is actually hidden. To avoid having to create our own
|
||||||
// title view, we just reflect into Toolbar and grab the hidden field.
|
// title view, we just reflect into Toolbar and grab the hidden field.
|
||||||
|
|
|
@ -22,7 +22,6 @@ import androidx.annotation.StringRes
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import java.lang.Exception
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
@ -298,7 +297,7 @@ constructor(
|
||||||
* End a playlist editing session and commits it to the database. Does nothing if there was no
|
* End a playlist editing session and commits it to the database. Does nothing if there was no
|
||||||
* prior editing session.
|
* prior editing session.
|
||||||
*/
|
*/
|
||||||
fun confirmPlaylistEdit() {
|
fun savePlaylistEdit() {
|
||||||
val playlist = _currentPlaylist.value ?: return
|
val playlist = _currentPlaylist.value ?: return
|
||||||
val editedPlaylist = (_editedPlaylist.value ?: return).also { _editedPlaylist.value = null }
|
val editedPlaylist = (_editedPlaylist.value ?: return).also { _editedPlaylist.value = null }
|
||||||
musicRepository.rewritePlaylist(playlist, editedPlaylist)
|
musicRepository.rewritePlaylist(playlist, editedPlaylist)
|
||||||
|
@ -307,11 +306,18 @@ constructor(
|
||||||
/**
|
/**
|
||||||
* End a playlist editing session and keep the prior state. Does nothing if there was no prior
|
* End a playlist editing session and keep the prior state. Does nothing if there was no prior
|
||||||
* editing session.
|
* editing session.
|
||||||
|
*
|
||||||
|
* @return true if the session was ended, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun dropPlaylistEdit() {
|
fun dropPlaylistEdit(): Boolean {
|
||||||
val playlist = _currentPlaylist.value ?: return
|
val playlist = _currentPlaylist.value ?: return false
|
||||||
|
if (_editedPlaylist.value == null) {
|
||||||
|
// Nothing to do.
|
||||||
|
return false
|
||||||
|
}
|
||||||
_editedPlaylist.value = null
|
_editedPlaylist.value = null
|
||||||
refreshPlaylistList(playlist)
|
refreshPlaylistList(playlist)
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -319,8 +325,10 @@ constructor(
|
||||||
*
|
*
|
||||||
* @param from The start position, in the list adapter data.
|
* @param from The start position, in the list adapter data.
|
||||||
* @param to The destination position, in the list adapter data.
|
* @param to The destination position, in the list adapter data.
|
||||||
|
* @return true if the song was moved, false otherwise.
|
||||||
*/
|
*/
|
||||||
fun movePlaylistSongs(from: Int, to: Int): Boolean {
|
fun movePlaylistSongs(from: Int, to: Int): Boolean {
|
||||||
|
// TODO: Song re-sorting
|
||||||
val playlist = _currentPlaylist.value ?: return false
|
val playlist = _currentPlaylist.value ?: return false
|
||||||
val editedPlaylist = (_editedPlaylist.value ?: return false).toMutableList()
|
val editedPlaylist = (_editedPlaylist.value ?: return false).toMutableList()
|
||||||
val realFrom = from - 2
|
val realFrom = from - 2
|
||||||
|
@ -340,6 +348,7 @@ constructor(
|
||||||
* @param at The position of the item to remove, in the list adapter data.
|
* @param at The position of the item to remove, in the list adapter data.
|
||||||
*/
|
*/
|
||||||
fun removePlaylistSong(at: Int) {
|
fun removePlaylistSong(at: Int) {
|
||||||
|
// TODO: Remove header when empty
|
||||||
val playlist = _currentPlaylist.value ?: return
|
val playlist = _currentPlaylist.value ?: return
|
||||||
val editedPlaylist = (_editedPlaylist.value ?: return).toMutableList()
|
val editedPlaylist = (_editedPlaylist.value ?: return).toMutableList()
|
||||||
val realAt = at - 2
|
val realAt = at - 2
|
||||||
|
@ -478,7 +487,6 @@ constructor(
|
||||||
playlist: Playlist,
|
playlist: Playlist,
|
||||||
instructions: UpdateInstructions = UpdateInstructions.Diff
|
instructions: UpdateInstructions = UpdateInstructions.Diff
|
||||||
) {
|
) {
|
||||||
logD(Exception().stackTraceToString())
|
|
||||||
logD("Refreshing playlist list")
|
logD("Refreshing playlist list")
|
||||||
val list = mutableListOf<Item>()
|
val list = mutableListOf<Item>()
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,7 @@ class GenreDetailFragment :
|
||||||
super.onBindingCreated(binding, savedInstanceState)
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
binding.detailToolbar.apply {
|
binding.detailNormalToolbar.apply {
|
||||||
inflateMenu(R.menu.menu_parent_detail)
|
inflateMenu(R.menu.menu_parent_detail)
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
setOnMenuItemClickListener(this@GenreDetailFragment)
|
setOnMenuItemClickListener(this@GenreDetailFragment)
|
||||||
|
@ -115,7 +115,7 @@ class GenreDetailFragment :
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||||
// during list initialization and crash the app. Could happen if the user is fast enough.
|
// during list initialization and crash the app. Could happen if the user is fast enough.
|
||||||
|
@ -214,7 +214,7 @@ class GenreDetailFragment :
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requireBinding().detailToolbar.title = genre.name.resolve(requireContext())
|
requireBinding().detailNormalToolbar.title = genre.name.resolve(requireContext())
|
||||||
genreHeaderAdapter.setParent(genre)
|
genreHeaderAdapter.setParent(genre)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -260,6 +260,13 @@ class GenreDetailFragment :
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
genreListAdapter.setSelected(selected.toSet())
|
genreListAdapter.setSelected(selected.toSet())
|
||||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
|
||||||
|
val binding = requireBinding()
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
|
||||||
|
binding.detailToolbar.setVisible(R.id.detail_selection_toolbar)
|
||||||
|
} else {
|
||||||
|
binding.detailToolbar.setVisible(R.id.detail_normal_toolbar)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,12 +90,17 @@ class PlaylistDetailFragment :
|
||||||
super.onBindingCreated(binding, savedInstanceState)
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
binding.detailToolbar.apply {
|
binding.detailNormalToolbar.apply {
|
||||||
inflateMenu(R.menu.menu_playlist_detail)
|
inflateMenu(R.menu.menu_playlist_detail)
|
||||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||||
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.detailEditToolbar.apply {
|
||||||
|
setNavigationOnClickListener { detailModel.dropPlaylistEdit() }
|
||||||
|
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
||||||
|
}
|
||||||
|
|
||||||
binding.detailRecycler.apply {
|
binding.detailRecycler.apply {
|
||||||
adapter = ConcatAdapter(playlistHeaderAdapter, playlistListAdapter)
|
adapter = ConcatAdapter(playlistHeaderAdapter, playlistListAdapter)
|
||||||
touchHelper =
|
touchHelper =
|
||||||
|
@ -139,7 +144,7 @@ class PlaylistDetailFragment :
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
|
||||||
touchHelper = null
|
touchHelper = null
|
||||||
binding.detailRecycler.adapter = null
|
binding.detailRecycler.adapter = null
|
||||||
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||||
|
@ -159,7 +164,8 @@ class PlaylistDetailFragment :
|
||||||
initialNavDestinationChange = true
|
initialNavDestinationChange = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Drop any pending playlist edits when navigating away.
|
// Drop any pending playlist edits when navigating away. This could actually happen
|
||||||
|
// if the user is quick enough.
|
||||||
detailModel.dropPlaylistEdit()
|
detailModel.dropPlaylistEdit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +194,10 @@ class PlaylistDetailFragment :
|
||||||
musicModel.deletePlaylist(currentPlaylist)
|
musicModel.deletePlaylist(currentPlaylist)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
R.id.action_save -> {
|
||||||
|
detailModel.savePlaylistEdit()
|
||||||
|
true
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -214,21 +224,10 @@ class PlaylistDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartEdit() {
|
override fun onStartEdit() {
|
||||||
selectionModel.drop()
|
|
||||||
detailModel.startPlaylistEdit()
|
detailModel.startPlaylistEdit()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfirmEdit() {
|
override fun onOpenSortMenu(anchor: View) {}
|
||||||
detailModel.confirmPlaylistEdit()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDropEdit() {
|
|
||||||
detailModel.dropPlaylistEdit()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOpenSortMenu(anchor: View) {
|
|
||||||
throw IllegalStateException()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updatePlaylist(playlist: Playlist?) {
|
private fun updatePlaylist(playlist: Playlist?) {
|
||||||
if (playlist == null) {
|
if (playlist == null) {
|
||||||
|
@ -236,7 +235,9 @@ class PlaylistDetailFragment :
|
||||||
findNavController().navigateUp()
|
findNavController().navigateUp()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
requireBinding().detailToolbar.title = playlist.name.resolve(requireContext())
|
val binding = requireBinding()
|
||||||
|
binding.detailNormalToolbar.title = playlist.name.resolve(requireContext())
|
||||||
|
binding.detailEditToolbar.title = "Editing ${playlist.name.resolve(requireContext())}"
|
||||||
playlistHeaderAdapter.setParent(playlist)
|
playlistHeaderAdapter.setParent(playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,18 +280,35 @@ class PlaylistDetailFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateEditedPlaylist(editedPlaylist: List<Song>?) {
|
private fun updateEditedPlaylist(editedPlaylist: List<Song>?) {
|
||||||
// TODO: Disable check item when no edits have been made
|
|
||||||
|
|
||||||
// TODO: Massively improve how this UI is indicated:
|
|
||||||
// - Add an additional toolbar to indicate editing
|
|
||||||
// - Header should flip to re-sort button eventually
|
|
||||||
|
|
||||||
playlistListAdapter.setEditing(editedPlaylist != null)
|
playlistListAdapter.setEditing(editedPlaylist != null)
|
||||||
playlistHeaderAdapter.setEditedPlaylist(editedPlaylist)
|
playlistHeaderAdapter.setEditedPlaylist(editedPlaylist)
|
||||||
|
selectionModel.drop()
|
||||||
|
|
||||||
|
logD(editedPlaylist == detailModel.currentPlaylist.value?.songs)
|
||||||
|
requireBinding().detailEditToolbar.menu.findItem(R.id.action_save).isEnabled =
|
||||||
|
editedPlaylist != detailModel.currentPlaylist.value?.songs
|
||||||
|
|
||||||
|
updateMultiToolbar()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
playlistListAdapter.setSelected(selected.toSet())
|
playlistListAdapter.setSelected(selected.toSet())
|
||||||
requireBinding().detailSelectionToolbar.updateSelectionAmount(selected.size)
|
|
||||||
|
val binding = requireBinding()
|
||||||
|
if (selected.isNotEmpty()) {
|
||||||
|
binding.detailSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
|
||||||
|
}
|
||||||
|
updateMultiToolbar()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateMultiToolbar() {
|
||||||
|
val id =
|
||||||
|
when {
|
||||||
|
detailModel.editedPlaylist.value != null -> R.id.detail_edit_toolbar
|
||||||
|
selectionModel.selected.value.isNotEmpty() -> R.id.detail_selection_toolbar
|
||||||
|
else -> R.id.detail_normal_toolbar
|
||||||
|
}
|
||||||
|
|
||||||
|
requireBinding().detailToolbar.setVisible(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,10 +106,6 @@ class PlaylistDetailListAdapter(private val listener: Listener) :
|
||||||
interface Listener : DetailListAdapter.Listener<Song>, EditableListListener {
|
interface Listener : DetailListAdapter.Listener<Song>, EditableListListener {
|
||||||
/** Called when the "edit" option is selected in the edit header. */
|
/** Called when the "edit" option is selected in the edit header. */
|
||||||
fun onStartEdit()
|
fun onStartEdit()
|
||||||
/** Called when the "confirm" option is selected in the edit header. */
|
|
||||||
fun onConfirmEdit()
|
|
||||||
/** Called when the "cancel" option is selected in the edit header. */
|
|
||||||
fun onDropEdit()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -169,13 +165,9 @@ private class EditHeaderViewHolder private constructor(private val binding: Item
|
||||||
TooltipCompat.setTooltipText(this, contentDescription)
|
TooltipCompat.setTooltipText(this, contentDescription)
|
||||||
setOnClickListener { listener.onStartEdit() }
|
setOnClickListener { listener.onStartEdit() }
|
||||||
}
|
}
|
||||||
binding.headerConfirm.apply {
|
binding.headerSort.apply {
|
||||||
TooltipCompat.setTooltipText(this, contentDescription)
|
TooltipCompat.setTooltipText(this, contentDescription)
|
||||||
setOnClickListener { listener.onConfirmEdit() }
|
setOnClickListener(listener::onOpenSortMenu)
|
||||||
}
|
|
||||||
binding.headerCancel.apply {
|
|
||||||
TooltipCompat.setTooltipText(this, contentDescription)
|
|
||||||
setOnClickListener { listener.onDropEdit() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,12 +176,8 @@ private class EditHeaderViewHolder private constructor(private val binding: Item
|
||||||
isGone = editing
|
isGone = editing
|
||||||
jumpDrawablesToCurrentState()
|
jumpDrawablesToCurrentState()
|
||||||
}
|
}
|
||||||
binding.headerConfirm.apply {
|
binding.headerSort.apply {
|
||||||
isVisible = editing
|
isGone = !editing
|
||||||
jumpDrawablesToCurrentState()
|
|
||||||
}
|
|
||||||
binding.headerCancel.apply {
|
|
||||||
isVisible = editing
|
|
||||||
jumpDrawablesToCurrentState()
|
jumpDrawablesToCurrentState()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,6 +226,7 @@ private constructor(private val binding: ItemEditableSongBinding) :
|
||||||
elevation = binding.context.getDimen(R.dimen.elevation_normal)
|
elevation = binding.context.getDimen(R.dimen.elevation_normal)
|
||||||
alpha = 0
|
alpha = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.body.background =
|
binding.body.background =
|
||||||
LayerDrawable(
|
LayerDrawable(
|
||||||
|
|
|
@ -102,7 +102,7 @@ class HomeFragment :
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
binding.homeAppbar.addOnOffsetChangedListener(this)
|
binding.homeAppbar.addOnOffsetChangedListener(this)
|
||||||
binding.homeToolbar.apply {
|
binding.homeNormalToolbar.apply {
|
||||||
setOnMenuItemClickListener(this@HomeFragment)
|
setOnMenuItemClickListener(this@HomeFragment)
|
||||||
MenuCompat.setGroupDividerEnabled(menu, true)
|
MenuCompat.setGroupDividerEnabled(menu, true)
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,7 @@ class HomeFragment :
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
storagePermissionLauncher = null
|
storagePermissionLauncher = null
|
||||||
binding.homeAppbar.removeOnOffsetChangedListener(this)
|
binding.homeAppbar.removeOnOffsetChangedListener(this)
|
||||||
binding.homeToolbar.setOnMenuItemClickListener(null)
|
binding.homeNormalToolbar.setOnMenuItemClickListener(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
||||||
|
@ -178,8 +178,7 @@ class HomeFragment :
|
||||||
// Fade out the toolbar as the AppBarLayout collapses. To prevent status bar overlap,
|
// Fade out the toolbar as the AppBarLayout collapses. To prevent status bar overlap,
|
||||||
// the alpha transition is shifted such that the Toolbar becomes fully transparent
|
// the alpha transition is shifted such that the Toolbar becomes fully transparent
|
||||||
// when the AppBarLayout is only at half-collapsed.
|
// when the AppBarLayout is only at half-collapsed.
|
||||||
binding.homeSelectionToolbar.alpha =
|
binding.homeToolbar.alpha = 1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2))
|
||||||
1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2))
|
|
||||||
binding.homeContent.updatePadding(
|
binding.homeContent.updatePadding(
|
||||||
bottom = binding.homeAppbar.totalScrollRange + verticalOffset)
|
bottom = binding.homeAppbar.totalScrollRange + verticalOffset)
|
||||||
}
|
}
|
||||||
|
@ -243,7 +242,7 @@ class HomeFragment :
|
||||||
binding.homePager.adapter =
|
binding.homePager.adapter =
|
||||||
HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner)
|
HomePagerAdapter(homeModel.currentTabModes, childFragmentManager, viewLifecycleOwner)
|
||||||
|
|
||||||
val toolbarParams = binding.homeSelectionToolbar.layoutParams as AppBarLayout.LayoutParams
|
val toolbarParams = binding.homeToolbar.layoutParams as AppBarLayout.LayoutParams
|
||||||
if (homeModel.currentTabModes.size == 1) {
|
if (homeModel.currentTabModes.size == 1) {
|
||||||
// A single tab makes the tab layout redundant, hide it and disable the collapsing
|
// A single tab makes the tab layout redundant, hide it and disable the collapsing
|
||||||
// behavior.
|
// behavior.
|
||||||
|
@ -285,7 +284,7 @@ class HomeFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
val sortMenu =
|
val sortMenu =
|
||||||
unlikelyToBeNull(binding.homeToolbar.menu.findItem(R.id.submenu_sorting).subMenu)
|
unlikelyToBeNull(binding.homeNormalToolbar.menu.findItem(R.id.submenu_sorting).subMenu)
|
||||||
val toHighlight = homeModel.getSortForTab(tabMode)
|
val toHighlight = homeModel.getSortForTab(tabMode)
|
||||||
|
|
||||||
for (option in sortMenu) {
|
for (option in sortMenu) {
|
||||||
|
@ -456,12 +455,16 @@ class HomeFragment :
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
if (binding.homeSelectionToolbar.updateSelectionAmount(selected.size) &&
|
if (selected.isNotEmpty()) {
|
||||||
selected.isNotEmpty()) {
|
binding.homeSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
|
||||||
|
if (binding.homeToolbar.setVisible(R.id.home_selection_toolbar)) {
|
||||||
// New selection started, show the AppBarLayout to indicate the new state.
|
// New selection started, show the AppBarLayout to indicate the new state.
|
||||||
logD("Significant selection occurred, expanding AppBar")
|
logD("Significant selection occurred, expanding AppBar")
|
||||||
binding.homeAppbar.expandWithScrollingRecycler()
|
binding.homeAppbar.expandWithScrollingRecycler()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
binding.homeToolbar.setVisible(R.id.home_normal_toolbar)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupAxisTransitions(axis: Int) {
|
private fun setupAxisTransitions(axis: Int) {
|
||||||
|
|
|
@ -50,6 +50,7 @@ import okio.buffer
|
||||||
import okio.source
|
import okio.source
|
||||||
import org.oxycblt.auxio.image.CoverMode
|
import org.oxycblt.auxio.image.CoverMode
|
||||||
import org.oxycblt.auxio.image.ImageSettings
|
import org.oxycblt.auxio.image.ImageSettings
|
||||||
|
import org.oxycblt.auxio.list.Sort
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Song
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
@ -66,10 +67,10 @@ constructor(
|
||||||
val albums = computeAlbumOrdering(songs)
|
val albums = computeAlbumOrdering(songs)
|
||||||
val streams = mutableListOf<InputStream>()
|
val streams = mutableListOf<InputStream>()
|
||||||
for (album in albums) {
|
for (album in albums) {
|
||||||
|
openInputStream(album)?.let(streams::add)
|
||||||
if (streams.size == 4) {
|
if (streams.size == 4) {
|
||||||
return createMosaic(streams, size)
|
return createMosaic(streams, size)
|
||||||
}
|
}
|
||||||
openInputStream(album)?.let(streams::add)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return streams.firstOrNull()?.let { stream ->
|
return streams.firstOrNull()?.let { stream ->
|
||||||
|
@ -81,7 +82,7 @@ constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
fun computeAlbumOrdering(songs: List<Song>): Collection<Album> =
|
fun computeAlbumOrdering(songs: List<Song>): Collection<Album> =
|
||||||
songs.groupByTo(sortedMapOf(compareByDescending { it.songs.size })) { it.album }.keys
|
Sort(Sort.Mode.ByCount, Sort.Direction.DESCENDING).albums(songs.groupBy { it.album }.keys)
|
||||||
|
|
||||||
private suspend fun openInputStream(album: Album): InputStream? =
|
private suspend fun openInputStream(album: Album): InputStream? =
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -39,20 +39,13 @@ abstract class SelectionFragment<VB : ViewBinding> :
|
||||||
protected abstract val musicModel: MusicViewModel
|
protected abstract val musicModel: MusicViewModel
|
||||||
protected abstract val playbackModel: PlaybackViewModel
|
protected abstract val playbackModel: PlaybackViewModel
|
||||||
|
|
||||||
/**
|
open fun getSelectionToolbar(binding: VB): Toolbar? = null
|
||||||
* Get the [SelectionToolbarOverlay] of the concrete Fragment to be automatically managed by
|
|
||||||
* [SelectionFragment].
|
|
||||||
*
|
|
||||||
* @return The [SelectionToolbarOverlay] of the concrete [SelectionFragment]'s [VB], or null if
|
|
||||||
* there is not one.
|
|
||||||
*/
|
|
||||||
open fun getSelectionToolbar(binding: VB): SelectionToolbarOverlay? = null
|
|
||||||
|
|
||||||
override fun onBindingCreated(binding: VB, savedInstanceState: Bundle?) {
|
override fun onBindingCreated(binding: VB, savedInstanceState: Bundle?) {
|
||||||
super.onBindingCreated(binding, savedInstanceState)
|
super.onBindingCreated(binding, savedInstanceState)
|
||||||
getSelectionToolbar(binding)?.apply {
|
getSelectionToolbar(binding)?.apply {
|
||||||
// Add cancel and menu item listeners to manage what occurs with the selection.
|
// Add cancel and menu item listeners to manage what occurs with the selection.
|
||||||
setOnSelectionCancelListener { selectionModel.drop() }
|
setNavigationOnClickListener { selectionModel.drop() }
|
||||||
setOnMenuItemClickListener(this@SelectionFragment)
|
setOnMenuItemClickListener(this@SelectionFragment)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,178 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 Auxio Project
|
|
||||||
* SelectionToolbarOverlay.kt is part of Auxio.
|
|
||||||
*
|
|
||||||
* 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.selection
|
|
||||||
|
|
||||||
import android.animation.ValueAnimator
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.annotation.AttrRes
|
|
||||||
import androidx.appcompat.widget.Toolbar.OnMenuItemClickListener
|
|
||||||
import androidx.core.view.isInvisible
|
|
||||||
import com.google.android.material.appbar.MaterialToolbar
|
|
||||||
import org.oxycblt.auxio.R
|
|
||||||
import org.oxycblt.auxio.util.getInteger
|
|
||||||
import org.oxycblt.auxio.util.logD
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A wrapper around a [MaterialToolbar] that adds an additional [MaterialToolbar] showing the
|
|
||||||
* current selection state.
|
|
||||||
*
|
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
|
||||||
*
|
|
||||||
* TODO: Generalize this into a "view flipper" class and then derive it through other means?
|
|
||||||
*/
|
|
||||||
class SelectionToolbarOverlay
|
|
||||||
@JvmOverloads
|
|
||||||
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
|
||||||
FrameLayout(context, attrs, defStyleAttr) {
|
|
||||||
private lateinit var innerToolbar: MaterialToolbar
|
|
||||||
private val selectionToolbar =
|
|
||||||
MaterialToolbar(context).apply {
|
|
||||||
setNavigationIcon(R.drawable.ic_close_24)
|
|
||||||
inflateMenu(R.menu.menu_selection_actions)
|
|
||||||
|
|
||||||
if (isInEditMode) {
|
|
||||||
isInvisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var fadeThroughAnimator: ValueAnimator? = null
|
|
||||||
|
|
||||||
override fun onFinishInflate() {
|
|
||||||
super.onFinishInflate()
|
|
||||||
// Sanity check: Avoid incorrect views from being included in this layout.
|
|
||||||
check(childCount == 1 && getChildAt(0) is MaterialToolbar) {
|
|
||||||
"SelectionToolbarOverlay Must have only one MaterialToolbar child"
|
|
||||||
}
|
|
||||||
// The inner toolbar should be the first child.
|
|
||||||
innerToolbar = getChildAt(0) as MaterialToolbar
|
|
||||||
// Selection toolbar should appear on top of the inner toolbar.
|
|
||||||
addView(selectionToolbar)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set an OnClickListener for when the "cancel" button in the selection [MaterialToolbar] is
|
|
||||||
* pressed.
|
|
||||||
*
|
|
||||||
* @param listener The OnClickListener to respond to this interaction.
|
|
||||||
* @see MaterialToolbar.setNavigationOnClickListener
|
|
||||||
*/
|
|
||||||
fun setOnSelectionCancelListener(listener: OnClickListener) {
|
|
||||||
selectionToolbar.setNavigationOnClickListener(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set an [OnMenuItemClickListener] for when a MenuItem is selected from the selection
|
|
||||||
* [MaterialToolbar].
|
|
||||||
*
|
|
||||||
* @param listener The [OnMenuItemClickListener] to respond to this interaction.
|
|
||||||
* @see MaterialToolbar.setOnMenuItemClickListener
|
|
||||||
*/
|
|
||||||
fun setOnMenuItemClickListener(listener: OnMenuItemClickListener?) {
|
|
||||||
selectionToolbar.setOnMenuItemClickListener(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the selection [MaterialToolbar] to reflect the current selection amount.
|
|
||||||
*
|
|
||||||
* @param amount The amount of items that are currently selected.
|
|
||||||
* @return true if the selection [MaterialToolbar] changes, false otherwise.
|
|
||||||
*/
|
|
||||||
fun updateSelectionAmount(amount: Int): Boolean {
|
|
||||||
logD("Updating selection amount to $amount")
|
|
||||||
return if (amount > 0) {
|
|
||||||
// Only update the selected amount when it's non-zero to prevent a strange
|
|
||||||
// title text.
|
|
||||||
selectionToolbar.title = context.getString(R.string.fmt_selected, amount)
|
|
||||||
animateToolbarsVisibility(true)
|
|
||||||
} else {
|
|
||||||
animateToolbarsVisibility(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Animate the visibility of the inner and selection [MaterialToolbar]s to the given state.
|
|
||||||
*
|
|
||||||
* @param selectionVisible Whether the selection [MaterialToolbar] should be visible or not.
|
|
||||||
* @return true if the toolbars have changed, false otherwise.
|
|
||||||
*/
|
|
||||||
private fun animateToolbarsVisibility(selectionVisible: Boolean): Boolean {
|
|
||||||
// TODO: Animate nicer Material Fade transitions using animators (Normal transitions
|
|
||||||
// don't work due to translation)
|
|
||||||
// Set up the target transitions for both the inner and selection toolbars.
|
|
||||||
val targetInnerAlpha: Float
|
|
||||||
val targetSelectionAlpha: Float
|
|
||||||
val targetDuration: Long
|
|
||||||
|
|
||||||
if (selectionVisible) {
|
|
||||||
targetInnerAlpha = 0f
|
|
||||||
targetSelectionAlpha = 1f
|
|
||||||
targetDuration = context.getInteger(R.integer.anim_fade_enter_duration).toLong()
|
|
||||||
} else {
|
|
||||||
targetInnerAlpha = 1f
|
|
||||||
targetSelectionAlpha = 0f
|
|
||||||
targetDuration = context.getInteger(R.integer.anim_fade_exit_duration).toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (innerToolbar.alpha == targetInnerAlpha &&
|
|
||||||
selectionToolbar.alpha == targetSelectionAlpha) {
|
|
||||||
// Nothing to do.
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isLaidOut) {
|
|
||||||
// Not laid out, just change it immediately while are not shown to the user.
|
|
||||||
// This is an initialization, so we return false despite changing.
|
|
||||||
setToolbarsAlpha(targetInnerAlpha)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fadeThroughAnimator != null) {
|
|
||||||
fadeThroughAnimator?.cancel()
|
|
||||||
fadeThroughAnimator = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fadeThroughAnimator =
|
|
||||||
ValueAnimator.ofFloat(innerToolbar.alpha, targetInnerAlpha).apply {
|
|
||||||
duration = targetDuration
|
|
||||||
addUpdateListener { setToolbarsAlpha(it.animatedValue as Float) }
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the alpha of the inner and selection [MaterialToolbar]s.
|
|
||||||
*
|
|
||||||
* @param innerAlpha The opacity of the inner [MaterialToolbar]. This will map to the inverse
|
|
||||||
* opacity of the selection [MaterialToolbar].
|
|
||||||
*/
|
|
||||||
private fun setToolbarsAlpha(innerAlpha: Float) {
|
|
||||||
innerToolbar.apply {
|
|
||||||
alpha = innerAlpha
|
|
||||||
isInvisible = innerAlpha == 0f
|
|
||||||
}
|
|
||||||
|
|
||||||
selectionToolbar.apply {
|
|
||||||
alpha = 1 - innerAlpha
|
|
||||||
isInvisible = innerAlpha == 1f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -81,7 +81,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
|
|
||||||
imm = binding.context.getSystemServiceCompat(InputMethodManager::class)
|
imm = binding.context.getSystemServiceCompat(InputMethodManager::class)
|
||||||
|
|
||||||
binding.searchToolbar.apply {
|
binding.searchNormalToolbar.apply {
|
||||||
// Initialize the current filtering mode.
|
// Initialize the current filtering mode.
|
||||||
menu.findItem(searchModel.getFilterOptionId()).isChecked = true
|
menu.findItem(searchModel.getFilterOptionId()).isChecked = true
|
||||||
|
|
||||||
|
@ -126,7 +126,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
|
|
||||||
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
||||||
super.onDestroyBinding(binding)
|
super.onDestroyBinding(binding)
|
||||||
binding.searchToolbar.setOnMenuItemClickListener(null)
|
binding.searchNormalToolbar.setOnMenuItemClickListener(null)
|
||||||
binding.searchRecycler.adapter = null
|
binding.searchRecycler.adapter = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,11 +198,17 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
||||||
|
|
||||||
private fun updateSelection(selected: List<Music>) {
|
private fun updateSelection(selected: List<Music>) {
|
||||||
searchAdapter.setSelected(selected.toSet())
|
searchAdapter.setSelected(selected.toSet())
|
||||||
if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) &&
|
val binding = requireBinding()
|
||||||
selected.isNotEmpty()) {
|
if (selected.isNotEmpty()) {
|
||||||
// Make selection of obscured items easier by hiding the keyboard.
|
binding.searchSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
|
||||||
|
if (binding.searchToolbar.setVisible(R.id.search_selection_toolbar)) {
|
||||||
|
// New selection started, show the keyboard to make selection easier.
|
||||||
|
logD("Significant selection occurred, hiding keyboard")
|
||||||
hideKeyboard()
|
hideKeyboard()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
binding.searchToolbar.setVisible(R.id.search_normal_toolbar)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
114
app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt
Normal file
114
app/src/main/java/org/oxycblt/auxio/ui/MultiToolbar.kt
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2023 Auxio Project
|
||||||
|
* MultiToolbar.kt is part of Auxio.
|
||||||
|
*
|
||||||
|
* 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.ui
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.annotation.AttrRes
|
||||||
|
import androidx.annotation.IdRes
|
||||||
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.core.view.children
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.util.getInteger
|
||||||
|
import org.oxycblt.auxio.util.logD
|
||||||
|
|
||||||
|
class MultiToolbar
|
||||||
|
@JvmOverloads
|
||||||
|
constructor(context: Context, attrs: AttributeSet? = null, @AttrRes defStyleAttr: Int = 0) :
|
||||||
|
FrameLayout(context, attrs, defStyleAttr) {
|
||||||
|
private var fadeThroughAnimator: ValueAnimator? = null
|
||||||
|
private var currentlyVisible = 0
|
||||||
|
|
||||||
|
override fun onFinishInflate() {
|
||||||
|
super.onFinishInflate()
|
||||||
|
for (i in 1 until childCount) {
|
||||||
|
getChildAt(i).apply {
|
||||||
|
alpha = 0f
|
||||||
|
isInvisible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setVisible(@IdRes viewId: Int): Boolean {
|
||||||
|
val index = children.indexOfFirst { it.id == viewId }
|
||||||
|
if (index == currentlyVisible) return false
|
||||||
|
return animateToolbarsVisibility(currentlyVisible, index).also { currentlyVisible = index }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun animateToolbarsVisibility(from: Int, to: Int): Boolean {
|
||||||
|
// TODO: Animate nicer Material Fade transitions using animators (Normal transitions
|
||||||
|
// don't work due to translation)
|
||||||
|
// Set up the target transitions for both the inner and selection toolbars.
|
||||||
|
val targetFromAlpha = 0f
|
||||||
|
val targetToAlpha = 1f
|
||||||
|
val targetDuration =
|
||||||
|
if (from < to) {
|
||||||
|
context.getInteger(R.integer.anim_fade_enter_duration).toLong()
|
||||||
|
} else {
|
||||||
|
context.getInteger(R.integer.anim_fade_exit_duration).toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
logD(targetDuration)
|
||||||
|
|
||||||
|
val fromView = getChildAt(from) as Toolbar
|
||||||
|
val toView = getChildAt(to) as Toolbar
|
||||||
|
|
||||||
|
if (fromView.alpha == targetFromAlpha && toView.alpha == targetToAlpha) {
|
||||||
|
// Nothing to do.
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isLaidOut) {
|
||||||
|
// Not laid out, just change it immediately while are not shown to the user.
|
||||||
|
// This is an initialization, so we return false despite changing.
|
||||||
|
setToolbarsAlpha(fromView, toView, targetFromAlpha)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fadeThroughAnimator != null) {
|
||||||
|
fadeThroughAnimator?.cancel()
|
||||||
|
fadeThroughAnimator = null
|
||||||
|
}
|
||||||
|
|
||||||
|
fadeThroughAnimator =
|
||||||
|
ValueAnimator.ofFloat(fromView.alpha, targetFromAlpha).apply {
|
||||||
|
duration = targetDuration
|
||||||
|
addUpdateListener { setToolbarsAlpha(fromView, toView, it.animatedValue as Float) }
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setToolbarsAlpha(from: Toolbar, to: Toolbar, innerAlpha: Float) {
|
||||||
|
logD("${to.id == R.id.detail_edit_toolbar} ${1 - innerAlpha}")
|
||||||
|
from.apply {
|
||||||
|
alpha = innerAlpha
|
||||||
|
isInvisible = innerAlpha == 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
to.apply {
|
||||||
|
alpha = 1 - innerAlpha
|
||||||
|
isInvisible = innerAlpha == 1f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
app/src/main/res/drawable/ic_save_24.xml
Normal file
11
app/src/main/res/drawable/ic_save_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M840,280L840,840L120,840L120,120L680,120L840,280ZM760,314L646,200L200,200L200,760L760,760L760,314ZM480,720Q530,720 565,685Q600,650 600,600Q600,550 565,515Q530,480 480,480Q430,480 395,515Q360,550 360,600Q360,650 395,685Q430,720 480,720ZM240,400L600,400L600,240L240,240L240,400ZM200,314L200,760L200,760L200,200L200,200L200,314Z"/>
|
||||||
|
</vector>
|
|
@ -13,19 +13,38 @@
|
||||||
app:liftOnScroll="true"
|
app:liftOnScroll="true"
|
||||||
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
app:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||||
|
|
||||||
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
<org.oxycblt.auxio.ui.MultiToolbar
|
||||||
android:id="@+id/detail_selection_toolbar"
|
android:id="@+id/detail_toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/detail_toolbar"
|
android:id="@+id/detail_normal_toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
app:navigationIcon="@drawable/ic_back_24" />
|
app:navigationIcon="@drawable/ic_back_24" />
|
||||||
|
|
||||||
</org.oxycblt.auxio.list.selection.SelectionToolbarOverlay>
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/detail_selection_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:navigationIcon="@drawable/ic_close_24"
|
||||||
|
app:menu="@menu/menu_selection_actions" />
|
||||||
|
|
||||||
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/detail_edit_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:navigationIcon="@drawable/ic_close_24"
|
||||||
|
app:menu="@menu/menu_edit_actions" />
|
||||||
|
|
||||||
|
</org.oxycblt.auxio.ui.MultiToolbar>
|
||||||
|
|
||||||
</org.oxycblt.auxio.detail.DetailAppBarLayout>
|
</org.oxycblt.auxio.detail.DetailAppBarLayout>
|
||||||
|
|
||||||
|
|
|
@ -12,20 +12,29 @@
|
||||||
android:id="@+id/home_appbar"
|
android:id="@+id/home_appbar"
|
||||||
style="@style/Widget.Auxio.AppBarLayout">
|
style="@style/Widget.Auxio.AppBarLayout">
|
||||||
|
|
||||||
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
<org.oxycblt.auxio.ui.MultiToolbar
|
||||||
android:id="@+id/home_selection_toolbar"
|
android:id="@+id/home_toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/home_toolbar"
|
android:id="@+id/home_normal_toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:layout_scrollFlags="scroll|enterAlways"
|
app:layout_scrollFlags="scroll|enterAlways"
|
||||||
app:menu="@menu/menu_home"
|
app:menu="@menu/menu_home"
|
||||||
app:title="@string/info_app_name" />
|
app:title="@string/info_app_name" />
|
||||||
|
|
||||||
</org.oxycblt.auxio.list.selection.SelectionToolbarOverlay>
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/home_selection_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:navigationIcon="@drawable/ic_close_24"
|
||||||
|
app:menu="@menu/menu_selection_actions" />
|
||||||
|
|
||||||
|
</org.oxycblt.auxio.ui.MultiToolbar>
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/home_tabs"
|
android:id="@+id/home_tabs"
|
||||||
|
|
|
@ -12,13 +12,13 @@
|
||||||
app:liftOnScroll="true"
|
app:liftOnScroll="true"
|
||||||
app:liftOnScrollTargetViewId="@id/search_recycler">
|
app:liftOnScrollTargetViewId="@id/search_recycler">
|
||||||
|
|
||||||
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
<org.oxycblt.auxio.ui.MultiToolbar
|
||||||
android:id="@+id/search_selection_toolbar"
|
android:id="@+id/search_toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
android:id="@+id/search_toolbar"
|
android:id="@+id/search_normal_toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:menu="@menu/menu_search"
|
app:menu="@menu/menu_search"
|
||||||
|
@ -49,7 +49,16 @@
|
||||||
|
|
||||||
</com.google.android.material.appbar.MaterialToolbar>
|
</com.google.android.material.appbar.MaterialToolbar>
|
||||||
|
|
||||||
</org.oxycblt.auxio.list.selection.SelectionToolbarOverlay>
|
<com.google.android.material.appbar.MaterialToolbar
|
||||||
|
android:id="@+id/search_selection_toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
app:navigationIcon="@drawable/ic_close_24"
|
||||||
|
app:menu="@menu/menu_selection_actions" />
|
||||||
|
|
||||||
|
</org.oxycblt.auxio.ui.MultiToolbar>
|
||||||
|
|
||||||
</org.oxycblt.auxio.ui.CoordinatorAppBarLayout>
|
</org.oxycblt.auxio.ui.CoordinatorAppBarLayout>
|
||||||
|
|
||||||
|
|
|
@ -29,22 +29,13 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
<org.oxycblt.auxio.ui.RippleFixMaterialButton
|
<org.oxycblt.auxio.ui.RippleFixMaterialButton
|
||||||
android:id="@+id/header_confirm"
|
android:id="@+id/header_sort"
|
||||||
style="@style/Widget.Auxio.Button.Icon.Small"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:contentDescription="@string/lbl_ok"
|
|
||||||
app:icon="@drawable/ic_check_24"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
|
||||||
|
|
||||||
<org.oxycblt.auxio.ui.RippleFixMaterialButton
|
|
||||||
android:id="@+id/header_cancel"
|
|
||||||
style="@style/Widget.Auxio.Button.Icon.Small"
|
style="@style/Widget.Auxio.Button.Icon.Small"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginEnd="@dimen/spacing_mid_medium"
|
android:layout_marginEnd="@dimen/spacing_mid_medium"
|
||||||
android:contentDescription="@string/lbl_cancel"
|
android:contentDescription="@string/lbl_cancel"
|
||||||
app:icon="@drawable/ic_close_24"
|
app:icon="@drawable/ic_sort_24"
|
||||||
app:layout_constraintEnd_toEndOf="parent" />
|
app:layout_constraintEnd_toEndOf="parent" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
9
app/src/main/res/menu/menu_edit_actions.xml
Normal file
9
app/src/main/res/menu/menu_edit_actions.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_save"
|
||||||
|
android:title="@string/lbl_save"
|
||||||
|
app:showAsAction="always"
|
||||||
|
android:icon="@drawable/ic_save_24" />
|
||||||
|
</menu>
|
Loading…
Reference in a new issue