summaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
authorH. Lehmann <ByteHamster@users.noreply.github.com>2019-04-06 21:45:55 +0200
committerGitHub <noreply@github.com>2019-04-06 21:45:55 +0200
commit5f86af88b18b94d3ba741963a6ae4ea51a532050 (patch)
tree00e5f60c915a016fe01729fe9cce375ce66511d0 /app/src
parent31194e02a5c7b4ddea37dcbc5ce48cbef5f6e8dc (diff)
parentcb14fd930fd3967053d9ddbab152ea0af65ffc0b (diff)
downloadAntennaPod-5f86af88b18b94d3ba741963a6ae4ea51a532050.zip
Merge pull request #3083 from orionlee/bulk_remove_from_queue_1145
Bulk remove from queue
Diffstat (limited to 'app/src')
-rw-r--r--app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java110
-rw-r--r--app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java225
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java2
-rw-r--r--app/src/main/res/layout/downloaded_episodeslist_item.xml2
-rw-r--r--app/src/main/res/layout/episodes_apply_action_fragment.xml143
-rw-r--r--app/src/main/res/layout/simple_list_item_multiple_choice_on_start.xml33
-rw-r--r--app/src/main/res/menu/episodes_apply_action_speeddial.xml34
-rw-r--r--app/src/main/res/menu/opml_selection_options.xml4
-rw-r--r--app/src/main/res/values-w300dp/dimens-fabspeeddial.xml8
11 files changed, 369 insertions, 196 deletions
diff --git a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java
index b80482c53..27d76116d 100644
--- a/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java
+++ b/app/src/androidTest/java/de/test/antennapod/storage/DBWriterTest.java
@@ -7,6 +7,8 @@ import android.preference.PreferenceManager;
import android.test.InstrumentationTestCase;
import android.util.Log;
+import org.awaitility.Awaitility;
+
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@@ -24,8 +26,7 @@ import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.PodDBAdapter;
-
-import org.awaitility.Awaitility;
+import de.danoeh.antennapod.core.util.Consumer;
/**
* Test class for DBWriter
@@ -574,29 +575,16 @@ public class DBWriterTest extends InstrumentationTestCase {
public void testRemoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException {
final int NUM_ITEMS = 10;
final Context context = getInstrumentation().getTargetContext();
- Feed feed = new Feed("url", null, "title");
- feed.setItems(new ArrayList<>());
- for (int i = 0; i < NUM_ITEMS; i++) {
- FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.PLAYED, feed);
- feed.getItems().add(item);
- }
-
- PodDBAdapter adapter = PodDBAdapter.getInstance();
- adapter.open();
- adapter.setCompleteFeed(feed);
- adapter.close();
+ Feed feed = createTestFeed(NUM_ITEMS);
- for (FeedItem item : feed.getItems()) {
- assertTrue(item.getId() != 0);
- }
for (int removeIndex = 0; removeIndex < NUM_ITEMS; removeIndex++) {
final FeedItem item = feed.getItems().get(removeIndex);
- adapter = PodDBAdapter.getInstance();
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
adapter.open();
adapter.setQueue(feed.getItems());
adapter.close();
- DBWriter.removeQueueItem(context, item, false).get(TIMEOUT, TimeUnit.SECONDS);
+ DBWriter.removeQueueItem(context, false, item).get(TIMEOUT, TimeUnit.SECONDS);
adapter = PodDBAdapter.getInstance();
adapter.open();
Cursor queue = adapter.getQueueIDCursor();
@@ -616,6 +604,43 @@ public class DBWriterTest extends InstrumentationTestCase {
}
}
+ public void testRemoveQueueItemMultipleItems() throws InterruptedException, ExecutionException, TimeoutException {
+ // Setup test data
+ //
+ final int NUM_ITEMS = 5;
+ final int NUM_IN_QUEUE = NUM_ITEMS - 1; // the last one not in queue for boundary condition
+ final Context context = getInstrumentation().getTargetContext();
+ Feed feed = createTestFeed(NUM_ITEMS);
+
+ List<FeedItem> itemsToAdd = feed.getItems().subList(0, NUM_IN_QUEUE);
+ withPodDB(adapter -> adapter.setQueue(itemsToAdd) );
+
+ // Actual tests
+ //
+
+ // Use array rather than List to make codes more succinct
+ Long[] itemIds = toItemIds(feed.getItems()).toArray(new Long[0]);
+
+ DBWriter.removeQueueItem(context, false,
+ itemIds[1], itemIds[3]).get(TIMEOUT, TimeUnit.SECONDS);
+ assertQueueByItemIds("Average case - 2 items removed successfully",
+ itemIds[0], itemIds[2]);
+
+ DBWriter.removeQueueItem(context, false).get(TIMEOUT, TimeUnit.SECONDS);
+ assertQueueByItemIds("Boundary case - no items supplied. queue should see no change",
+ itemIds[0], itemIds[2]);
+
+ DBWriter.removeQueueItem(context, false,
+ itemIds[0], itemIds[4], -1L).get(TIMEOUT, TimeUnit.SECONDS);
+ assertQueueByItemIds("Boundary case - items not in queue ignored",
+ itemIds[2]);
+
+ DBWriter.removeQueueItem(context, false,
+ itemIds[2], -1L).get(TIMEOUT, TimeUnit.SECONDS);
+ assertQueueByItemIds("Boundary case - invalid itemIds ignored"); // the queue is empty
+
+ }
+
public void testMoveQueueItem() throws InterruptedException, ExecutionException, TimeoutException {
final int NUM_ITEMS = 10;
Feed feed = new Feed("url", null, "title");
@@ -713,4 +738,53 @@ public class DBWriterTest extends InstrumentationTestCase {
assertTrue(item.isPlayed());
}
}
+
+ private static Feed createTestFeed(int numItems) {
+ Feed feed = new Feed("url", null, "title");
+ feed.setItems(new ArrayList<>());
+ for (int i = 0; i < numItems; i++) {
+ FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i, new Date(), FeedItem.PLAYED, feed);
+ feed.getItems().add(item);
+ }
+
+ withPodDB(adapter -> adapter.setCompleteFeed(feed));
+
+ for (FeedItem item : feed.getItems()) {
+ assertTrue(item.getId() != 0);
+ }
+ return feed;
+ }
+
+ private static void withPodDB(Consumer<PodDBAdapter> action) {
+ PodDBAdapter adapter = PodDBAdapter.getInstance();
+ try {
+ adapter.open();
+ action.accept(adapter);
+ } finally {
+ adapter.close();
+ }
+ }
+
+ private static void assertQueueByItemIds(
+ String message,
+ long... itemIdsExpected
+ ) {
+ List<FeedItem> queue = DBReader.getQueue();
+ List<Long> itemIdsActualList = toItemIds(queue);
+ List<Long> itemIdsExpectedList = new ArrayList<Long>(itemIdsExpected.length);
+ for (long id : itemIdsExpected) {
+ itemIdsExpectedList.add(id);
+ }
+
+ assertEquals(message, itemIdsExpectedList, itemIdsActualList);
+ }
+
+ private static List<Long> toItemIds(List<FeedItem> items) {
+ List<Long> itemIds = new ArrayList<Long>(items.size());
+ for(FeedItem item : items) {
+ itemIds.add(item.getId());
+ }
+ return itemIds;
+ }
+
}
diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java
index 07a64cde8..02071e355 100644
--- a/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/dialog/EpisodesApplyActionFragment.java
@@ -4,9 +4,17 @@ import android.app.AlertDialog;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.PluralsRes;
+import android.support.annotation.StringRes;
+import android.support.design.widget.Snackbar;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
import android.support.v4.util.ArrayMap;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -14,11 +22,12 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
-import android.widget.Button;
import android.widget.ListView;
-import android.widget.Toast;
+
+import com.leinardi.android.speeddial.SpeedDialView;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -35,22 +44,42 @@ public class EpisodesApplyActionFragment extends Fragment {
public static final String TAG = "EpisodeActionFragment";
- public static final int ACTION_QUEUE = 1;
- private static final int ACTION_MARK_PLAYED = 2;
- private static final int ACTION_MARK_UNPLAYED = 4;
- private static final int ACTION_DOWNLOAD = 8;
- public static final int ACTION_REMOVE = 16;
- private static final int ACTION_ALL = ACTION_QUEUE | ACTION_MARK_PLAYED | ACTION_MARK_UNPLAYED
- | ACTION_DOWNLOAD | ACTION_REMOVE;
+ public static final int ACTION_ADD_TO_QUEUE = 1;
+ private static final int ACTION_REMOVE_FROM_QUEUE = 2;
+ private static final int ACTION_MARK_PLAYED = 4;
+ private static final int ACTION_MARK_UNPLAYED = 8;
+ private static final int ACTION_DOWNLOAD = 16;
+ public static final int ACTION_DELETE = 32;
+ private static final int ACTION_ALL = ACTION_ADD_TO_QUEUE | ACTION_REMOVE_FROM_QUEUE
+ | ACTION_MARK_PLAYED | ACTION_MARK_UNPLAYED | ACTION_DOWNLOAD | ACTION_DELETE;
+
+ /**
+ * Specify an action (defined by #flag) 's UI bindings.
+ *
+ * Includes: the menu / action item and the actual logic
+ */
+ private class ActionBinding {
+ int flag;
+ @IdRes
+ final int actionItemId;
+ @NonNull
+ final Runnable action;
+
+ ActionBinding(int flag, @IdRes int actionItemId, @NonNull Runnable action) {
+ this.flag = flag;
+ this.actionItemId = actionItemId;
+ this.action = action;
+ }
+ }
+
+ private final List<? extends ActionBinding> actionBindings;
private ListView mListView;
private ArrayAdapter<String> mAdapter;
- private Button btnAddToQueue;
- private Button btnMarkAsPlayed;
- private Button btnMarkAsUnplayed;
- private Button btnDownload;
- private Button btnDelete;
+ private SpeedDialView mSpeedDialView;
+ @NonNull
+ private CharSequence actionBarTitleOriginal = "";
private final Map<Long,FeedItem> idMap = new ArrayMap<>();
private final List<FeedItem> episodes = new ArrayList<>();
@@ -60,6 +89,23 @@ public class EpisodesApplyActionFragment extends Fragment {
private MenuItem mSelectToggle;
+ public EpisodesApplyActionFragment() {
+ actionBindings = Arrays.asList(
+ new ActionBinding(ACTION_ADD_TO_QUEUE,
+ R.id.add_to_queue_batch, this::queueChecked),
+ new ActionBinding(ACTION_REMOVE_FROM_QUEUE,
+ R.id.remove_from_queue_batch, this::removeFromQueueChecked),
+ new ActionBinding(ACTION_MARK_PLAYED,
+ R.id.mark_read_batch, this::markedCheckedPlayed),
+ new ActionBinding(ACTION_MARK_UNPLAYED,
+ R.id.mark_unread_batch, this::markedCheckedUnplayed),
+ new ActionBinding(ACTION_DOWNLOAD,
+ R.id.download_batch, this::downloadChecked),
+ new ActionBinding(ACTION_DELETE,
+ R.id.delete_batch, this::deleteChecked)
+ );
+ }
+
public static EpisodesApplyActionFragment newInstance(List<FeedItem> items) {
return newInstance(items, ACTION_ALL);
}
@@ -124,57 +170,58 @@ public class EpisodesApplyActionFragment extends Fragment {
}
mAdapter = new ArrayAdapter<>(getActivity(),
- android.R.layout.simple_list_item_multiple_choice, titles);
+ R.layout.simple_list_item_multiple_choice_on_start, titles);
mListView.setAdapter(mAdapter);
- checkAll();
- int lastVisibleDiv = 0;
- btnAddToQueue = view.findViewById(R.id.btnAddToQueue);
- if((actions & ACTION_QUEUE) != 0) {
- btnAddToQueue.setOnClickListener(v -> queueChecked());
- lastVisibleDiv = R.id.divider1;
- } else {
- btnAddToQueue.setVisibility(View.GONE);
- view.findViewById(R.id.divider1).setVisibility(View.GONE);
- }
- btnMarkAsPlayed = view.findViewById(R.id.btnMarkAsPlayed);
- if((actions & ACTION_MARK_PLAYED) != 0) {
- btnMarkAsPlayed.setOnClickListener(v -> markedCheckedPlayed());
- lastVisibleDiv = R.id.divider2;
- } else {
- btnMarkAsPlayed.setVisibility(View.GONE);
- view.findViewById(R.id.divider2).setVisibility(View.GONE);
- }
- btnMarkAsUnplayed = view.findViewById(R.id.btnMarkAsUnplayed);
- if((actions & ACTION_MARK_UNPLAYED) != 0) {
- btnMarkAsUnplayed.setOnClickListener(v -> markedCheckedUnplayed());
- lastVisibleDiv = R.id.divider3;
- } else {
- btnMarkAsUnplayed.setVisibility(View.GONE);
- view.findViewById(R.id.divider3).setVisibility(View.GONE);
- }
- btnDownload = view.findViewById(R.id.btnDownload);
- if((actions & ACTION_DOWNLOAD) != 0) {
- btnDownload.setOnClickListener(v -> downloadChecked());
- lastVisibleDiv = R.id.divider4;
- } else {
- btnDownload.setVisibility(View.GONE);
- view.findViewById(R.id.divider4).setVisibility(View.GONE);
- }
- btnDelete = view.findViewById(R.id.btnDelete);
- if((actions & ACTION_REMOVE) != 0) {
- btnDelete.setOnClickListener(v -> deleteChecked());
- } else {
- btnDelete.setVisibility(View.GONE);
- if(lastVisibleDiv > 0) {
- view.findViewById(lastVisibleDiv).setVisibility(View.GONE);
+ saveActionBarTitle(); // needed when we dynamically change the title based on selection
+
+ // Init action UI (via a FAB Speed Dial)
+ mSpeedDialView = view.findViewById(R.id.fabSD);
+ mSpeedDialView.inflate(R.menu.episodes_apply_action_speeddial);
+
+ // show only specified actions, and bind speed dial UIs to the actual logic
+ for (ActionBinding binding : actionBindings) {
+ if ((actions & binding.flag) == 0) {
+ mSpeedDialView.removeActionItemById(binding.actionItemId);
}
}
+ mSpeedDialView.setOnActionSelectedListener(actionItem -> {
+ ActionBinding selectedBinding = null;
+ for (ActionBinding binding : actionBindings) {
+ if (actionItem.getId() == binding.actionItemId) {
+ selectedBinding = binding;
+ break;
+ }
+ }
+ if (selectedBinding != null) {
+ selectedBinding.action.run();
+ } else {
+ Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + actionItem.getId());
+ }
+ return true;
+ });
+
+ showSpeedDialIfAnyChecked();
+
return view;
}
@Override
+ public void onStop() {
+ restoreActionBarTitle(); // it might have been changed to "N selected". Restore original.
+ super.onStop();
+ }
+
+ private void showSpeedDialIfAnyChecked() {
+ mSpeedDialView.setVisibility(checkedIds.size() > 0 ? View.VISIBLE : View.GONE);
+ }
+
+ private void hideSpeedDial() {
+ mSpeedDialView.setVisibility(View.GONE);
+ }
+
+ @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
inflater.inflate(R.menu.episodes_apply_action_options, menu);
@@ -196,11 +243,9 @@ public class EpisodesApplyActionFragment extends Fragment {
int[] icon = new int[1];
if (checkedIds.size() == episodes.size()) {
- icon[0] = R.attr.ic_check_box;
- } else if (checkedIds.size() == 0) {
- icon[0] = R.attr.ic_check_box_outline;
+ icon[0] = R.attr.ic_select_none;
} else {
- icon[0] = R.attr.ic_indeterminate_check_box;
+ icon[0] = R.attr.ic_select_all;
}
TypedArray a = getActivity().obtainStyledAttributes(icon);
@@ -212,7 +257,7 @@ public class EpisodesApplyActionFragment extends Fragment {
@Override
public boolean onOptionsItemSelected(MenuItem item) {
- int resId = 0;
+ @StringRes int resId = 0;
switch(item.getItemId()) {
case R.id.select_options:
return true;
@@ -272,7 +317,8 @@ public class EpisodesApplyActionFragment extends Fragment {
return true;
}
if(resId != 0) {
- Toast.makeText(getActivity(), resId, Toast.LENGTH_SHORT).show();
+ Snackbar.make(getActivity().findViewById(R.id.content), resId, Snackbar.LENGTH_SHORT)
+ .show();
return true;
} else {
return false;
@@ -410,21 +456,56 @@ public class EpisodesApplyActionFragment extends Fragment {
mListView.setItemChecked(i, checked);
}
ActivityCompat.invalidateOptionsMenu(EpisodesApplyActionFragment.this.getActivity());
+ showSpeedDialIfAnyChecked();
+ updateActionBarTitle();
+ }
+
+ private void saveActionBarTitle() {
+ ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+ if (actionBar != null) {
+ CharSequence title = actionBar.getTitle();
+ if (title == null) {
+ title = "";
+ }
+ actionBarTitleOriginal = title;
+ }
+ }
+
+ private void restoreActionBarTitle() {
+ ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(actionBarTitleOriginal);
+ }
+ }
+
+ private void updateActionBarTitle() {
+ ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
+ if (actionBar != null) {
+ CharSequence title = checkedIds.size() > 0 ?
+ getString(R.string.num_selected_label, checkedIds.size()) :
+ actionBarTitleOriginal;
+ actionBar.setTitle(title);
+ }
}
private void queueChecked() {
DBWriter.addQueueItem(getActivity(), true, checkedIds.toArray());
- close();
+ close(R.plurals.added_to_queue_batch_label, checkedIds.size());
+ }
+
+ private void removeFromQueueChecked() {
+ DBWriter.removeQueueItem(getActivity(), true, checkedIds.toArray());
+ close(R.plurals.removed_from_queue_batch_label, checkedIds.size());
}
private void markedCheckedPlayed() {
DBWriter.markItemPlayed(FeedItem.PLAYED, checkedIds.toArray());
- close();
+ close(R.plurals.marked_read_batch_label, checkedIds.size());
}
private void markedCheckedUnplayed() {
DBWriter.markItemPlayed(FeedItem.UNPLAYED, checkedIds.toArray());
- close();
+ close(R.plurals.marked_unread_batch_label, checkedIds.size());
}
private void downloadChecked() {
@@ -441,7 +522,7 @@ public class EpisodesApplyActionFragment extends Fragment {
e.printStackTrace();
DownloadRequestErrorDialogCreator.newRequestErrorDialog(getActivity(), e.getMessage());
}
- close();
+ close(R.plurals.downloading_batch_label, checkedIds.size());
}
private void deleteChecked() {
@@ -451,10 +532,18 @@ public class EpisodesApplyActionFragment extends Fragment {
DBWriter.deleteFeedMediaOfItem(getActivity(), episode.getMedia().getId());
}
}
- close();
+ close(R.plurals.deleted_episode_batch_label, checkedIds.size());
}
- private void close() {
+ private void close(@PluralsRes int msgId, int numItems) {
+ if (numItems > 0) {
+ Snackbar.make(getActivity().findViewById(R.id.content),
+ getResources().getQuantityString(msgId, numItems, numItems),
+ Snackbar.LENGTH_LONG
+ )
+ .setAction(android.R.string.ok, v -> {})
+ .show();
+ }
getActivity().getSupportFragmentManager().popBackStack();
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
index ddf9044ab..b52fd444f 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
@@ -149,7 +149,7 @@ public class CompletedDownloadsFragment extends ListFragment {
switch (item.getItemId()) {
case R.id.episode_actions:
EpisodesApplyActionFragment fragment = EpisodesApplyActionFragment
- .newInstance(items, EpisodesApplyActionFragment.ACTION_REMOVE | EpisodesApplyActionFragment.ACTION_QUEUE);
+ .newInstance(items, EpisodesApplyActionFragment.ACTION_DELETE | EpisodesApplyActionFragment.ACTION_ADD_TO_QUEUE);
((MainActivity) getActivity()).loadChildFragment(fragment);
return true;
default:
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
index 763bc487b..0e8f2f083 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
@@ -431,7 +431,7 @@ public class QueueFragment extends Fragment {
final FeedItem item = queue.get(position);
final boolean isRead = item.isPlayed();
DBWriter.markItemPlayed(FeedItem.PLAYED, false, item.getId());
- DBWriter.removeQueueItem(getActivity(), item, true);
+ DBWriter.removeQueueItem(getActivity(), true, item);
Snackbar snackbar = Snackbar.make(root, getString(R.string.marked_as_read_label), Snackbar.LENGTH_LONG);
snackbar.setAction(getString(R.string.undo), v -> {
DBWriter.addQueueItemAt(getActivity(), item.getId(), position, false);
diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
index 321b9c7bb..3f8d88af7 100644
--- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
+++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java
@@ -199,7 +199,7 @@ public class FeedItemMenuHandler {
DBWriter.addQueueItem(context, selectedItem);
break;
case R.id.remove_from_queue_item:
- DBWriter.removeQueueItem(context, selectedItem, true);
+ DBWriter.removeQueueItem(context, true, selectedItem);
break;
case R.id.add_to_favorites_item:
DBWriter.addFavoriteItem(selectedItem);
diff --git a/app/src/main/res/layout/downloaded_episodeslist_item.xml b/app/src/main/res/layout/downloaded_episodeslist_item.xml
index 66ae6c180..65a08251f 100644
--- a/app/src/main/res/layout/downloaded_episodeslist_item.xml
+++ b/app/src/main/res/layout/downloaded_episodeslist_item.xml
@@ -92,7 +92,7 @@
android:layout_height="match_parent"
android:background="?attr/selectableItemBackground"
android:clickable="false"
- android:contentDescription="@string/remove_episode_lable"
+ android:contentDescription="@string/delete_episode_label"
android:focusable="false"
android:focusableInTouchMode="false"
android:src="?attr/content_discard"
diff --git a/app/src/main/res/layout/episodes_apply_action_fragment.xml b/app/src/main/res/layout/episodes_apply_action_fragment.xml
index e9a2e2e23..984e960d8 100644
--- a/app/src/main/res/layout/episodes_apply_action_fragment.xml
+++ b/app/src/main/res/layout/episodes_apply_action_fragment.xml
@@ -1,115 +1,50 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
-
- <LinearLayout
- android:id="@+id/bottomBar"
- android:layout_width="match_parent"
- android:layout_height="68dp"
- android:layout_alignParentBottom="true"
- android:gravity="center_vertical"
- android:orientation="horizontal"
- android:padding="4dp">
-
- <Button
- android:id="@+id/btnAddToQueue"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="@android:color/transparent"
- android:drawableTop="?attr/content_new"
- android:text="@string/add_to_queue_label"
- android:textSize="10sp" />
-
- <View
- android:id="@+id/divider1"
- android:layout_width="1dp"
- android:layout_height="match_parent"
- android:layout_margin="4dp"
- android:background="?android:attr/listDivider"
- tools:background="@android:color/holo_red_dark" />
-
- <Button
- android:id="@+id/btnMarkAsPlayed"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="@android:color/transparent"
- android:drawableTop="?attr/navigation_accept"
- android:text="@string/mark_read_label"
- android:textSize="10sp" />
-
- <View
- android:id="@+id/divider2"
- android:layout_width="1dp"
- android:layout_height="match_parent"
- android:layout_margin="4dp"
- android:background="?android:attr/listDivider"
- tools:background="@android:color/holo_red_dark" />
-
- <Button
- android:id="@+id/btnMarkAsUnplayed"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="@android:color/transparent"
- android:drawableTop="?attr/navigation_cancel"
- android:text="@string/mark_unread_label"
- android:textSize="10sp" />
-
- <View
- android:id="@+id/divider3"
- android:layout_width="1dp"
- android:layout_height="match_parent"
- android:layout_margin="4dp"
- android:background="?android:attr/listDivider"
- tools:background="@android:color/holo_red_dark" />
-
- <Button
- android:id="@+id/btnDownload"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="@android:color/transparent"
- android:drawableTop="?attr/av_download"
- android:text="@string/download_label"
- android:textSize="10sp" />
-
- <View
- android:id="@+id/divider4"
- android:layout_width="1dp"
- android:layout_height="match_parent"
- android:layout_margin="4dp"
- android:background="?android:attr/listDivider"
- tools:background="@android:color/holo_red_dark" />
-
- <Button
- android:id="@+id/btnDelete"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_weight="1"
- android:background="@android:color/transparent"
- android:drawableTop="?attr/content_discard"
- android:text="@string/remove_episode_lable"
- android:textSize="10sp" />
-
- </LinearLayout>
-
- <View
- android:id="@+id/divider"
- android:layout_width="match_parent"
- android:layout_height="1dp"
- android:layout_above="@id/bottomBar"
- android:background="?android:attr/listDivider"
- android:paddingBottom="4dp"
- tools:background="@android:color/holo_red_dark" />
-
<ListView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_above="@id/divider"/>
+ android:layout_alignParentTop="true"
+ android:layout_marginTop="0dp" />
+
+ <com.leinardi.android.speeddial.SpeedDialOverlayLayout
+ android:id="@+id/fabSDOverlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+ <!-- The FAB SpeedDial
+ 1. MUST be placed at the bottom of the layout xml to ensure it is at the front,
+ clickable on Pre-Lollipop devices (that do not support elevation).
+ See: https://stackoverflow.com/a/2614402
+ 2. ScrollView is needed to ensure the vertical list of speed dials are
+ accessible when screen height is small, eg., landscape mode on most phones.
+ -->
+ <ScrollView
+ android:id="@+id/fabSDScrollCtr"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentEnd="true"
+ android:layout_alignParentRight="true"
+ android:elevation="@dimen/sd_close_elevation"
+ tools:ignore="UnusedAttribute">
+ <!-- android:elevation:
+ 1. Needs to match the speed dial's minimal elevation,
+ or the speed dial can't be clicked at all
+ -->
+ <com.leinardi.android.speeddial.SpeedDialView
+ android:id="@+id/fabSD"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:sdMainFabClosedSrc="@drawable/ic_fab_edit"
+ app:sdOverlayLayout="@id/fabSDOverlay"
+ android:layout_marginEnd="16dp"
+ android:layout_marginRight="16dp"
+ android:layout_marginBottom="16dp"
+ />
+ </ScrollView>
</RelativeLayout>
diff --git a/app/src/main/res/layout/simple_list_item_multiple_choice_on_start.xml b/app/src/main/res/layout/simple_list_item_multiple_choice_on_start.xml
new file mode 100644
index 000000000..3c03f71ea
--- /dev/null
+++ b/app/src/main/res/layout/simple_list_item_multiple_choice_on_start.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Based on simple_list_item_multiple_choice.xml
+ from The Android Open Source Project
+ This one puts the check box at the start of the view (rather than at the end).
+ -->
+<!-- Copyright (C) 2008 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="?android:attr/listPreferredItemHeightSmall"
+ android:textAppearance="?android:attr/textAppearanceListItemSmall"
+ android:gravity="center_vertical"
+ android:drawableStart="?android:attr/listChoiceIndicatorMultiple"
+ android:drawableLeft="?android:attr/listChoiceIndicatorMultiple"
+ android:paddingStart="?android:attr/listPreferredItemPaddingStart"
+ android:paddingLeft="?android:attr/listPreferredItemPaddingLeft"
+ android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:paddingRight="?android:attr/listPreferredItemPaddingRight"
+ />
diff --git a/app/src/main/res/menu/episodes_apply_action_speeddial.xml b/app/src/main/res/menu/episodes_apply_action_speeddial.xml
new file mode 100644
index 000000000..39083e41b
--- /dev/null
+++ b/app/src/main/res/menu/episodes_apply_action_speeddial.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <!-- the order is opposite of the typical menu:
+ catered to FAB speed dial, which somehow shows the item in reverse.
+ E.g., item @id/delete_batch is the first in the xml,
+ visually it will be shown at the bottom of the list of actions.
+ -->
+ <item android:id="@+id/delete_batch"
+ android:icon="?attr/content_discard"
+ android:title="@string/delete_episode_label"
+ />
+ <item android:id="@+id/download_batch"
+ android:icon="?attr/av_download"
+ android:title="@string/download_label"
+ />
+ <item android:id="@+id/mark_unread_batch"
+ android:icon="?attr/navigation_cancel"
+ android:title="@string/mark_unread_label"
+ />
+ <item
+ android:id="@+id/mark_read_batch"
+ android:icon="?attr/navigation_accept"
+ android:title="@string/mark_read_label"
+ />
+ <item android:id="@+id/remove_from_queue_batch"
+ android:icon="?attr/content_remove_from_queue"
+ android:title="@string/remove_from_queue_label"
+ />
+ <item
+ android:id="@+id/add_to_queue_batch"
+ android:icon="?attr/content_new"
+ android:title="@string/add_to_queue_label"
+ />
+</menu>
diff --git a/app/src/main/res/menu/opml_selection_options.xml b/app/src/main/res/menu/opml_selection_options.xml
index 26d2a0519..8b3310dc2 100644
--- a/app/src/main/res/menu/opml_selection_options.xml
+++ b/app/src/main/res/menu/opml_selection_options.xml
@@ -4,14 +4,14 @@
<item
android:id="@id/select_all_item"
- android:icon="?attr/ic_check_box_outline"
+ android:icon="?attr/ic_select_all"
android:title="@string/select_all_label"
custom:showAsAction="ifRoom">
</item>
<item
android:id="@id/deselect_all_item"
- android:icon="?attr/ic_check_box"
+ android:icon="?attr/ic_select_none"
android:title="@string/deselect_all_label"
custom:showAsAction="ifRoom">
</item>
diff --git a/app/src/main/res/values-w300dp/dimens-fabspeeddial.xml b/app/src/main/res/values-w300dp/dimens-fabspeeddial.xml
new file mode 100644
index 000000000..e531395c0
--- /dev/null
+++ b/app/src/main/res/values-w300dp/dimens-fabspeeddial.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- increase FAB speed dial label's max width if the screen is wide enough
+ (300dp ~ 1.875 inch, devices with 3.5-inch screens have a width of ~ 1.9in
+ so the setup is applicable for most phones)
+ -->
+ <dimen name="sd_label_max_width">240dp</dimen>
+</resources>