From 8bb70e7d98ad8cbaaa390a77b8687f6e3b39c6cb Mon Sep 17 00:00:00 2001 From: casey langen Date: Wed, 29 Nov 2017 23:13:26 -0800 Subject: [PATCH] Added "remove (track) from playlist" functionality to the Android client. --- .../ui/shared/mixin/ItemContextMenuMixin.kt | 79 +++++++++++++++++++ .../ui/tracks/activity/TrackListActivity.kt | 63 +++++++++------ .../ui/tracks/adapter/TrackListAdapter.kt | 31 ++++++-- .../main/res/menu/track_item_context_menu.xml | 4 + .../app/src/main/res/values/strings.xml | 3 + 5 files changed, 152 insertions(+), 28 deletions(-) diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ItemContextMenuMixin.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ItemContextMenuMixin.kt index c3e790cbb..aa5677a99 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ItemContextMenuMixin.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/shared/mixin/ItemContextMenuMixin.kt @@ -35,6 +35,8 @@ import javax.inject.Inject class ItemContextMenuMixin(private val activity: AppCompatActivity, internal val listener: EventListener? = null): MixinBase() { + private enum class TrackType { Normal, Playlist } + @Inject lateinit var provider: IDataProvider open class EventListener { @@ -56,6 +58,7 @@ class ItemContextMenuMixin(private val activity: AppCompatActivity, super.onCreate(bundle) ConfirmDeletePlaylistDialog.rebind(activity, this) EnterPlaylistNameDialog.rebind(activity, this) + ConfirmRemoveFromPlaylistDialog.rebind(activity, this) } override fun onResume() { @@ -174,15 +177,32 @@ class ItemContextMenuMixin(private val activity: AppCompatActivity, } fun showForTrack(track: ITrack, anchorView: View) { + showForTrack(track, -1, -1, anchorView, TrackType.Normal) + } + + fun showForPlaylistTrack(track: ITrack, position: Int, playlistId: Long, anchorView: View) { + showForTrack(track, position, playlistId, anchorView, TrackType.Playlist) + } + + private fun showForTrack(track: ITrack, position: Int, categoryId: Long, anchorView: View, type: TrackType) { val popup = PopupMenu(activity, anchorView) popup.inflate(R.menu.track_item_context_menu) + if (type != TrackType.Playlist) { + popup.menu.removeItem(R.id.menu_remove_from_playlist) + } + popup.setOnMenuItemClickListener { item -> val intent: Intent? = when (item.itemId) { R.id.menu_add_to_playlist -> { addToPlaylist(track) null } + R.id.menu_remove_from_playlist -> { + ConfirmRemoveFromPlaylistDialog.show( + activity, this, categoryId, position, track) + null + } R.id.menu_show_artist_albums -> { AlbumBrowseActivity.getStartIntent( activity, Messages.Category.ARTIST, track.artistId, track.artist) @@ -319,6 +339,17 @@ class ItemContextMenuMixin(private val activity: AppCompatActivity, } } + private fun removeFromPlaylistConfirmed(playlistId: Long, externalId: String, position: Int) { + provider.removeTracksFromPlaylist(playlistId, listOf(externalId), listOf(position)).subscribeBy( + onNext = { success -> + listener?.onPlaylistUpdated(playlistId) + }, + onError = { + + } + ) + } + private fun showSuccess(stringId: Int) = showSuccess(activity.getString(stringId)) @@ -331,6 +362,54 @@ class ItemContextMenuMixin(private val activity: AppCompatActivity, private fun showError(message: String) = showErrorSnackbar(activity.findViewById(android.R.id.content), message) + class ConfirmRemoveFromPlaylistDialog : BaseDialogFragment() { + private lateinit var mixin: ItemContextMenuMixin + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val playlistId = arguments.getLong(EXTRA_PLAYLIST_ID, -1) + val trackTitle = arguments.getString(EXTRA_TRACK_TITLE, "") + val trackExternalId = arguments.getString(EXTRA_TRACK_EXTERNAL_ID, "") + val trackPosition = arguments.getInt(EXTRA_TRACK_POSITION, -1) + + val dlg = AlertDialog.Builder(activity) + .setTitle(R.string.playlist_confirm_delete_title) + .setMessage(getString(R.string.playlist_confirm_delete_message, trackTitle)) + .setNegativeButton(R.string.button_no, null) + .setPositiveButton(R.string.button_yes, { _: DialogInterface, _: Int -> + mixin.removeFromPlaylistConfirmed(playlistId, trackExternalId, trackPosition) + }) + .create() + + dlg.setCancelable(false) + return dlg + } + + companion object { + val TAG = "confirm_delete_playlist_dialog" + private val EXTRA_PLAYLIST_ID = "extra_playlist_id" + private val EXTRA_TRACK_TITLE = "extra_track_title" + private val EXTRA_TRACK_EXTERNAL_ID = "extra_track_external_id" + private val EXTRA_TRACK_POSITION = "extra_track_position" + + fun rebind(activity: AppCompatActivity, mixin: ItemContextMenuMixin) { + find(activity, TAG)?.mixin = mixin + } + + fun show(activity: AppCompatActivity, mixin: ItemContextMenuMixin, playlistId: Long, position: Int, track: ITrack) { + dismiss(activity, TAG) + val args = Bundle() + args.putLong(EXTRA_PLAYLIST_ID, playlistId) + args.putString(EXTRA_TRACK_TITLE, track.title) + args.putString(EXTRA_TRACK_EXTERNAL_ID, track.externalId) + args.putInt(EXTRA_TRACK_POSITION, position) + val result = ConfirmRemoveFromPlaylistDialog() + result.arguments = args + result.mixin = mixin + result.show(activity.supportFragmentManager, TAG) + } + } + } + class ConfirmDeletePlaylistDialog : BaseDialogFragment() { private lateinit var mixin: ItemContextMenuMixin diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt index 631f96c91..a4093e1c9 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/activity/TrackListActivity.kt @@ -27,6 +27,7 @@ import io.casey.musikcube.remote.util.Debouncer import io.casey.musikcube.remote.util.Strings import io.reactivex.Observable import io.reactivex.rxkotlin.subscribeBy +import java.util.* class TrackListActivity : BaseActivity(), Filterable { private lateinit var tracks: TrackListSlidingWindow @@ -41,31 +42,10 @@ class TrackListActivity : BaseActivity(), Filterable { private var categoryId: Long = 0 private var lastFilter = "" - private val onItemClickListener = { view: View -> - val index = view.tag as Int - - if (isValidCategory(categoryType, categoryId)) { - playback.service.play(categoryType, categoryId, index, lastFilter) - } - else { - playback.service.playAll(index, lastFilter) - } - - startActivity(MainActivity.getStartIntent(this)) - finish() - } - - private val onActionClickListener = { view: View -> - val track = view.tag as ITrack - mixin(ItemContextMenuMixin::class.java)?.showForTrack(track, view) - Unit - } - override fun onCreate(savedInstanceState: Bundle?) { component.inject(this) data = mixin(DataProviderMixin()) playback = mixin(PlaybackMixin()) - mixin(ItemContextMenuMixin(this)) super.onCreate(savedInstanceState) @@ -74,6 +54,8 @@ class TrackListActivity : BaseActivity(), Filterable { categoryId = intent.getLongExtra(EXTRA_SELECTED_ID, 0) val titleId = intent.getIntExtra(EXTRA_TITLE_ID, R.string.songs_title) + mixin(ItemContextMenuMixin(this, menuListener)) + setContentView(R.layout.recycler_view_activity) setTitleFromIntent(titleId) @@ -83,7 +65,7 @@ class TrackListActivity : BaseActivity(), Filterable { val recyclerView = findViewById(R.id.recycler_view) tracks = TrackListSlidingWindow(recyclerView, data.provider, queryFactory) - adapter = TrackListAdapter(tracks, onItemClickListener, onActionClickListener, playback) + adapter = TrackListAdapter(tracks, eventListener, playback) setupDefaultRecyclerView(recyclerView, adapter) @@ -127,6 +109,30 @@ class TrackListActivity : BaseActivity(), Filterable { filterDebouncer.call() } + private val eventListener = object: TrackListAdapter.EventListener { + override fun onItemClick(view: View, track: ITrack, position: Int) { + if (isValidCategory(categoryType, categoryId)) { + playback.service.play(categoryType, categoryId, position, lastFilter) + } + else { + playback.service.playAll(position, lastFilter) + } + + startActivity(MainActivity.getStartIntent(this@TrackListActivity)) + finish() + } + + override fun onActionItemClick(view: View, track: ITrack, position: Int) { + val mixin = mixin(ItemContextMenuMixin::class.java)!! + if (categoryType == Messages.Category.Companion.PLAYLISTS) { + mixin.showForPlaylistTrack(track, position, categoryId, view) + } + else { + mixin.showForTrack(track, view) + } + } + } + private fun initObservers() { disposables.add(data.provider.observeState().subscribeBy( onNext = { states -> @@ -215,6 +221,19 @@ class TrackListActivity : BaseActivity(), Filterable { override fun onMetadataLoaded(offset: Int, count: Int) {} } + private val menuListener: ItemContextMenuMixin.EventListener? + get() { + if (categoryType == Messages.Category.PLAYLISTS) { + return object: ItemContextMenuMixin.EventListener () { + override fun onPlaylistUpdated(id: Long) { + tracks.requery() + } + } + } + + return null + } + companion object { private val EXTRA_CATEGORY_TYPE = "extra_category_type" private val EXTRA_SELECTED_ID = "extra_selected_id" diff --git a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/adapter/TrackListAdapter.kt b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/adapter/TrackListAdapter.kt index a610dd55e..4adfcb446 100644 --- a/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/adapter/TrackListAdapter.kt +++ b/src/musikdroid/app/src/main/java/io/casey/musikcube/remote/ui/tracks/adapter/TrackListAdapter.kt @@ -13,15 +13,31 @@ import io.casey.musikcube.remote.ui.shared.mixin.PlaybackMixin import io.casey.musikcube.remote.ui.shared.model.TrackListSlidingWindow class TrackListAdapter(private val tracks: TrackListSlidingWindow, - private val onItemClickListener: (View) -> Unit, - private val onActionClickListener: (View) -> Unit, + private val listener: EventListener?, private var playback: PlaybackMixin) : RecyclerView.Adapter() { + interface EventListener { + fun onItemClick(view: View, track: ITrack, position: Int) + fun onActionItemClick(view: View, track: ITrack, position: Int) + } + + private data class Tag(var position: Int?, var track: ITrack?) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackListAdapter.ViewHolder { val inflater = LayoutInflater.from(parent.context) val view = inflater.inflate(R.layout.simple_list_item, parent, false) - view.setOnClickListener(onItemClickListener) - view.findViewById(R.id.action).setOnClickListener(onActionClickListener) + view.tag = Tag(null, null) + + view.setOnClickListener({ v -> + val tag = v.tag as Tag + listener?.onItemClick(v, tag.track!!, tag.position!!) + }) + + view.findViewById(R.id.action).setOnClickListener({ v -> + val tag = v.tag as Tag + listener?.onActionItemClick(v, tag.track!!, tag.position!!) + }) + return ViewHolder(view, playback) } @@ -39,8 +55,11 @@ class TrackListAdapter(private val tracks: TrackListSlidingWindow, private val action: View = view.findViewById(R.id.action) internal fun bind(track: ITrack?, position: Int) { - itemView.tag = position - action.tag = track + val tag = itemView.tag as Tag + tag.position = position + tag.track = track + itemView.tag = tag + action.tag = tag var titleColor = R.color.theme_foreground var subtitleColor = R.color.theme_disabled_foreground diff --git a/src/musikdroid/app/src/main/res/menu/track_item_context_menu.xml b/src/musikdroid/app/src/main/res/menu/track_item_context_menu.xml index 9cbeb2f69..4b3d6ae66 100644 --- a/src/musikdroid/app/src/main/res/menu/track_item_context_menu.xml +++ b/src/musikdroid/app/src/main/res/menu/track_item_context_menu.xml @@ -4,6 +4,10 @@ android:id="@+id/menu_add_to_playlist" android:title="@string/menu_add_to_playlist"/> + + diff --git a/src/musikdroid/app/src/main/res/values/strings.xml b/src/musikdroid/app/src/main/res/values/strings.xml index b52b38475..faef186e1 100644 --- a/src/musikdroid/app/src/main/res/values/strings.xml +++ b/src/musikdroid/app/src/main/res/values/strings.xml @@ -68,6 +68,7 @@ remote playback offline songs add to playlist + remove from playlist songs albums artists @@ -143,6 +144,8 @@ playlist \'%s\' not renamed playlist \'%s\' deleted could not delete playlist \'%s\' + confirm + are you sure you want to remove \`%s\` from this playlist? playback mode want to listen to music from your phone?\n\nclick here to switch between remote control and streaming playback modes.