Added a playlist list specific context menu, which allows the user to

delete the selected playlist, or play it immediately.
This commit is contained in:
casey langen 2017-11-18 15:26:24 -08:00
parent 6573194dac
commit 8534225c55
10 changed files with 255 additions and 100 deletions

View File

@ -9,7 +9,7 @@ interface IPlaybackService {
fun playAll()
fun playAll(index: Int, filter: String)
fun play(category: String, categoryId: Long, index: Int, filter: String)
fun play(category: String, categoryId: Long, index: Int = 0, filter: String = "")
fun playAt(index: Int)
fun pauseOrResume()

View File

@ -15,10 +15,7 @@ import io.casey.musikcube.remote.ui.category.adapter.CategoryBrowseAdapter
import io.casey.musikcube.remote.ui.shared.activity.BaseActivity
import io.casey.musikcube.remote.ui.shared.activity.Filterable
import io.casey.musikcube.remote.ui.shared.constants.Navigation
import io.casey.musikcube.remote.ui.shared.extension.addTransportFragment
import io.casey.musikcube.remote.ui.shared.extension.enableUpNavigation
import io.casey.musikcube.remote.ui.shared.extension.initSearchMenu
import io.casey.musikcube.remote.ui.shared.extension.setupDefaultRecyclerView
import io.casey.musikcube.remote.ui.shared.extension.*
import io.casey.musikcube.remote.ui.shared.fragment.TransportFragment
import io.casey.musikcube.remote.ui.shared.mixin.DataProviderMixin
import io.casey.musikcube.remote.ui.shared.mixin.ItemContextMenuMixin
@ -64,7 +61,7 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
adapter = CategoryBrowseAdapter(eventListener, playback, category)
setContentView(R.layout.recycler_view_activity)
setTitle(categoryTitleStringId)
setTitleFromIntent(categoryTitleStringId)
val recyclerView = findViewById<FastScrollRecyclerView>(R.id.recycler_view)
setupDefaultRecyclerView(recyclerView, adapter)
@ -187,6 +184,7 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
private val EXTRA_PREDICATE_TYPE = "extra_predicate_type"
private val EXTRA_PREDICATE_ID = "extra_predicate_id"
private val EXTRA_NAVIGATION_TYPE = "extra_navigation_type"
private val EXTRA_TITLE = "extra_title"
private val CATEGORY_NAME_TO_TITLE: Map<String, Int> = mapOf(
Messages.Category.ALBUM_ARTIST to R.string.artists_title,
@ -209,10 +207,11 @@ class CategoryBrowseActivity : BaseActivity(), Filterable {
.putExtra(EXTRA_PREDICATE_ID, predicateId)
}
fun getStartIntent(context: Context, category: String, navigationType: NavigationType): Intent {
fun getStartIntent(context: Context, category: String, navigationType: NavigationType, title: String = ""): Intent {
return Intent(context, CategoryBrowseActivity::class.java)
.putExtra(EXTRA_CATEGORY, category)
.putExtra(EXTRA_NAVIGATION_TYPE, navigationType.ordinal)
.putExtra(EXTRA_TITLE, title)
}
}
}

View File

@ -6,7 +6,6 @@ import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.service.websocket.model.ICategoryValue
import io.casey.musikcube.remote.ui.shared.extension.fallback
import io.casey.musikcube.remote.ui.shared.extension.getColorCompat
@ -58,7 +57,7 @@ class CategoryBrowseAdapter(private val listener: EventListener,
internal fun bind(categoryValue: ICategoryValue) {
action.tag = categoryValue
action.visibility = if (category == Messages.Category.PLAYLISTS) View.GONE else View.VISIBLE
action.visibility = View.VISIBLE
val playing = playback.service.playingTrack
val playingId = playing.getCategoryId(category)

View File

@ -1,6 +1,5 @@
package io.casey.musikcube.remote.ui.shared.extension
import android.app.Activity
import android.app.SearchManager
import android.content.Context
import android.support.design.widget.Snackbar
@ -41,21 +40,17 @@ fun AppCompatActivity.setupDefaultRecyclerView(
recyclerView.addItemDecoration(dividerItemDecoration)
}
fun RecyclerView.ViewHolder.getColorCompat(resourceId: Int): Int {
return ContextCompat.getColor(itemView.context, resourceId)
}
fun RecyclerView.ViewHolder.getColorCompat(resourceId: Int): Int =
ContextCompat.getColor(itemView.context, resourceId)
fun View.getColorCompat(resourceId: Int): Int {
return ContextCompat.getColor(context, resourceId)
}
fun View.getColorCompat(resourceId: Int): Int =
ContextCompat.getColor(context, resourceId)
fun Fragment.getColorCompat(resourceId: Int): Int {
return ContextCompat.getColor(activity, resourceId)
}
fun Fragment.getColorCompat(resourceId: Int): Int =
ContextCompat.getColor(activity, resourceId)
fun AppCompatActivity.getColorCompat(resourceId: Int): Int {
return ContextCompat.getColor(this, resourceId)
}
fun AppCompatActivity.getColorCompat(resourceId: Int): Int =
ContextCompat.getColor(this, resourceId)
fun AppCompatActivity.enableUpNavigation() {
val ab = this.supportActionBar
@ -83,14 +78,12 @@ fun AppCompatActivity.addTransportFragment(
}
fun AppCompatActivity.setTitleFromIntent(defaultId: Int) {
fun AppCompatActivity.setTitleFromIntent(defaultId: Int) =
this.setTitleFromIntent(getString(defaultId))
fun AppCompatActivity.setTitleFromIntent(defaultTitle: String) {
val title = this.intent.getStringExtra(EXTRA_ACTIVITY_TITLE)
if (Strings.notEmpty(title)) {
this.title = title
}
else {
this.setTitle(defaultId)
}
this.title = if (Strings.notEmpty(title)) title else defaultTitle
}
fun AppCompatActivity.initSearchMenu(menu: Menu, filterable: Filterable?) {
@ -103,9 +96,7 @@ fun AppCompatActivity.initSearchMenu(menu: Menu, filterable: Filterable?) {
if (filterable != null) {
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
return false
}
override fun onQueryTextSubmit(query: String): Boolean = false
override fun onQueryTextChange(newText: String): Boolean {
filterable.setFilter(newText)
@ -142,9 +133,7 @@ fun View.setVisible(visible: Boolean) {
this.visibility = if (visible) View.VISIBLE else View.GONE
}
fun AppCompatActivity.dpToPx(dp: Float): Float {
return dp * this.resources.displayMetrics.density
}
fun AppCompatActivity.dpToPx(dp: Float): Float = dp * this.resources.displayMetrics.density
fun showKeyboard(context: Context) {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
@ -156,22 +145,17 @@ fun hideKeyboard(context: Context, view: View) {
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
fun AppCompatActivity.showKeyboard() {
showKeyboard(this)
}
fun AppCompatActivity.showKeyboard() = showKeyboard(this)
fun AppCompatActivity.hideKeyboard(view: View? = null) {
val v = view ?: this.findViewById(android.R.id.content)
hideKeyboard(this, v)
}
fun DialogFragment.showKeyboard() {
showKeyboard(activity)
}
fun DialogFragment.showKeyboard() = showKeyboard(activity)
fun DialogFragment.hideKeyboard() {
fun DialogFragment.hideKeyboard() =
hideKeyboard(activity, activity.findViewById(android.R.id.content))
}
fun AppCompatActivity.dialogVisible(tag: String): Boolean =
this.supportFragmentManager.findFragmentByTag(tag) != null
@ -190,17 +174,14 @@ fun showSnackbar(view: View, stringId: Int, bgColor: Int, fgColor: Int) {
sb.show()
}
fun showSnackbar(view: View, stringId: Int) {
fun showSnackbar(view: View, stringId: Int) =
showSnackbar(view, stringId, R.color.color_primary, R.color.theme_foreground)
}
fun showErrorSnackbar(view: View, stringId: Int) {
fun showErrorSnackbar(view: View, stringId: Int) =
showSnackbar(view, stringId, R.color.theme_red, R.color.theme_foreground)
}
fun AppCompatActivity.showSnackbar(viewId: Int, stringId: Int) {
fun AppCompatActivity.showSnackbar(viewId: Int, stringId: Int) =
showSnackbar(this.findViewById<View>(viewId), stringId)
}
fun fallback(input: String?, fallback: String): String =
if (input.isNullOrEmpty()) fallback else input!!

View File

@ -0,0 +1,58 @@
package io.casey.musikcube.remote.ui.shared.fragment
import android.content.Intent
import android.os.Bundle
import android.support.v4.app.DialogFragment
import io.casey.musikcube.remote.framework.IMixin
import io.casey.musikcube.remote.framework.MixinSet
import io.casey.musikcube.remote.framework.ViewModel
import io.casey.musikcube.remote.ui.shared.mixin.ViewModelMixin
open class BaseDialogFragment: DialogFragment(), ViewModel.Provider {
private val mixins = MixinSet()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mixins.onCreate(savedInstanceState ?: Bundle())
}
override fun onStart() {
super.onStart()
mixins.onStart()
}
override fun onResume() {
super.onResume()
mixins.onResume()
}
override fun onPause() {
super.onPause()
mixins.onPause()
}
override fun onStop() {
super.onStop()
mixins.onStop()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
mixins.onActivityResult(requestCode, resultCode, data)
}
override fun onSaveInstanceState(outState: Bundle?) {
super.onSaveInstanceState(outState)
mixins.onSaveInstanceState(outState ?: Bundle())
}
override fun onDestroy() {
super.onDestroy()
mixins.onDestroy()
}
override fun <T: ViewModel<*>> createViewModel(): T? = null
protected fun <T: ViewModel<*>> getViewModel(): T? = mixin(ViewModelMixin::class.java)?.get<T>() as T
protected fun <T: IMixin> mixin(mixin: T): T = mixins.add(mixin)
protected fun <T: IMixin> mixin(cls: Class<out T>): T? = mixins.get(cls)
}

View File

@ -8,7 +8,6 @@ import io.casey.musikcube.remote.framework.MixinSet
import io.casey.musikcube.remote.framework.ViewModel
import io.casey.musikcube.remote.ui.shared.mixin.ViewModelMixin
open class BaseFragment: Fragment(), ViewModel.Provider {
private val mixins = MixinSet()

View File

@ -1,7 +1,12 @@
package io.casey.musikcube.remote.ui.shared.mixin
import android.app.Activity
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.support.v7.app.AlertDialog
import android.support.v7.app.AppCompatActivity
import android.view.View
import android.widget.PopupMenu
import io.casey.musikcube.remote.Application
@ -9,6 +14,7 @@ import io.casey.musikcube.remote.R
import io.casey.musikcube.remote.framework.MixinBase
import io.casey.musikcube.remote.injection.DaggerViewComponent
import io.casey.musikcube.remote.injection.DataModule
import io.casey.musikcube.remote.service.playback.PlaybackServiceFactory
import io.casey.musikcube.remote.service.websocket.Messages
import io.casey.musikcube.remote.service.websocket.model.ICategoryValue
import io.casey.musikcube.remote.service.websocket.model.IDataProvider
@ -17,14 +23,21 @@ import io.casey.musikcube.remote.ui.albums.activity.AlbumBrowseActivity
import io.casey.musikcube.remote.ui.category.activity.CategoryBrowseActivity
import io.casey.musikcube.remote.ui.shared.extension.showErrorSnackbar
import io.casey.musikcube.remote.ui.shared.extension.showSnackbar
import io.casey.musikcube.remote.ui.shared.fragment.BaseDialogFragment
import io.casey.musikcube.remote.ui.tracks.activity.TrackListActivity
import io.reactivex.Observable
import io.reactivex.rxkotlin.subscribeBy
import javax.inject.Inject
class ItemContextMenuMixin(private val activity: Activity): MixinBase() {
class ItemContextMenuMixin(private val activity: AppCompatActivity,
internal val listener: Listener? = null): MixinBase() {
@Inject lateinit var provider: IDataProvider
interface Listener {
fun onPlaylistDeleted()
fun onPlaylistCreated()
}
private var pendingCode = -1
private var completion: ((Long) -> Unit)? = null
@ -36,6 +49,11 @@ class ItemContextMenuMixin(private val activity: Activity): MixinBase() {
.inject(this)
}
override fun onCreate(bundle: Bundle) {
super.onCreate(bundle)
ConfirmDeletePlaylistDialog.rebind(activity, listener)
}
override fun onResume() {
super.onResume()
provider.attach()
@ -66,23 +84,23 @@ class ItemContextMenuMixin(private val activity: Activity): MixinBase() {
super.onActivityResult(request, result, data)
}
fun add(track: ITrack) {
add(listOf(track))
fun addToPlaylist(track: ITrack) {
addToPlaylist(listOf(track))
}
fun add(tracks: List<ITrack>) {
fun addToPlaylist(tracks: List<ITrack>) {
showPlaylistChooser { id ->
addWithErrorHandler(provider.appendToPlaylist(id, tracks))
}
}
fun add(categoryType: String, categoryId: Long) {
fun addToPlaylist(categoryType: String, categoryId: Long) {
showPlaylistChooser { id ->
addWithErrorHandler(provider.appendToPlaylist(id, categoryType, categoryId))
}
}
fun add(category: ICategoryValue) {
fun addToPlaylist(category: ICategoryValue) {
showPlaylistChooser { id ->
addWithErrorHandler(provider.appendToPlaylist(id, category))
}
@ -103,22 +121,22 @@ class ItemContextMenuMixin(private val activity: Activity): MixinBase() {
val intent = CategoryBrowseActivity.getStartIntent(
activity,
Messages.Category.PLAYLISTS,
CategoryBrowseActivity.NavigationType.Select)
CategoryBrowseActivity.NavigationType.Select,
activity.getString(R.string.playlist_edit_pick_playlist))
activity.startActivityForResult(intent, pendingCode)
}
fun showForTrack(track: ITrack, anchorView: View)
{
fun showForTrack(track: ITrack, anchorView: View) {
val popup = PopupMenu(activity, anchorView)
popup.inflate(R.menu.item_context_menu)
popup.inflate(R.menu.generic_item_context_menu)
popup.menu.removeItem(R.id.menu_show_tracks)
popup.setOnMenuItemClickListener { item ->
val intent: Intent? = when (item.itemId) {
R.id.menu_add_to_playlist -> {
add(track)
addToPlaylist(track)
null
}
R.id.menu_show_albums -> {
@ -153,10 +171,33 @@ class ItemContextMenuMixin(private val activity: Activity): MixinBase() {
popup.show()
}
fun showForCategory(value: ICategoryValue, anchorView: View)
{
fun showForPlaylist(playlistName: String, playlistId: Long, anchorView: View) {
val popup = PopupMenu(activity, anchorView)
popup.inflate(R.menu.item_context_menu)
popup.inflate(R.menu.playlist_item_context_menu)
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.menu_playlist_delete -> {
ConfirmDeletePlaylistDialog.show(activity, listener, playlistName, playlistId)
}
R.id.menu_playlist_play -> {
val playback = PlaybackServiceFactory.instance(Application.instance!!)
playback.play(Messages.Category.PLAYLISTS, playlistId)
}
}
true
}
popup.show()
}
fun showForCategory(value: ICategoryValue, anchorView: View) {
if (value.type == Messages.Category.PLAYLISTS) {
showForPlaylist(value.value, value.id, anchorView)
}
else {
val popup = PopupMenu(activity, anchorView)
popup.inflate(R.menu.generic_item_context_menu)
if (value.type != Messages.Category.GENRE) {
popup.menu.removeItem(R.id.menu_show_artists)
@ -171,7 +212,7 @@ class ItemContextMenuMixin(private val activity: Activity): MixinBase() {
popup.setOnMenuItemClickListener { item ->
val intent: Intent? = when (item.itemId) {
R.id.menu_add_to_playlist -> {
add(value)
addToPlaylist(value)
null
}
R.id.menu_show_albums -> {
@ -202,6 +243,7 @@ class ItemContextMenuMixin(private val activity: Activity): MixinBase() {
popup.show()
}
}
private fun showSuccess() {
showSnackbar(
@ -213,6 +255,69 @@ class ItemContextMenuMixin(private val activity: Activity): MixinBase() {
showErrorSnackbar(activity.findViewById(android.R.id.content), message)
}
class ConfirmDeletePlaylistDialog : BaseDialogFragment() {
private var listener: Listener? = null
override fun onCreate(savedInstanceState: Bundle?) {
mixin(DataProviderMixin())
super.onCreate(savedInstanceState)
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val playlistName = arguments.getString(EXTRA_PLAYLIST_NAME, "")
val dlg = AlertDialog.Builder(activity)
.setTitle(R.string.playlist_confirm_delete_title)
.setMessage(getString(R.string.playlist_confirm_delete_message, playlistName))
.setNegativeButton(R.string.button_no, null)
.setPositiveButton(R.string.button_yes, positiveListener)
.create()
dlg.setCancelable(false)
return dlg
}
private val positiveListener = { dialog: DialogInterface, which: Int ->
val playlistId = arguments.getLong(EXTRA_PLAYLIST_ID, -1)
val provider = mixin(DataProviderMixin::class.java)?.provider
if (provider != null && playlistId != -1L) {
provider.deletePlaylist(playlistId).subscribeBy(
onNext = { listener?.onPlaylistDeleted() },
onError = { }
)}
}
companion object {
val TAG = "confirm_delete_playlist_dialog"
private val EXTRA_PLAYLIST_ID = "extra_playlist_id"
private val EXTRA_PLAYLIST_NAME = "extra_playlist_name"
private fun find(activity: AppCompatActivity): ConfirmDeletePlaylistDialog? =
activity.supportFragmentManager.findFragmentByTag(TAG) as ConfirmDeletePlaylistDialog?
fun rebind(activity: AppCompatActivity, listener: Listener?) {
val dlg = find(activity)
if (dlg != null) {
dlg.listener = listener
}
}
fun show(activity: AppCompatActivity, listener: Listener?, name: String, id: Long) {
val existing = find(activity)
existing?.dismiss()
val args = Bundle()
args.putString(EXTRA_PLAYLIST_NAME, name)
args.putLong(EXTRA_PLAYLIST_ID, id)
val result = ConfirmDeletePlaylistDialog()
result.arguments = args
result.listener = listener
result.show(activity.supportFragmentManager, TAG)
}
}
}
companion object {
private val REQUEST_ADD_TO_PLAYLIST = 128
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_playlist_play"
android:title="@string/menu_play_playlist"/>
<item
android:id="@+id/menu_playlist_delete"
android:title="@string/menu_delete_playlist"/>
</menu>

View File

@ -68,6 +68,8 @@
<string name="menu_show_albums">albums</string>
<string name="menu_show_artists">artist</string>
<string name="menu_show_genres">genres</string>
<string name="menu_delete_playlist">delete</string>
<string name="menu_play_playlist">play now</string>
<string name="unknown_value">&lt;unknown&gt;</string>
<string name="snackbar_streaming_enabled">switched to streaming mode</string>
<string name="snackbar_remote_enabled">switched to remote control mode</string>
@ -118,5 +120,7 @@
<string name="playlist_edit_no_playlists">couldn\'t get playlists from server</string>
<string name="playlist_edit_add_error">playlist update failed</string>
<string name="playlist_edit_add_success">playlist updated</string>
<string name="playlist_edit_list_title">pick a playlist</string>
<string name="playlist_edit_pick_playlist">pick a playlist</string>
<string name="playlist_confirm_delete_title">confirm delete</string>
<string name="playlist_confirm_delete_message">are you sure you want to delete playlist \'%s\'?</string>
</resources>