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.min
|
||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||
import org.oxycblt.auxio.detail.DetailViewModel
|
||||
import org.oxycblt.auxio.list.selection.SelectionViewModel
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
|
@ -66,6 +67,7 @@ class MainFragment :
|
|||
private val musicModel: MusicViewModel by activityViewModels()
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val selectionModel: SelectionViewModel by activityViewModels()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
private val callback = DynamicBackPressedCallback()
|
||||
private var lastInsets: WindowInsets? = null
|
||||
private var elevationNormal = 0f
|
||||
|
@ -458,6 +460,11 @@ class MainFragment :
|
|||
return
|
||||
}
|
||||
|
||||
// Clear out pending playlist edits.
|
||||
if (detailModel.dropPlaylistEdit()) {
|
||||
return
|
||||
}
|
||||
|
||||
// Clear out any prior selections.
|
||||
if (selectionModel.drop()) {
|
||||
return
|
||||
|
@ -487,6 +494,7 @@ class MainFragment :
|
|||
isEnabled =
|
||||
queueSheetBehavior?.state == BackportBottomSheetBehavior.STATE_EXPANDED ||
|
||||
playbackSheetBehavior.state == BackportBottomSheetBehavior.STATE_EXPANDED ||
|
||||
detailModel.editedPlaylist.value != null ||
|
||||
selectionModel.selected.value.isNotEmpty() ||
|
||||
exploreNavController.currentDestination?.id !=
|
||||
exploreNavController.graph.startDestinationId
|
||||
|
|
|
@ -93,7 +93,7 @@ class AlbumDetailFragment :
|
|||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
||||
// --- UI SETUP --
|
||||
binding.detailToolbar.apply {
|
||||
binding.detailNormalToolbar.apply {
|
||||
inflateMenu(R.menu.menu_album_detail)
|
||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
setOnMenuItemClickListener(this@AlbumDetailFragment)
|
||||
|
@ -124,7 +124,7 @@ class AlbumDetailFragment :
|
|||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
||||
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
|
||||
binding.detailRecycler.adapter = null
|
||||
// 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.
|
||||
|
@ -214,7 +214,7 @@ class AlbumDetailFragment :
|
|||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
requireBinding().detailToolbar.title = album.name.resolve(requireContext())
|
||||
requireBinding().detailNormalToolbar.title = album.name.resolve(requireContext())
|
||||
albumHeaderAdapter.setParent(album)
|
||||
}
|
||||
|
||||
|
@ -313,6 +313,13 @@ class AlbumDetailFragment :
|
|||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
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)
|
||||
|
||||
// --- UI SETUP ---
|
||||
binding.detailToolbar.apply {
|
||||
binding.detailNormalToolbar.apply {
|
||||
inflateMenu(R.menu.menu_parent_detail)
|
||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
setOnMenuItemClickListener(this@ArtistDetailFragment)
|
||||
|
@ -122,7 +122,7 @@ class ArtistDetailFragment :
|
|||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
||||
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
|
||||
binding.detailRecycler.adapter = null
|
||||
// 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.
|
||||
|
@ -223,7 +223,7 @@ class ArtistDetailFragment :
|
|||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
requireBinding().detailToolbar.title = artist.name.resolve(requireContext())
|
||||
requireBinding().detailNormalToolbar.title = artist.name.resolve(requireContext())
|
||||
artistHeaderAdapter.setParent(artist)
|
||||
}
|
||||
|
||||
|
@ -283,6 +283,13 @@ class ArtistDetailFragment :
|
|||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
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
|
||||
// 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
|
||||
// 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.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.lang.Exception
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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
|
||||
* prior editing session.
|
||||
*/
|
||||
fun confirmPlaylistEdit() {
|
||||
fun savePlaylistEdit() {
|
||||
val playlist = _currentPlaylist.value ?: return
|
||||
val editedPlaylist = (_editedPlaylist.value ?: return).also { _editedPlaylist.value = null }
|
||||
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
|
||||
* editing session.
|
||||
*
|
||||
* @return true if the session was ended, false otherwise.
|
||||
*/
|
||||
fun dropPlaylistEdit() {
|
||||
val playlist = _currentPlaylist.value ?: return
|
||||
fun dropPlaylistEdit(): Boolean {
|
||||
val playlist = _currentPlaylist.value ?: return false
|
||||
if (_editedPlaylist.value == null) {
|
||||
// Nothing to do.
|
||||
return false
|
||||
}
|
||||
_editedPlaylist.value = null
|
||||
refreshPlaylistList(playlist)
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -319,8 +325,10 @@ constructor(
|
|||
*
|
||||
* @param from The start 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 {
|
||||
// TODO: Song re-sorting
|
||||
val playlist = _currentPlaylist.value ?: return false
|
||||
val editedPlaylist = (_editedPlaylist.value ?: return false).toMutableList()
|
||||
val realFrom = from - 2
|
||||
|
@ -340,6 +348,7 @@ constructor(
|
|||
* @param at The position of the item to remove, in the list adapter data.
|
||||
*/
|
||||
fun removePlaylistSong(at: Int) {
|
||||
// TODO: Remove header when empty
|
||||
val playlist = _currentPlaylist.value ?: return
|
||||
val editedPlaylist = (_editedPlaylist.value ?: return).toMutableList()
|
||||
val realAt = at - 2
|
||||
|
@ -478,7 +487,6 @@ constructor(
|
|||
playlist: Playlist,
|
||||
instructions: UpdateInstructions = UpdateInstructions.Diff
|
||||
) {
|
||||
logD(Exception().stackTraceToString())
|
||||
logD("Refreshing playlist list")
|
||||
val list = mutableListOf<Item>()
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ class GenreDetailFragment :
|
|||
super.onBindingCreated(binding, savedInstanceState)
|
||||
|
||||
// --- UI SETUP ---
|
||||
binding.detailToolbar.apply {
|
||||
binding.detailNormalToolbar.apply {
|
||||
inflateMenu(R.menu.menu_parent_detail)
|
||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
setOnMenuItemClickListener(this@GenreDetailFragment)
|
||||
|
@ -115,7 +115,7 @@ class GenreDetailFragment :
|
|||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
||||
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
|
||||
binding.detailRecycler.adapter = null
|
||||
// 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.
|
||||
|
@ -214,7 +214,7 @@ class GenreDetailFragment :
|
|||
findNavController().navigateUp()
|
||||
return
|
||||
}
|
||||
requireBinding().detailToolbar.title = genre.name.resolve(requireContext())
|
||||
requireBinding().detailNormalToolbar.title = genre.name.resolve(requireContext())
|
||||
genreHeaderAdapter.setParent(genre)
|
||||
}
|
||||
|
||||
|
@ -260,6 +260,13 @@ class GenreDetailFragment :
|
|||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
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)
|
||||
|
||||
// --- UI SETUP ---
|
||||
binding.detailToolbar.apply {
|
||||
binding.detailNormalToolbar.apply {
|
||||
inflateMenu(R.menu.menu_playlist_detail)
|
||||
setNavigationOnClickListener { findNavController().navigateUp() }
|
||||
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
||||
}
|
||||
|
||||
binding.detailEditToolbar.apply {
|
||||
setNavigationOnClickListener { detailModel.dropPlaylistEdit() }
|
||||
setOnMenuItemClickListener(this@PlaylistDetailFragment)
|
||||
}
|
||||
|
||||
binding.detailRecycler.apply {
|
||||
adapter = ConcatAdapter(playlistHeaderAdapter, playlistListAdapter)
|
||||
touchHelper =
|
||||
|
@ -139,7 +144,7 @@ class PlaylistDetailFragment :
|
|||
|
||||
override fun onDestroyBinding(binding: FragmentDetailBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.detailToolbar.setOnMenuItemClickListener(null)
|
||||
binding.detailNormalToolbar.setOnMenuItemClickListener(null)
|
||||
touchHelper = null
|
||||
binding.detailRecycler.adapter = null
|
||||
// Avoid possible race conditions that could cause a bad replace instruction to be consumed
|
||||
|
@ -159,7 +164,8 @@ class PlaylistDetailFragment :
|
|||
initialNavDestinationChange = true
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -188,6 +194,10 @@ class PlaylistDetailFragment :
|
|||
musicModel.deletePlaylist(currentPlaylist)
|
||||
true
|
||||
}
|
||||
R.id.action_save -> {
|
||||
detailModel.savePlaylistEdit()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
@ -214,21 +224,10 @@ class PlaylistDetailFragment :
|
|||
}
|
||||
|
||||
override fun onStartEdit() {
|
||||
selectionModel.drop()
|
||||
detailModel.startPlaylistEdit()
|
||||
}
|
||||
|
||||
override fun onConfirmEdit() {
|
||||
detailModel.confirmPlaylistEdit()
|
||||
}
|
||||
|
||||
override fun onDropEdit() {
|
||||
detailModel.dropPlaylistEdit()
|
||||
}
|
||||
|
||||
override fun onOpenSortMenu(anchor: View) {
|
||||
throw IllegalStateException()
|
||||
}
|
||||
override fun onOpenSortMenu(anchor: View) {}
|
||||
|
||||
private fun updatePlaylist(playlist: Playlist?) {
|
||||
if (playlist == null) {
|
||||
|
@ -236,7 +235,9 @@ class PlaylistDetailFragment :
|
|||
findNavController().navigateUp()
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -279,18 +280,35 @@ class PlaylistDetailFragment :
|
|||
}
|
||||
|
||||
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)
|
||||
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>) {
|
||||
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 {
|
||||
/** Called when the "edit" option is selected in the edit header. */
|
||||
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)
|
||||
setOnClickListener { listener.onStartEdit() }
|
||||
}
|
||||
binding.headerConfirm.apply {
|
||||
binding.headerSort.apply {
|
||||
TooltipCompat.setTooltipText(this, contentDescription)
|
||||
setOnClickListener { listener.onConfirmEdit() }
|
||||
}
|
||||
binding.headerCancel.apply {
|
||||
TooltipCompat.setTooltipText(this, contentDescription)
|
||||
setOnClickListener { listener.onDropEdit() }
|
||||
setOnClickListener(listener::onOpenSortMenu)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,12 +176,8 @@ private class EditHeaderViewHolder private constructor(private val binding: Item
|
|||
isGone = editing
|
||||
jumpDrawablesToCurrentState()
|
||||
}
|
||||
binding.headerConfirm.apply {
|
||||
isVisible = editing
|
||||
jumpDrawablesToCurrentState()
|
||||
}
|
||||
binding.headerCancel.apply {
|
||||
isVisible = editing
|
||||
binding.headerSort.apply {
|
||||
isGone = !editing
|
||||
jumpDrawablesToCurrentState()
|
||||
}
|
||||
}
|
||||
|
@ -238,6 +226,7 @@ private constructor(private val binding: ItemEditableSongBinding) :
|
|||
elevation = binding.context.getDimen(R.dimen.elevation_normal)
|
||||
alpha = 0
|
||||
}
|
||||
|
||||
init {
|
||||
binding.body.background =
|
||||
LayerDrawable(
|
||||
|
|
|
@ -102,7 +102,7 @@ class HomeFragment :
|
|||
|
||||
// --- UI SETUP ---
|
||||
binding.homeAppbar.addOnOffsetChangedListener(this)
|
||||
binding.homeToolbar.apply {
|
||||
binding.homeNormalToolbar.apply {
|
||||
setOnMenuItemClickListener(this@HomeFragment)
|
||||
MenuCompat.setGroupDividerEnabled(menu, true)
|
||||
}
|
||||
|
@ -169,7 +169,7 @@ class HomeFragment :
|
|||
super.onDestroyBinding(binding)
|
||||
storagePermissionLauncher = null
|
||||
binding.homeAppbar.removeOnOffsetChangedListener(this)
|
||||
binding.homeToolbar.setOnMenuItemClickListener(null)
|
||||
binding.homeNormalToolbar.setOnMenuItemClickListener(null)
|
||||
}
|
||||
|
||||
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,
|
||||
// the alpha transition is shifted such that the Toolbar becomes fully transparent
|
||||
// when the AppBarLayout is only at half-collapsed.
|
||||
binding.homeSelectionToolbar.alpha =
|
||||
1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2))
|
||||
binding.homeToolbar.alpha = 1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2))
|
||||
binding.homeContent.updatePadding(
|
||||
bottom = binding.homeAppbar.totalScrollRange + verticalOffset)
|
||||
}
|
||||
|
@ -243,7 +242,7 @@ class HomeFragment :
|
|||
binding.homePager.adapter =
|
||||
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) {
|
||||
// A single tab makes the tab layout redundant, hide it and disable the collapsing
|
||||
// behavior.
|
||||
|
@ -285,7 +284,7 @@ class HomeFragment :
|
|||
}
|
||||
|
||||
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)
|
||||
|
||||
for (option in sortMenu) {
|
||||
|
@ -456,11 +455,15 @@ class HomeFragment :
|
|||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
val binding = requireBinding()
|
||||
if (binding.homeSelectionToolbar.updateSelectionAmount(selected.size) &&
|
||||
selected.isNotEmpty()) {
|
||||
// New selection started, show the AppBarLayout to indicate the new state.
|
||||
logD("Significant selection occurred, expanding AppBar")
|
||||
binding.homeAppbar.expandWithScrollingRecycler()
|
||||
if (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.
|
||||
logD("Significant selection occurred, expanding AppBar")
|
||||
binding.homeAppbar.expandWithScrollingRecycler()
|
||||
}
|
||||
} else {
|
||||
binding.homeToolbar.setVisible(R.id.home_normal_toolbar)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,6 +50,7 @@ import okio.buffer
|
|||
import okio.source
|
||||
import org.oxycblt.auxio.image.CoverMode
|
||||
import org.oxycblt.auxio.image.ImageSettings
|
||||
import org.oxycblt.auxio.list.Sort
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
@ -66,10 +67,10 @@ constructor(
|
|||
val albums = computeAlbumOrdering(songs)
|
||||
val streams = mutableListOf<InputStream>()
|
||||
for (album in albums) {
|
||||
openInputStream(album)?.let(streams::add)
|
||||
if (streams.size == 4) {
|
||||
return createMosaic(streams, size)
|
||||
}
|
||||
openInputStream(album)?.let(streams::add)
|
||||
}
|
||||
|
||||
return streams.firstOrNull()?.let { stream ->
|
||||
|
@ -81,7 +82,7 @@ constructor(
|
|||
}
|
||||
|
||||
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? =
|
||||
try {
|
||||
|
|
|
@ -39,20 +39,13 @@ abstract class SelectionFragment<VB : ViewBinding> :
|
|||
protected abstract val musicModel: MusicViewModel
|
||||
protected abstract val playbackModel: PlaybackViewModel
|
||||
|
||||
/**
|
||||
* 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
|
||||
open fun getSelectionToolbar(binding: VB): Toolbar? = null
|
||||
|
||||
override fun onBindingCreated(binding: VB, savedInstanceState: Bundle?) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
getSelectionToolbar(binding)?.apply {
|
||||
// Add cancel and menu item listeners to manage what occurs with the selection.
|
||||
setOnSelectionCancelListener { selectionModel.drop() }
|
||||
setNavigationOnClickListener { selectionModel.drop() }
|
||||
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)
|
||||
|
||||
binding.searchToolbar.apply {
|
||||
binding.searchNormalToolbar.apply {
|
||||
// Initialize the current filtering mode.
|
||||
menu.findItem(searchModel.getFilterOptionId()).isChecked = true
|
||||
|
||||
|
@ -126,7 +126,7 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
|||
|
||||
override fun onDestroyBinding(binding: FragmentSearchBinding) {
|
||||
super.onDestroyBinding(binding)
|
||||
binding.searchToolbar.setOnMenuItemClickListener(null)
|
||||
binding.searchNormalToolbar.setOnMenuItemClickListener(null)
|
||||
binding.searchRecycler.adapter = null
|
||||
}
|
||||
|
||||
|
@ -198,10 +198,16 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
|
|||
|
||||
private fun updateSelection(selected: List<Music>) {
|
||||
searchAdapter.setSelected(selected.toSet())
|
||||
if (requireBinding().searchSelectionToolbar.updateSelectionAmount(selected.size) &&
|
||||
selected.isNotEmpty()) {
|
||||
// Make selection of obscured items easier by hiding the keyboard.
|
||||
hideKeyboard()
|
||||
val binding = requireBinding()
|
||||
if (selected.isNotEmpty()) {
|
||||
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()
|
||||
}
|
||||
} 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:liftOnScrollTargetViewId="@id/detail_recycler">
|
||||
|
||||
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||
android:id="@+id/detail_selection_toolbar"
|
||||
<org.oxycblt.auxio.ui.MultiToolbar
|
||||
android:id="@+id/detail_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/detail_toolbar"
|
||||
android:id="@+id/detail_normal_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
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>
|
||||
|
||||
|
|
|
@ -12,20 +12,29 @@
|
|||
android:id="@+id/home_appbar"
|
||||
style="@style/Widget.Auxio.AppBarLayout">
|
||||
|
||||
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||
android:id="@+id/home_selection_toolbar"
|
||||
<org.oxycblt.auxio.ui.MultiToolbar
|
||||
android:id="@+id/home_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/home_toolbar"
|
||||
android:id="@+id/home_normal_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_scrollFlags="scroll|enterAlways"
|
||||
app:menu="@menu/menu_home"
|
||||
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
|
||||
android:id="@+id/home_tabs"
|
||||
|
|
|
@ -12,13 +12,13 @@
|
|||
app:liftOnScroll="true"
|
||||
app:liftOnScrollTargetViewId="@id/search_recycler">
|
||||
|
||||
<org.oxycblt.auxio.list.selection.SelectionToolbarOverlay
|
||||
android:id="@+id/search_selection_toolbar"
|
||||
<org.oxycblt.auxio.ui.MultiToolbar
|
||||
android:id="@+id/search_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/search_toolbar"
|
||||
android:id="@+id/search_normal_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:menu="@menu/menu_search"
|
||||
|
@ -49,7 +49,16 @@
|
|||
|
||||
</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>
|
||||
|
||||
|
|
|
@ -29,22 +29,13 @@
|
|||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
<org.oxycblt.auxio.ui.RippleFixMaterialButton
|
||||
android:id="@+id/header_confirm"
|
||||
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"
|
||||
android:id="@+id/header_sort"
|
||||
style="@style/Widget.Auxio.Button.Icon.Small"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/spacing_mid_medium"
|
||||
android:contentDescription="@string/lbl_cancel"
|
||||
app:icon="@drawable/ic_close_24"
|
||||
app:icon="@drawable/ic_sort_24"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</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