diff --git a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeMultiSelectActionHandler.java index a3ef938ff..7e27b89cd 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeMultiSelectActionHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/episodeslist/EpisodeMultiSelectActionHandler.java @@ -43,6 +43,10 @@ public class EpisodeMultiSelectActionHandler { downloadChecked(items); } else if (actionId == R.id.remove_item) { LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary(activity, items, () -> deleteChecked(items)); + } else if (actionId == R.id.move_to_top_item) { + moveToTopChecked(items); + } else if (actionId == R.id.move_to_bottom_item) { + moveToBottomChecked(items); } else { Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + actionId); } @@ -113,6 +117,16 @@ public class EpisodeMultiSelectActionHandler { showMessage(R.plurals.deleted_multi_episode_batch_label, countHasMedia); } + private void moveToTopChecked(List items) { + DBWriter.moveQueueItemsToTop(items); + showMessage(R.plurals.move_to_top_message, items.size()); + } + + private void moveToBottomChecked(List items) { + DBWriter.moveQueueItemsToBottom(items); + showMessage(R.plurals.move_to_bottom_message, items.size()); + } + private void showMessage(@PluralsRes int msgId, int numItems) { if (numItems == 1) { return; diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueFragment.java index 2cb7dd707..eeed170f0 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/queue/QueueFragment.java @@ -8,6 +8,7 @@ import android.util.Log; import android.view.ContextMenu; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -38,6 +39,7 @@ import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; import org.greenrobot.eventbus.ThreadMode; +import java.util.Collections; import java.util.List; import de.danoeh.antennapod.R; @@ -140,6 +142,7 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte loadItems(true); return; } + int position; switch (event.action) { case ADDED: queue.add(event.position, event.item); @@ -152,7 +155,7 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte break; case REMOVED: case IRREVERSIBLE_REMOVED: - int position = FeedItemEvent.indexOfItemWithId(queue, event.item.getId()); + position = FeedItemEvent.indexOfItemWithId(queue, event.item.getId()); queue.remove(position); recyclerAdapter.notifyItemRemoved(position); break; @@ -160,6 +163,11 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte queue.clear(); recyclerAdapter.updateItems(queue); break; + case MOVED: + position = FeedItemEvent.indexOfItemWithId(queue, event.item.getId()); + queue.add(event.position, queue.remove(position)); + recyclerAdapter.notifyItemMoved(position, event.position); + break; default: return; } @@ -374,16 +382,18 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte } final int itemId = item.getItemId(); - if (itemId == R.id.move_to_top_item) { - queue.add(0, queue.remove(position)); - recyclerAdapter.notifyItemMoved(position, 0); - DBWriter.moveQueueItemToTop(selectedItem.getId(), true); - return true; - } else if (itemId == R.id.move_to_bottom_item) { - queue.add(queue.size() - 1, queue.remove(position)); - recyclerAdapter.notifyItemMoved(position, queue.size() - 1); - DBWriter.moveQueueItemToBottom(selectedItem.getId(), true); - return true; + if (!recyclerAdapter.inActionMode()) { + if (itemId == R.id.move_to_top_item) { + queue.add(0, queue.remove(position)); + recyclerAdapter.notifyItemMoved(position, 0); + DBWriter.moveQueueItemsToTop(Collections.singletonList(selectedItem)); + return true; + } else if (itemId == R.id.move_to_bottom_item) { + queue.add(queue.remove(position)); + recyclerAdapter.notifyItemMoved(position, queue.size() - 1); + DBWriter.moveQueueItemsToBottom(Collections.singletonList(selectedItem)); + return true; + } } return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem); } @@ -437,8 +447,15 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte @Override protected void onSelectedItemsUpdated() { super.onSelectedItemsUpdated(); + Menu menu = floatingSelectMenu.getMenu(); + List selectedItems = getSelectedItems(); FeedItemMenuHandler.onPrepareMenu(floatingSelectMenu.getMenu(), getSelectedItems(), R.id.add_to_queue_item, R.id.remove_inbox_item); + + Pair canMove = canMove(queue, selectedItems); + menu.findItem(R.id.move_to_top_item).setVisible(canMove.first); + menu.findItem(R.id.move_to_bottom_item).setVisible(canMove.second); + floatingSelectMenu.updateItemVisibility(); } }; @@ -608,6 +625,40 @@ public class QueueFragment extends Fragment implements MaterialToolbar.OnMenuIte } } + /** + * This method checks if the selected items are allowed to be moved to the top, bottom or both of the Queue. + * @param queue The FeedItems currently in the Queue. + * @param selectedItems The FeedItems for which the check is performed. + * @return A Pair of booleans where + * [0] is true if moving to the top is allowed, false otherwise. + * [1] is true if moving to the bottom is allowed, false otherwise. + * */ + public static Pair canMove(List queue, List selectedItems) { + int queueSize = queue.size(); + int selectedSize = selectedItems.size(); + // No manual reordering allowed or reordering would be a no-op. + if (selectedItems.isEmpty() || queue.isEmpty() || UserPreferences.isQueueLocked() + || UserPreferences.isQueueKeepSorted() || selectedSize == queueSize) { + return new Pair<>(false, false); + } + boolean isFirstItemSelected = selectedItems.get(0).getId() == queue.get(0).getId(); + boolean isLastItemSelected = selectedItems.get(selectedSize - 1).getId() == queue.get(queueSize - 1).getId(); + // If only one item is selected and its already at the top of the list, disable option to move item to the top. + // If the item is already at the bottom of the list, disable the option to move it to the bottom. + if (selectedSize == 1) { + return new Pair<>(!isFirstItemSelected, !isLastItemSelected); + } + // If contiguous from the top, moving items to the top is disabled, as they are already there. + if (isFirstItemSelected && selectedItems.equals(queue.subList(0, selectedSize))) { + return new Pair<>(false, true); + } + // If contiguous from the bottom, moving items to the bottom is disabled, as they are already there. + if (isLastItemSelected && selectedItems.equals(queue.subList(queueSize - selectedSize, queueSize))) { + return new Pair<>(true, false); + } + return new Pair<>(true, true); + } + private class QueueSwipeActions extends SwipeActions { // Position tracking whilst dragging diff --git a/app/src/main/res/menu/episodes_apply_action_speeddial.xml b/app/src/main/res/menu/episodes_apply_action_speeddial.xml index fe48c6097..81a8a2af0 100644 --- a/app/src/main/res/menu/episodes_apply_action_speeddial.xml +++ b/app/src/main/res/menu/episodes_apply_action_speeddial.xml @@ -42,4 +42,14 @@ android:icon="@drawable/ic_check" android:title="@string/remove_inbox_label" /> + + + + diff --git a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBWriter.java b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBWriter.java index 1bf8c4900..93b961f9a 100644 --- a/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBWriter.java +++ b/storage/database/src/main/java/de/danoeh/antennapod/storage/database/DBWriter.java @@ -24,6 +24,7 @@ import org.greenrobot.eventbus.EventBus; import java.io.File; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; @@ -551,44 +552,6 @@ public class DBWriter { }); } - /** - * Moves the specified item to the top of the queue. - * - * @param itemId The item to move to the top of the queue - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - */ - public static Future moveQueueItemToTop(final long itemId, final boolean broadcastUpdate) { - return runOnDbThread(() -> { - LongList queueIdList = DBReader.getQueueIDList(); - int index = queueIdList.indexOf(itemId); - if (index >= 0) { - moveQueueItemHelper(index, 0, broadcastUpdate); - } else { - Log.e(TAG, "moveQueueItemToTop: item not found"); - } - }); - } - - /** - * Moves the specified item to the bottom of the queue. - * - * @param itemId The item to move to the bottom of the queue - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - */ - public static Future moveQueueItemToBottom(final long itemId, - final boolean broadcastUpdate) { - return runOnDbThread(() -> { - LongList queueIdList = DBReader.getQueueIDList(); - int index = queueIdList.indexOf(itemId); - if (index >= 0) { - moveQueueItemHelper(index, queueIdList.size() - 1, - broadcastUpdate); - } else { - Log.e(TAG, "moveQueueItemToBottom: item not found"); - } - }); - } - /** * Changes the position of a FeedItem in the queue. * @@ -598,36 +561,67 @@ public class DBWriter { * false if the caller wants to avoid unexpected updates of the GUI. * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) */ - public static Future moveQueueItem(final int from, - final int to, final boolean broadcastUpdate) { - return runOnDbThread(() -> moveQueueItemHelper(from, to, broadcastUpdate)); + public static Future moveQueueItem(final int from, final int to, final boolean broadcastUpdate) { + return runOnDbThread(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + final List queue = DBReader.getQueue(); + + if (from >= 0 && from < queue.size() && to >= 0 && to < queue.size()) { + final FeedItem item = queue.remove(from); + queue.add(to, item); + + adapter.setQueue(queue); + if (broadcastUpdate) { + EventBus.getDefault().post(QueueEvent.moved(item, to)); + } + } + adapter.close(); + }); } - /** - * Changes the position of a FeedItem in the queue. - *

- * This function must be run using the ExecutorService (dbExec). - * - * @param from Source index. Must be in range 0..queue.size()-1. - * @param to Destination index. Must be in range 0..queue.size()-1. - * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - * false if the caller wants to avoid unexpected updates of the GUI. - * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) - */ - private static void moveQueueItemHelper(final int from, - final int to, final boolean broadcastUpdate) { + public static Future moveQueueItemsToTop(final List items) { + return runOnDbThread(() -> moveQueueItemsSynchronous(true, items)); + } + + public static Future moveQueueItemsToBottom(final List items) { + return runOnDbThread(() -> moveQueueItemsSynchronous(false, items)); + } + + private static void moveQueueItemsSynchronous(final boolean moveToTop, final List items) { + if (items.isEmpty()) { + return; + } + final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); final List queue = DBReader.getQueue(); - if (from >= 0 && from < queue.size() && to >= 0 && to < queue.size()) { - final FeedItem item = queue.remove(from); - queue.add(to, item); + List selectedItems = moveToTop ? new ArrayList<>(items) : items; + if (moveToTop) { + Collections.reverse(selectedItems); + } + boolean queueModified = false; + List events = new ArrayList<>(); + + queue.removeAll(selectedItems); + events.add(QueueEvent.setQueue(queue)); + + for (FeedItem item : selectedItems) { + int newIndex = moveToTop ? 0 : queue.size(); + queue.add(newIndex, item); + events.add(QueueEvent.moved(item, newIndex)); + queueModified = true; + } + + if (queueModified) { adapter.setQueue(queue); - if (broadcastUpdate) { - EventBus.getDefault().post(QueueEvent.moved(item, to)); + for (QueueEvent event : events) { + EventBus.getDefault().post(event); } + } else { + Log.w(TAG, "moveToTop: " + moveToTop + " - Queue was not modified."); } adapter.close(); } diff --git a/ui/common/src/main/res/drawable/ic_arrow_full_down.xml b/ui/common/src/main/res/drawable/ic_arrow_full_down.xml new file mode 100644 index 000000000..d1528767d --- /dev/null +++ b/ui/common/src/main/res/drawable/ic_arrow_full_down.xml @@ -0,0 +1,7 @@ + + + diff --git a/ui/common/src/main/res/drawable/ic_arrow_full_up.xml b/ui/common/src/main/res/drawable/ic_arrow_full_up.xml new file mode 100644 index 000000000..ce1d01d31 --- /dev/null +++ b/ui/common/src/main/res/drawable/ic_arrow_full_up.xml @@ -0,0 +1,7 @@ + + + diff --git a/ui/i18n/src/main/res/values/strings.xml b/ui/i18n/src/main/res/values/strings.xml index 0a5d517f0..1f681b7e3 100644 --- a/ui/i18n/src/main/res/values/strings.xml +++ b/ui/i18n/src/main/res/values/strings.xml @@ -364,7 +364,15 @@ Clear queue Undo Move to top + + %d episode moved to top. + %d episodes moved to top. + Move to bottom + + %d episode moved to bottom. + %d episodes moved to bottom. + Sort Keep sorted Date