Create dedicated ActionMenu object

Create a dedicated object for every ActionMenu to reduce on code clutter caused by duplicated PopupMenu code.
This commit is contained in:
OxygenCobalt 2021-01-04 11:19:46 -07:00
parent f109130fb8
commit 119078fc77
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
7 changed files with 205 additions and 281 deletions

View file

@ -4,7 +4,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
@ -16,8 +15,9 @@ import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.recycler.CenterSmoothScroller import org.oxycblt.auxio.recycler.CenterSmoothScroller
import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.createToast import org.oxycblt.auxio.ui.createToast
import org.oxycblt.auxio.ui.setupAlbumSongActions import org.oxycblt.auxio.ui.requireCompatActivity
/** /**
* The [DetailFragment] for an album. * The [DetailFragment] for an album.
@ -47,9 +47,7 @@ class AlbumDetailFragment : DetailFragment() {
detailModel, playbackModel, viewLifecycleOwner, detailModel, playbackModel, viewLifecycleOwner,
doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) }, doOnClick = { playbackModel.playSong(it, PlaybackMode.IN_ALBUM) },
doOnLongClick = { data, view -> doOnLongClick = { data, view ->
PopupMenu(requireContext(), view).setupAlbumSongActions( ActionMenu(requireCompatActivity(), view, data, ActionMenu.FLAG_IN_ALBUM)
requireContext(), data, detailModel, playbackModel
)
} }
) )

View file

@ -4,7 +4,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.detail.adapters.ArtistDetailAdapter import org.oxycblt.auxio.detail.adapters.ArtistDetailAdapter
@ -14,7 +13,8 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.setupArtistAlbumActions import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.requireCompatActivity
/** /**
* The [DetailFragment] for an artist. * The [DetailFragment] for an artist.
@ -53,9 +53,7 @@ class ArtistDetailFragment : DetailFragment() {
} }
}, },
doOnLongClick = { data, view -> doOnLongClick = { data, view ->
PopupMenu(requireContext(), view).setupArtistAlbumActions( ActionMenu(requireCompatActivity(), view, data, ActionMenu.FLAG_IN_ARTIST)
requireContext(), data, playbackModel
)
} }
) )

View file

@ -4,7 +4,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs import androidx.navigation.fragment.navArgs
import org.oxycblt.auxio.detail.adapters.GenreDetailAdapter import org.oxycblt.auxio.detail.adapters.GenreDetailAdapter
@ -14,7 +13,8 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.state.PlaybackMode import org.oxycblt.auxio.playback.state.PlaybackMode
import org.oxycblt.auxio.ui.setupGenreSongActions import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.requireCompatActivity
/** /**
* The [DetailFragment] for a genre. * The [DetailFragment] for a genre.
@ -46,9 +46,7 @@ class GenreDetailFragment : DetailFragment() {
playbackModel.playSong(it, PlaybackMode.IN_GENRE) playbackModel.playSong(it, PlaybackMode.IN_GENRE)
}, },
doOnLongClick = { data, view -> doOnLongClick = { data, view ->
PopupMenu(requireContext(), view).setupGenreSongActions( ActionMenu(requireCompatActivity(), view, data, ActionMenu.FLAG_IN_GENRE)
requireContext(), data, playbackModel, detailModel
)
} }
) )

View file

@ -5,7 +5,6 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -28,15 +27,13 @@ import org.oxycblt.auxio.music.Header
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.accent import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.ui.applyColor import org.oxycblt.auxio.ui.applyColor
import org.oxycblt.auxio.ui.getLandscapeSpans import org.oxycblt.auxio.ui.getLandscapeSpans
import org.oxycblt.auxio.ui.isLandscape import org.oxycblt.auxio.ui.isLandscape
import org.oxycblt.auxio.ui.requireCompatActivity
import org.oxycblt.auxio.ui.resolveAttr import org.oxycblt.auxio.ui.resolveAttr
import org.oxycblt.auxio.ui.setupAlbumActions
import org.oxycblt.auxio.ui.setupArtistActions
import org.oxycblt.auxio.ui.setupGenreActions
import org.oxycblt.auxio.ui.setupSongActions
import org.oxycblt.auxio.ui.toColor import org.oxycblt.auxio.ui.toColor
/** /**
@ -197,22 +194,12 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
} }
/** /**
* Show the [PopupMenu] actions for an item. * Show the [ActionMenu] actions for an item.
* @param data The model that the actions should correspond to * @param data The model that the actions should correspond to
* @param view The anchor view the menu should be bound to. * @param view The anchor view the menu should be bound to.
*/ */
private fun showActionsForItem(data: BaseModel, view: View) { private fun showActionsForItem(data: BaseModel, view: View) {
val menu = PopupMenu(requireContext(), view) ActionMenu(requireCompatActivity(), view, data, ActionMenu.FLAG_NONE)
when (data) {
is Song -> menu.setupSongActions(requireContext(), data, playbackModel, detailModel)
is Album -> menu.setupAlbumActions(requireContext(), data, playbackModel, detailModel)
is Artist -> menu.setupArtistActions(data, playbackModel)
is Genre -> menu.setupGenreActions(data, playbackModel)
else -> {
}
}
} }
/** /**

View file

@ -7,7 +7,6 @@ import android.util.TypedValue
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
@ -21,10 +20,11 @@ import org.oxycblt.auxio.logD
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.ActionMenu
import org.oxycblt.auxio.ui.accent import org.oxycblt.auxio.ui.accent
import org.oxycblt.auxio.ui.getLandscapeSpans import org.oxycblt.auxio.ui.getLandscapeSpans
import org.oxycblt.auxio.ui.isLandscape import org.oxycblt.auxio.ui.isLandscape
import org.oxycblt.auxio.ui.setupSongActions import org.oxycblt.auxio.ui.requireCompatActivity
import org.oxycblt.auxio.ui.toColor import org.oxycblt.auxio.ui.toColor
import kotlin.math.ceil import kotlin.math.ceil
@ -61,9 +61,7 @@ class SongsFragment : Fragment() {
playbackModel.playSong(it, settingsManager.songPlaybackMode) playbackModel.playSong(it, settingsManager.songPlaybackMode)
}, },
doOnLongClick = { data, view -> doOnLongClick = { data, view ->
PopupMenu(requireContext(), view).setupSongActions( ActionMenu(requireCompatActivity(), view, data, ActionMenu.FLAG_NONE)
requireContext(), data, playbackModel, detailModel
)
} }
) )

View file

@ -0,0 +1,180 @@
package org.oxycblt.auxio.ui
import android.view.View
import androidx.annotation.IdRes
import androidx.annotation.MenuRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.ViewModelProvider
import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.DetailViewModel
import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.BaseModel
import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode
/**
* A wrapper around [PopupMenu] that automates a ton of things across all the menus in Auxio
* @param activity [AppCompatActivity] required as both a context and ViewModelStore owner.
* @param anchor [View] This should be centered around
* @param data [BaseModel] this menu corresponds to
* @param flag Any extra flags to accompany the data. See [Companion] for more details.
*/
class ActionMenu(
activity: AppCompatActivity,
anchor: View,
private val data: BaseModel,
private val flag: Int = FLAG_NONE
) : PopupMenu(activity, anchor) {
private val context = activity.applicationContext
private val detailModel: DetailViewModel by lazy {
ViewModelProvider(activity).get(DetailViewModel::class.java)
}
private val playbackModel: PlaybackViewModel by lazy {
ViewModelProvider(activity).get(PlaybackViewModel::class.java)
}
init {
val menuRes = determineMenu()
check(menuRes != -1) { "There is no menu associated with this configuration." }
inflate(menuRes)
setOnMenuItemClickListener {
onMenuClick(it.itemId)
true
}
show()
}
/**
* Figure out what menu to use here, based on the data & flags
*/
@MenuRes
private fun determineMenu(): Int {
return when (data) {
is Song -> {
when (flag) {
FLAG_NONE, FLAG_IN_GENRE -> R.menu.menu_song_actions
FLAG_IN_ALBUM -> R.menu.menu_album_song_actions
else -> -1
}
}
is Album -> {
when (flag) {
FLAG_NONE -> R.menu.menu_album_actions
FLAG_IN_ARTIST -> R.menu.menu_artist_album_actions
else -> -1
}
}
is Artist -> R.menu.menu_artist_actions
is Genre -> R.menu.menu_genre_actions
else -> -1
}
}
private fun onMenuClick(@IdRes id: Int) {
when (id) {
R.id.action_play -> {
when (data) {
is Song -> playbackModel.playSong(data, getPlaybackModeFromFlag())
is Album -> playbackModel.playAlbum(data, false)
is Artist -> playbackModel.playArtist(data, false)
is Genre -> playbackModel.playGenre(data, false)
else -> {}
}
}
R.id.action_shuffle -> {
when (data) {
is Album -> playbackModel.playAlbum(data, true)
is Artist -> playbackModel.playArtist(data, true)
is Genre -> playbackModel.playGenre(data, true)
else -> {}
}
}
R.id.action_play_artist -> {
if (flag == FLAG_IN_ALBUM && data is Song) {
playbackModel.playSong(data, PlaybackMode.IN_ARTIST)
}
}
R.id.action_queue_add -> {
val success = when (data) {
is Song -> {
playbackModel.addToUserQueue(data)
true
}
is Album -> {
playbackModel.addToUserQueue(data)
true
}
else -> false
}
if (success) {
context.getString(R.string.label_queue_added).createToast(context)
}
}
R.id.action_go_album -> {
if (data is Song) {
determineWhereToNavWithSong(data.album)
}
}
R.id.action_go_artist -> {
if (data is Song) {
determineWhereToNavWithSong(data.album.artist)
} else if (data is Album) {
detailModel.navToItem(data.artist)
}
}
}
}
private fun determineWhereToNavWithSong(parent: BaseModel) {
when (flag) {
FLAG_NONE -> detailModel.navToItem(parent)
FLAG_IN_ALBUM -> detailModel.navToParent()
FLAG_IN_GENRE -> detailModel.navToChild(parent)
}
}
private fun getPlaybackModeFromFlag(): PlaybackMode {
return when (flag) {
FLAG_NONE -> PlaybackMode.ALL_SONGS
FLAG_IN_ALBUM -> PlaybackMode.IN_ALBUM
FLAG_IN_ARTIST -> PlaybackMode.IN_ARTIST
FLAG_IN_GENRE -> PlaybackMode.IN_GENRE
else -> PlaybackMode.ALL_SONGS
}
}
companion object {
/** No Flags **/
const val FLAG_NONE = -1
/** Flag for when an item is accessed from an artist **/
const val FLAG_IN_ARTIST = 0
/** Flag for when an item is accessed from an album **/
const val FLAG_IN_ALBUM = 1
/** Flag or when an item is accessed from a genre **/
const val FLAG_IN_GENRE = 2
}
}

View file

@ -14,8 +14,10 @@ import android.widget.Toast
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.MenuRes import androidx.annotation.MenuRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.detail.DetailViewModel import org.oxycblt.auxio.detail.DetailViewModel
@ -24,7 +26,6 @@ import org.oxycblt.auxio.music.Artist
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.playback.PlaybackViewModel import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.state.PlaybackMode
/** /**
* Apply a text color to a [MenuItem] * Apply a text color to a [MenuItem]
@ -107,250 +108,14 @@ fun MaterialButton.applyAccents(highlighted: Boolean) {
} }
/** /**
* Show actions for a song item, such as the ones found in [org.oxycblt.auxio.songs.SongsFragment] * Require an [AppCompatActivity]
* @param context [Context] required
* @param song [Song] The menu should correspond to
* @param playbackModel The [PlaybackViewModel] the menu should dispatch actions to.
* @param detailModel The [DetailViewModel] the menu should dispatch actions to.
*/ */
fun PopupMenu.setupSongActions( fun Fragment.requireCompatActivity(): AppCompatActivity {
context: Context, val activity = requireActivity()
song: Song,
playbackModel: PlaybackViewModel,
detailModel: DetailViewModel
) {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_queue_add -> {
playbackModel.addToUserQueue(song)
context.getString(R.string.label_queue_added).createToast(context)
true
}
R.id.action_go_artist -> { if (activity is AppCompatActivity) {
detailModel.navToItem(song.album.artist) return activity
true } else {
} error("Required activity to be AppCompatActivity, however it wasn't.")
R.id.action_go_album -> {
detailModel.navToItem(song.album)
true
}
else -> false
}
} }
inflateAndShow(R.menu.menu_song_actions)
}
/**
* Show actions for a album song item, such as the ones found in
* [org.oxycblt.auxio.detail.AlbumDetailFragment]
* @param context [Context] required
* @param song [Song] The menu should correspond to
* @param detailModel The [DetailViewModel] the menu should dispatch some actions to.
* @param playbackModel The [PlaybackViewModel] the menu should dispatch actions to.
*/
fun PopupMenu.setupAlbumSongActions(
context: Context,
song: Song,
detailModel: DetailViewModel,
playbackModel: PlaybackViewModel
) {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_queue_add -> {
playbackModel.addToUserQueue(song)
context.getString(R.string.label_queue_added).createToast(context)
true
}
R.id.action_go_artist -> {
detailModel.navToParent()
true
}
R.id.action_play_artist -> {
playbackModel.playSong(song, PlaybackMode.IN_ARTIST)
true
}
else -> false
}
}
inflateAndShow(R.menu.menu_album_song_actions)
}
/**
* Show actions for an [Album].
* @param context [Context] required
* @param album [Album] The menu should correspond to
* @param playbackModel The [PlaybackViewModel] the menu should dispatch actions to.
* @param detailModel The [DetailViewModel] the menu should dispatch actions to.
*/
fun PopupMenu.setupAlbumActions(
context: Context,
album: Album,
playbackModel: PlaybackViewModel,
detailModel: DetailViewModel
) {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_play -> {
playbackModel.playAlbum(album, false)
true
}
R.id.action_shuffle -> {
playbackModel.playAlbum(album, true)
true
}
R.id.action_queue_add -> {
playbackModel.addToUserQueue(album)
context.getString(R.string.label_queue_added).createToast(context)
true
}
R.id.action_go_artist -> {
detailModel.navToItem(album.artist)
true
}
else -> false
}
}
inflateAndShow(R.menu.menu_album_actions)
}
/**
* Show actions for an [Album] in the artist detail fragment
* @param context [Context] required
* @param album [Album] The menu should correspond to
* @param playbackModel The [PlaybackViewModel] the menu should dispatch actions to.
*/
fun PopupMenu.setupArtistAlbumActions(context: Context, album: Album, playbackModel: PlaybackViewModel) {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_play -> {
playbackModel.playAlbum(album, false)
true
}
R.id.action_shuffle -> {
playbackModel.playAlbum(album, true)
true
}
R.id.action_queue_add -> {
playbackModel.addToUserQueue(album)
context.getString(R.string.label_queue_added).createToast(context)
true
}
else -> false
}
}
inflateAndShow(R.menu.menu_artist_album_actions)
}
/**
* Show actions for an [Artist].
* @param artist The [Artist] The menu should correspond to
* @param playbackModel The [PlaybackViewModel] the menu should dispatch actions to.
*/
fun PopupMenu.setupArtistActions(artist: Artist, playbackModel: PlaybackViewModel) {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_play -> {
playbackModel.playArtist(artist, false)
true
}
R.id.action_shuffle -> {
playbackModel.playArtist(artist, true)
true
}
else -> false
}
}
inflateAndShow(R.menu.menu_artist_actions)
}
/**
* Show actions for a [Genre].
* @param genre The [Genre] The menu should correspond to
* @param playbackModel The [PlaybackViewModel] the menu should dispatch actions to.
*/
fun PopupMenu.setupGenreActions(genre: Genre, playbackModel: PlaybackViewModel) {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_play -> {
playbackModel.playGenre(genre, true)
true
}
R.id.action_shuffle -> {
playbackModel.playGenre(genre, true)
true
}
else -> false
}
}
inflateAndShow(R.menu.menu_genre_actions)
}
/**
* Show actions for a [Genre] song. Mostly identical to [setupSongActions] aside from a different
* flag being used for navigation.
* @param context [Context] required
* @param song [Song] The menu should correspond to
* @param playbackModel The [PlaybackViewModel] the menu should dispatch actions to.
* @param detailModel The [DetailViewModel] the menu should dispatch actions to.
*/
fun PopupMenu.setupGenreSongActions(
context: Context,
song: Song,
playbackModel: PlaybackViewModel,
detailModel: DetailViewModel
) {
setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_queue_add -> {
playbackModel.addToUserQueue(song)
context.getString(R.string.label_queue_added).createToast(context)
true
}
R.id.action_go_artist -> {
detailModel.navToChild(song.album.artist)
true
}
R.id.action_go_album -> {
detailModel.navToChild(song.album)
true
}
else -> false
}
}
inflateAndShow(R.menu.menu_song_actions)
}
/**
* Shortcut method that inflates a menu and shows the action menu.
* @param menuRes the menu that should be shown.
*/
private fun PopupMenu.inflateAndShow(@MenuRes menuRes: Int) {
inflate(menuRes)
show()
} }