summaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
authorpeakvalleytech <65185819+peakvalleytech@users.noreply.github.com>2021-06-29 13:01:04 -0700
committerGitHub <noreply@github.com>2021-06-29 22:01:04 +0200
commit323f1f61424c39f8cde6076a4d30501bc75fc109 (patch)
tree824dcc1e70c00af5e5959bbe147e5c67ebd556aa /app/src/main
parent1fbc565bc3e436491c197ff8610a3bf5b8660f8d (diff)
downloadAntennaPod-323f1f61424c39f8cde6076a4d30501bc75fc109.zip
Contextual menu for multi selecting episodes (#5130)
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java87
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java168
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java4
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java97
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java2
-rw-r--r--app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java130
-rw-r--r--app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java7
-rw-r--r--app/src/main/res/layout/feed_item_list_fragment.xml107
-rw-r--r--app/src/main/res/layout/feeditemlist_item.xml344
-rw-r--r--app/src/main/res/layout/multi_select_speed_dial.xml38
-rw-r--r--app/src/main/res/menu/feeditemlist_context.xml12
-rw-r--r--app/src/main/res/menu/feedlist.xml7
-rw-r--r--app/src/main/res/menu/multi_select_context_popup.xml11
-rw-r--r--app/src/main/res/menu/multi_select_options.xml8
18 files changed, 756 insertions, 276 deletions
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java
index 774b97454..674b5f86e 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/EpisodeItemListAdapter.java
@@ -3,11 +3,20 @@ package de.danoeh.antennapod.adapter;
import android.app.Activity;
import android.view.ContextMenu;
import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
+
+import org.apache.commons.lang3.ArrayUtils;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.model.feed.FeedItem;
@@ -15,24 +24,20 @@ import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.fragment.ItemPagerFragment;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
-import org.apache.commons.lang3.ArrayUtils;
-
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.List;
/**
* List adapter for the list of new episodes.
*/
-public class EpisodeItemListAdapter extends RecyclerView.Adapter<EpisodeItemViewHolder>
+public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHolder>
implements View.OnCreateContextMenuListener {
private final WeakReference<MainActivity> mainActivityRef;
private List<FeedItem> episodes = new ArrayList<>();
- private FeedItem selectedItem;
+ private FeedItem longPressedItem;
+ int longPressedPosition = 0; // used to init actionMode
public EpisodeItemListAdapter(MainActivity mainActivity) {
- super();
+ super(mainActivity);
this.mainActivityRef = new WeakReference<>(mainActivity);
setHasStableIds(true);
}
@@ -63,19 +68,35 @@ public class EpisodeItemListAdapter extends RecyclerView.Adapter<EpisodeItemView
FeedItem item = episodes.get(pos);
holder.bind(item);
- holder.itemView.setOnLongClickListener(v -> {
- selectedItem = item;
- return false;
- });
+
holder.itemView.setOnClickListener(v -> {
MainActivity activity = mainActivityRef.get();
- if (activity != null) {
+ if (activity != null && !inActionMode()) {
long[] ids = FeedItemUtil.getIds(episodes);
int position = ArrayUtils.indexOf(ids, item.getId());
activity.loadChildFragment(ItemPagerFragment.newInstance(ids, position));
+ } else {
+ toggleSelection(pos);
}
});
holder.itemView.setOnCreateContextMenuListener(this);
+ holder.itemView.setOnLongClickListener(v -> {
+ longPressedItem = item;
+ longPressedPosition = pos;
+ return false;
+ });
+
+ if (inActionMode()) {
+ holder.secondaryActionButton.setVisibility(View.GONE);
+ holder.selectCheckBox.setOnClickListener(v -> {
+ toggleSelection(pos);
+ });
+ holder.selectCheckBox.setChecked(isSelected(pos));
+ holder.selectCheckBox.setVisibility(View.VISIBLE);
+ } else {
+ holder.selectCheckBox.setVisibility(View.GONE);
+ }
+
afterBindViewHolder(holder, pos);
holder.hideSeparatorIfNecessary();
}
@@ -106,6 +127,7 @@ public class EpisodeItemListAdapter extends RecyclerView.Adapter<EpisodeItemView
* Instead, we tell the adapter to use partial binding by calling {@link #notifyItemChanged(int, Object)}.
* We actually ignore the payload and always do a full bind but calling the partial bind method ensures
* that ViewHolders are always re-used.
+ *
* @param position Position of the item that has changed
*/
public void notifyItemChangedCompat(int position) {
@@ -113,8 +135,8 @@ public class EpisodeItemListAdapter extends RecyclerView.Adapter<EpisodeItemView
}
@Nullable
- public FeedItem getSelectedItem() {
- return selectedItem;
+ public FeedItem getLongPressedItem() {
+ return longPressedItem;
}
@Override
@@ -139,8 +161,37 @@ public class EpisodeItemListAdapter extends RecyclerView.Adapter<EpisodeItemView
@Override
public void onCreateContextMenu(final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
MenuInflater inflater = mainActivityRef.get().getMenuInflater();
- inflater.inflate(R.menu.feeditemlist_context, menu);
- menu.setHeaderTitle(selectedItem.getTitle());
- FeedItemMenuHandler.onPrepareMenu(menu, selectedItem, R.id.skip_episode_item);
+ if (inActionMode()) {
+ inflater.inflate(R.menu.multi_select_context_popup, menu);
+ } else {
+ inflater.inflate(R.menu.feeditemlist_context, menu);
+ menu.setHeaderTitle(longPressedItem.getTitle());
+ FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item);
+ }
+ }
+
+ public boolean onContextItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.multi_select) {
+ startSelectMode(longPressedPosition);
+ return true;
+ } else if (item.getItemId() == R.id.select_all_above) {
+ setSelected(0, longPressedPosition, true);
+ return true;
+ } else if (item.getItemId() == R.id.select_all_below) {
+ setSelected(longPressedPosition + 1, getItemCount(), true);
+ return true;
+ }
+ return false;
}
+
+ public List<FeedItem> getSelectedItems() {
+ List<FeedItem> items = new ArrayList<>();
+ for (int i = 0; i < getItemCount(); i++) {
+ if (isSelected(i)) {
+ items.add(getItem(i));
+ }
+ }
+ return items;
+ }
+
}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java
index 01712ea29..117ba3258 100644
--- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java
@@ -64,10 +64,10 @@ public class QueueRecyclerAdapter extends EpisodeItemListAdapter {
super.onCreateContextMenu(menu, v, menuInfo);
final boolean keepSorted = UserPreferences.isQueueKeepSorted();
- if (getItem(0).getId() == getSelectedItem().getId() || keepSorted) {
+ if (getItem(0).getId() == getLongPressedItem().getId() || keepSorted) {
menu.findItem(R.id.move_to_top_item).setVisible(false);
}
- if (getItem(getItemCount() - 1).getId() == getSelectedItem().getId() || keepSorted) {
+ if (getItem(getItemCount() - 1).getId() == getLongPressedItem().getId() || keepSorted) {
menu.findItem(R.id.move_to_bottom_item).setVisible(false);
}
}
diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java
new file mode 100644
index 000000000..670357d16
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java
@@ -0,0 +1,168 @@
+package de.danoeh.antennapod.adapter;
+
+import android.app.Activity;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import de.danoeh.antennapod.R;
+
+import java.util.HashSet;
+
+/**
+ * Used by Recyclerviews that need to provide ability to select items.
+ */
+abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {
+ private ActionMode actionMode;
+ private final HashSet<Long> selectedIds = new HashSet<>();
+ private final Activity activity;
+ private OnEndSelectModeListener onEndSelectModeListener;
+
+ public SelectableAdapter(Activity activity) {
+ this.activity = activity;
+ }
+
+ public void startSelectMode(int pos) {
+ if (inActionMode()) {
+ endSelectMode();
+ }
+
+ selectedIds.clear();
+ selectedIds.add(getItemId(pos));
+ notifyDataSetChanged();
+
+ actionMode = activity.startActionMode(new ActionMode.Callback() {
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.multi_select_options, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ updateTitle();
+ toggleSelectAllIcon(menu.findItem(R.id.select_toggle), false);
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ if (item.getItemId() == R.id.select_toggle) {
+ boolean allSelected = selectedIds.size() == getItemCount();
+ setSelected(0, getItemCount(), !allSelected);
+ toggleSelectAllIcon(item, !allSelected);
+ updateTitle();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ callOnEndSelectMode();
+ actionMode = null;
+ selectedIds.clear();
+ notifyDataSetChanged();
+ }
+ });
+ updateTitle();
+ }
+
+ /**
+ * End action mode if currently in select mode, otherwise do nothing
+ */
+ public void endSelectMode() {
+ if (inActionMode()) {
+ callOnEndSelectMode();
+ actionMode.finish();
+ }
+ }
+
+ public boolean isSelected(int pos) {
+ return selectedIds.contains(getItemId(pos));
+ }
+
+ /**
+ * Set the selected state of item at given position
+ *
+ * @param pos the position to select
+ * @param selected true for selected state and false for unselected
+ */
+ public void setSelected(int pos, boolean selected) {
+ if (selected) {
+ selectedIds.add(getItemId(pos));
+ } else {
+ selectedIds.remove(getItemId(pos));
+ }
+ updateTitle();
+ }
+
+ /**
+ * Set the selected state of item for a given range
+ *
+ * @param startPos start position of range, inclusive
+ * @param endPos end position of range, inclusive
+ * @param selected indicates the selection state
+ * @throws IllegalArgumentException if start and end positions are not valid
+ */
+ public void setSelected(int startPos, int endPos, boolean selected) throws IllegalArgumentException {
+ for (int i = startPos; i < endPos && i < getItemCount(); i++) {
+ setSelected(i, selected);
+ }
+ notifyItemRangeChanged(startPos, (endPos - startPos));
+ }
+
+ protected void toggleSelection(int pos) {
+ setSelected(pos, !isSelected(pos));
+ notifyItemChanged(pos);
+
+ if (selectedIds.size() == 0) {
+ endSelectMode();
+ }
+ }
+
+ public boolean inActionMode() {
+ return actionMode != null;
+ }
+
+ public int getSelectedCount() {
+ return selectedIds.size();
+ }
+
+ private void toggleSelectAllIcon(MenuItem selectAllItem, boolean allSelected) {
+ if (allSelected) {
+ selectAllItem.setIcon(R.drawable.ic_select_none);
+ selectAllItem.setTitle(R.string.deselect_all_label);
+ } else {
+ selectAllItem.setIcon(R.drawable.ic_select_all);
+ selectAllItem.setTitle(R.string.select_all_label);
+ }
+ }
+
+ private void updateTitle() {
+ if (actionMode == null) {
+ return;
+ }
+ actionMode.setTitle(activity.getResources()
+ .getQuantityString(R.plurals.num_selected_label, selectedIds.size(),
+ selectedIds.size(), getItemCount()));
+ }
+
+ public void setOnEndSelectModeListener(OnEndSelectModeListener onEndSelectModeListener) {
+ this.onEndSelectModeListener = onEndSelectModeListener;
+ }
+
+ private void callOnEndSelectMode() {
+ if (onEndSelectModeListener != null) {
+ onEndSelectModeListener.onEndSelectMode();
+ }
+ }
+
+ public interface OnEndSelectModeListener {
+ void onEndSelectMode();
+ }
+}
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 66f85d84d..9e3402972 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java
@@ -135,7 +135,7 @@ public class CompletedDownloadsFragment extends Fragment {
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
- FeedItem selectedItem = adapter.getSelectedItem();
+ FeedItem selectedItem = adapter.getLongPressedItem();
if (selectedItem == null) {
Log.i(TAG, "Selected item at current position was null, ignoring selection");
return super.onContextItemSelected(item);
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java
index e88ef432c..9e5600d76 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/EpisodesListFragment.java
@@ -177,11 +177,11 @@ public abstract class EpisodesListFragment extends Fragment {
return true; // avoids that the position is reset when we need it in the submenu
}
- if (listAdapter.getSelectedItem() == null) {
+ if (listAdapter.getLongPressedItem() == null) {
Log.i(TAG, "Selected item or listAdapter was null, ignoring selection");
return super.onContextItemSelected(item);
}
- FeedItem selectedItem = listAdapter.getSelectedItem();
+ FeedItem selectedItem = listAdapter.getLongPressedItem();
return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem);
}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
index de27ff0af..11f9950f9 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java
@@ -1,13 +1,14 @@
package de.danoeh.antennapod.fragment;
import android.content.Context;
-import android.content.res.Configuration;
import android.content.Intent;
+import android.content.res.Configuration;
import android.graphics.LightingColorFilter;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
+import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
@@ -18,6 +19,7 @@ import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatDrawableManager;
@@ -30,8 +32,19 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.CollapsingToolbarLayout;
+import com.google.android.material.snackbar.Snackbar;
import com.joanzapata.iconify.Iconify;
import com.joanzapata.iconify.widget.IconTextView;
+import com.leinardi.android.speeddial.SpeedDialView;
+
+import org.apache.commons.lang3.Validate;
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+
+import java.util.List;
+import java.util.Set;
+
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
@@ -58,10 +71,10 @@ import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.FeedItemPermutors;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil;
-import de.danoeh.antennapod.dialog.EpisodesApplyActionFragment;
import de.danoeh.antennapod.dialog.FilterDialog;
import de.danoeh.antennapod.dialog.RemoveFeedDialog;
import de.danoeh.antennapod.dialog.RenameFeedDialog;
+import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
@@ -72,19 +85,12 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
-import org.apache.commons.lang3.Validate;
-import org.greenrobot.eventbus.EventBus;
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-
-import java.util.List;
-import java.util.Set;
/**
* Displays a list of FeedItems.
*/
public class FeedItemlistFragment extends Fragment implements AdapterView.OnItemClickListener,
- Toolbar.OnMenuItemClickListener {
+ Toolbar.OnMenuItemClickListener, EpisodeItemListAdapter.OnEndSelectModeListener {
private static final String TAG = "ItemlistFragment";
private static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id";
private static final String KEY_UP_ARROW = "up_arrow";
@@ -106,6 +112,8 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
private View header;
private Toolbar toolbar;
private ToolbarIconTintManager iconTintManager;
+ private SpeedDialView speedDialView;
+
private boolean displayUpArrow;
private long feedID;
@@ -222,6 +230,32 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
});
loadItems();
+
+ // Init action UI (via a FAB Speed Dial)
+ speedDialView = root.findViewById(R.id.fabSD);
+ speedDialView.inflate(R.menu.episodes_apply_action_speeddial);
+ speedDialView.setOnChangeListener(new SpeedDialView.OnChangeListener() {
+ @Override
+ public boolean onMainActionSelected() {
+ return false;
+ }
+
+ @Override
+ public void onToggleChanged(boolean open) {
+ if (open && adapter.getSelectedCount() == 0) {
+ ((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected,
+ Snackbar.LENGTH_SHORT);
+ speedDialView.close();
+ }
+ }
+ });
+ speedDialView.setOnActionSelectedListener(actionItem -> {
+ new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), adapter.getSelectedItems())
+ .handleAction(actionItem.getId());
+ onEndSelectMode();
+ adapter.endSelectMode();
+ return true;
+ });
return root;
}
@@ -292,17 +326,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
return true;
}
switch (item.getItemId()) {
- case R.id.episode_actions:
- int actions = EpisodesApplyActionFragment.ACTION_ALL;
- if (feed.isLocalFeed()) {
- // turn off download and delete actions for local feed
- actions ^= EpisodesApplyActionFragment.ACTION_DOWNLOAD;
- actions ^= EpisodesApplyActionFragment.ACTION_DELETE;
- }
- EpisodesApplyActionFragment fragment = EpisodesApplyActionFragment
- .newInstance(feed.getItems(), actions);
- ((MainActivity) getActivity()).loadChildFragment(fragment);
- return true;
case R.id.rename_item:
new RenameFeedDialog(getActivity(), feed).show();
return true;
@@ -317,11 +340,24 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
- FeedItem selectedItem = adapter.getSelectedItem();
+ FeedItem selectedItem = adapter.getLongPressedItem();
if (selectedItem == null) {
Log.i(TAG, "Selected item at current position was null, ignoring selection");
return super.onContextItemSelected(item);
}
+
+ if (item.getItemId() == R.id.multi_select) {
+ if (feed.isLocalFeed()) {
+ speedDialView.removeActionItemById(R.id.download_batch);
+ speedDialView.removeActionItemById(R.id.delete_batch);
+ }
+ speedDialView.setVisibility(View.VISIBLE);
+ refreshToolbarState();
+ // Do not return: Let adapter handle its actions, too.
+ }
+ if (adapter.onContextItemSelected(item)) {
+ return true;
+ }
return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem);
}
@@ -393,6 +429,12 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
}
}
+ @Override
+ public void onEndSelectMode() {
+ speedDialView.close();
+ speedDialView.setVisibility(View.GONE);
+ }
+
private void updateUi() {
loadItems();
updateSyncProgressBarVisibility();
@@ -433,6 +475,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
if (adapter == null) {
recyclerView.setAdapter(null);
adapter = new FeedItemListAdapter((MainActivity) getActivity());
+ adapter.setOnEndSelectModeListener(this);
recyclerView.setAdapter(adapter);
}
progressBar.setVisibility(View.GONE);
@@ -592,5 +635,13 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
protected void beforeBindViewHolder(EpisodeItemViewHolder holder, int pos) {
holder.coverHolder.setVisibility(View.GONE);
}
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
+ super.onCreateContextMenu(menu, v, menuInfo);
+ if (!inActionMode()) {
+ menu.findItem(R.id.multi_select).setVisible(true);
+ }
+ }
}
-} \ No newline at end of file
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
index 84b0e97c1..5e3d36c03 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java
@@ -172,7 +172,7 @@ public class PlaybackHistoryFragment extends Fragment implements Toolbar.OnMenuI
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
- FeedItem selectedItem = adapter.getSelectedItem();
+ FeedItem selectedItem = adapter.getLongPressedItem();
if (selectedItem == null) {
Log.i(TAG, "Selected item at current position was null, ignoring selection");
return super.onContextItemSelected(item);
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 80f666fa2..d309ac253 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java
@@ -391,7 +391,7 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
if (!isVisible() || recyclerAdapter == null) {
return false;
}
- FeedItem selectedItem = recyclerAdapter.getSelectedItem();
+ FeedItem selectedItem = recyclerAdapter.getLongPressedItem();
if (selectedItem == null) {
Log.i(TAG, "Selected item was null, ignoring selection");
return super.onContextItemSelected(item);
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
index 0394b5987..855078d03 100644
--- a/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/SearchFragment.java
@@ -191,7 +191,7 @@ public class SearchFragment extends Fragment {
@Override
public boolean onContextItemSelected(@NonNull MenuItem item) {
- FeedItem selectedItem = adapter.getSelectedItem();
+ FeedItem selectedItem = adapter.getLongPressedItem();
if (selectedItem == null) {
Log.i(TAG, "Selected item at current position was null, ignoring selection");
return super.onContextItemSelected(item);
diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java b/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java
new file mode 100644
index 000000000..028d2fff4
--- /dev/null
+++ b/app/src/main/java/de/danoeh/antennapod/fragment/actions/EpisodeMultiSelectActionHandler.java
@@ -0,0 +1,130 @@
+package de.danoeh.antennapod.fragment.actions;
+
+import android.util.Log;
+
+import androidx.annotation.PluralsRes;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
+import de.danoeh.antennapod.core.storage.DBWriter;
+import de.danoeh.antennapod.core.storage.DownloadRequestException;
+import de.danoeh.antennapod.core.storage.DownloadRequester;
+import de.danoeh.antennapod.core.util.LongList;
+import de.danoeh.antennapod.model.feed.FeedItem;
+
+public class EpisodeMultiSelectActionHandler {
+ private static final String TAG = "EpisodeSelectHandler";
+ private final MainActivity activity;
+ private final List<FeedItem> selectedItems;
+
+ public EpisodeMultiSelectActionHandler(MainActivity activity, List<FeedItem> selectedItems) {
+ this.activity = activity;
+ this.selectedItems = selectedItems;
+ }
+
+ public void handleAction(int id) {
+ if (id == R.id.add_to_queue_batch) {
+ queueChecked();
+ } else if (id == R.id.remove_from_queue_batch) {
+ removeFromQueueChecked();
+ } else if (id == R.id.mark_read_batch) {
+ markedCheckedPlayed();
+ } else if (id == R.id.mark_unread_batch) {
+ markedCheckedUnplayed();
+ } else if (id == R.id.download_batch) {
+ downloadChecked();
+ } else if (id == R.id.delete_batch) {
+ deleteChecked();
+ } else {
+ Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + id);
+ }
+ }
+
+ private void queueChecked() {
+ // Check if an episode actually contains any media files before adding it to queue
+ LongList toQueue = new LongList(selectedItems.size());
+ for (FeedItem episode : selectedItems) {
+ if (episode.hasMedia()) {
+ toQueue.add(episode.getId());
+ }
+ }
+ DBWriter.addQueueItem(activity, true, toQueue.toArray());
+ showMessage(R.plurals.added_to_queue_batch_label, toQueue.size());
+ }
+
+ private void removeFromQueueChecked() {
+ long[] checkedIds = getSelectedIds();
+ DBWriter.removeQueueItem(activity, true, checkedIds);
+ showMessage(R.plurals.removed_from_queue_batch_label, checkedIds.length);
+ }
+
+ private void markedCheckedPlayed() {
+ long[] checkedIds = getSelectedIds();
+ DBWriter.markItemPlayed(FeedItem.PLAYED, checkedIds);
+ showMessage(R.plurals.marked_read_batch_label, checkedIds.length);
+ }
+
+ private void markedCheckedUnplayed() {
+ long[] checkedIds = getSelectedIds();
+ DBWriter.markItemPlayed(FeedItem.UNPLAYED, checkedIds);
+ showMessage(R.plurals.marked_unread_batch_label, checkedIds.length);
+ }
+
+ private void downloadChecked() {
+ // download the check episodes in the same order as they are currently displayed
+ List<FeedItem> toDownload = new ArrayList<>(selectedItems.size());
+ for (FeedItem episode : selectedItems) {
+ if (episode.hasMedia() && !episode.getFeed().isLocalFeed()) {
+ toDownload.add(episode);
+ }
+ }
+ try {
+ DownloadRequester.getInstance().downloadMedia(activity, true, toDownload.toArray(new FeedItem[0]));
+ } catch (DownloadRequestException e) {
+ e.printStackTrace();
+ DownloadRequestErrorDialogCreator.newRequestErrorDialog(activity, e.getMessage());
+ }
+ showMessage(R.plurals.downloading_batch_label, toDownload.size());
+ }
+
+ private void deleteChecked() {
+ int countHasMedia = 0;
+ int countNoMedia = 0;
+ for (FeedItem feedItem : selectedItems) {
+ if (feedItem.hasMedia() && feedItem.getMedia().isDownloaded()) {
+ countHasMedia++;
+ DBWriter.deleteFeedMediaOfItem(activity, feedItem.getMedia().getId());
+ } else {
+ countNoMedia++;
+ }
+ }
+ showMessageMore(R.plurals.deleted_multi_episode_batch_label, countNoMedia, countHasMedia);
+ }
+
+ private void showMessage(@PluralsRes int msgId, int numItems) {
+ activity.showSnackbarAbovePlayer(activity.getResources()
+ .getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG);
+ }
+
+ private void showMessageMore(@PluralsRes int msgId, int countNoMedia, int countHasMedia) {
+ activity.showSnackbarAbovePlayer(activity.getResources()
+ .getQuantityString(msgId,
+ (countHasMedia + countNoMedia),
+ (countHasMedia + countNoMedia), countHasMedia),
+ Snackbar.LENGTH_LONG);
+ }
+
+ private long[] getSelectedIds() {
+ long[] checkedIds = new long[selectedItems.size()];
+ for (int i = 0; i < selectedItems.size(); ++i) {
+ checkedIds[i] = selectedItems.get(i).getId();
+ }
+ return checkedIds;
+ }
+}
diff --git a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java
index 428ce9e1e..02d45b2a0 100644
--- a/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java
+++ b/app/src/main/java/de/danoeh/antennapod/view/viewholder/EpisodeItemViewHolder.java
@@ -7,13 +7,16 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;
+
import com.joanzapata.iconify.Iconify;
+
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.CoverLoader;
@@ -31,8 +34,8 @@ import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.DateUtils;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.NetworkUtils;
-import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.ui.common.CircularProgressBar;
+import de.danoeh.antennapod.ui.common.ThemeUtils;
/**
* Holds the view which shows FeedItems.
@@ -60,6 +63,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder {
private final TextView separatorIcons;
private final View leftPadding;
public final CardView coverHolder;
+ public final CheckBox selectCheckBox;
private final MainActivity activity;
private FeedItem item;
@@ -91,6 +95,7 @@ public class EpisodeItemViewHolder extends RecyclerView.ViewHolder {
coverHolder = itemView.findViewById(R.id.coverHolder);
leftPadding = itemView.findViewById(R.id.left_padding);
itemView.setTag(this);
+ selectCheckBox = itemView.findViewById(R.id.selectCheckBox);
}
public void bind(FeedItem item) {
diff --git a/app/src/main/res/layout/feed_item_list_fragment.xml b/app/src/main/res/layout/feed_item_list_fragment.xml
index 6dc484e2f..734ce64dd 100644
--- a/app/src/main/res/layout/feed_item_list_fragment.xml
+++ b/app/src/main/res/layout/feed_item_list_fragment.xml
@@ -1,51 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="match_parent"
- android:layout_height="match_parent">
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
- android:id="@+id/appBar"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ android:id="@+id/appBar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
- android:id="@+id/collapsing_toolbar"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="?android:attr/windowBackground"
- app:contentScrim="?android:attr/windowBackground"
- app:scrimAnimationDuration="200"
- app:layout_scrollFlags="scroll|exitUntilCollapsed">
+ android:id="@+id/collapsing_toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:background="?android:attr/windowBackground"
+ app:contentScrim="?android:attr/windowBackground"
+ app:scrimAnimationDuration="200"
+ app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView
- android:id="@+id/imgvBackground"
- style="@style/BigBlurryBackground"
- android:background="@color/image_readability_tint"
- android:layout_width="match_parent"
- android:layout_height="232dp"
- app:layout_collapseMode="parallax"
- app:layout_collapseParallaxMultiplier="0.6"/>
+ android:id="@+id/imgvBackground"
+ android:layout_width="match_parent"
+ android:layout_height="232dp"
+ android:background="@color/image_readability_tint"
+ style="@style/BigBlurryBackground"
+ app:layout_collapseMode="parallax"
+ app:layout_collapseParallaxMultiplier="0.6" />
- <include layout="@layout/feeditemlist_header"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom"
- app:layout_collapseMode="parallax"
- app:layout_collapseParallaxMultiplier="0.6" />
+ <include
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ layout="@layout/feeditemlist_header"
+ app:layout_collapseMode="parallax"
+ app:layout_collapseParallaxMultiplier="0.6" />
<androidx.appcompat.widget.Toolbar
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:minHeight="?attr/actionBarSize"
- android:theme="?attr/actionBarTheme"
- android:layout_alignParentTop="true"
- android:id="@+id/toolbar"
- app:navigationIcon="?homeAsUpIndicator"
- app:layout_collapseMode="pin"/>
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="?attr/actionBarSize"
+ android:theme="?attr/actionBarTheme"
+ android:layout_alignParentTop="true"
+ app:navigationIcon="?homeAsUpIndicator"
+ app:layout_collapseMode="pin" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
+
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
@@ -54,27 +56,30 @@
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
- <de.danoeh.antennapod.view.EpisodeItemListRecyclerView
- android:id="@+id/recyclerView"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:paddingHorizontal="@dimen/additional_horizontal_spacing" />
+ <de.danoeh.antennapod.view.EpisodeItemListRecyclerView
+ android:id="@+id/recyclerView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingHorizontal="@dimen/additional_horizontal_spacing" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<ProgressBar
- android:id="@+id/progLoading"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:indeterminateOnly="true"
- android:visibility="gone"/>
+ android:id="@+id/progLoading"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminateOnly="true"
+ android:visibility="gone" />
<include
- layout="@layout/more_content_list_footer"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom"
- android:visibility="gone"/>
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom"
+ android:visibility="gone"
+ layout="@layout/more_content_list_footer" />
+
+ <include
+ layout="@layout/multi_select_speed_dial" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
diff --git a/app/src/main/res/layout/feeditemlist_item.xml b/app/src/main/res/layout/feeditemlist_item.xml
index 37b88d1b5..0202e0e34 100644
--- a/app/src/main/res/layout/feeditemlist_item.xml
+++ b/app/src/main/res/layout/feeditemlist_item.xml
@@ -1,177 +1,185 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
- xmlns:app="http://schemas.android.com/apk/res-auto"
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="wrap_content">
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
<!--
This parent FrameLayout is necessary because RecyclerView's ItemAnimator changes alpha values,
which conflicts with our played state indicator.
-->
-
<LinearLayout
- android:id="@+id/container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:orientation="horizontal"
- android:gravity="center_vertical"
- android:baselineAligned="false"
- android:paddingStart="12dp"
- android:paddingLeft="12dp"
- android:paddingEnd="0dp"
- android:paddingRight="0dp"
- tools:ignore="UselessParent">
+ android:id="@+id/container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical"
+ android:baselineAligned="false"
+ android:paddingStart="12dp"
+ android:paddingLeft="12dp"
+ android:paddingEnd="0dp"
+ android:paddingRight="0dp"
+ tools:ignore="UselessParent">
<LinearLayout
- android:layout_width="wrap_content"
- android:layout_height="match_parent"
- android:id="@+id/left_padding"
- android:minWidth="4dp">
+ android:id="@+id/left_padding"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:minWidth="4dp">
+
<ImageView
- android:id="@+id/drag_handle"
- android:layout_width="16dp"
- android:layout_height="match_parent"
- android:importantForAccessibility="no"
- android:scaleType="fitCenter"
- app:srcCompat="?attr/dragview_background"
- android:paddingStart="0dp"
- android:paddingLeft="0dp"
- android:paddingEnd="4dp"
- android:paddingRight="4dp"
- tools:src="@drawable/ic_drag_darktheme"
- tools:background="@android:color/holo_green_dark"/>
+ android:id="@+id/drag_handle"
+ android:layout_width="16dp"
+ android:layout_height="match_parent"
+ android:importantForAccessibility="no"
+ android:scaleType="fitCenter"
+ android:paddingStart="0dp"
+ android:paddingLeft="0dp"
+ android:paddingEnd="4dp"
+ android:paddingRight="4dp"
+ app:srcCompat="?attr/dragview_background"
+ tools:src="@drawable/ic_drag_darktheme"
+ tools:background="@android:color/holo_green_dark" />
+
+ <!-- Needs to have the same width as the action button. Otherwise, the screen jumps around. -->
+ <CheckBox
+ android:id="@+id/selectCheckBox"
+ android:layout_width="60dp"
+ android:layout_height="match_parent" />
</LinearLayout>
<androidx.cardview.widget.CardView
- android:layout_width="@dimen/thumbnail_length_queue_item"
- android:layout_height="@dimen/thumbnail_length_queue_item"
- android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding"
- android:layout_marginTop="@dimen/listitem_threeline_verticalpadding"
- android:layout_marginRight="@dimen/listitem_threeline_textleftpadding"
- android:layout_marginEnd="@dimen/listitem_threeline_textleftpadding"
- android:id="@+id/coverHolder"
- app:cardBackgroundColor="@color/non_square_icon_background"
- app:cardCornerRadius="4dp"
- app:cardPreventCornerOverlap="false"
- app:cardElevation="0dp">
+ android:id="@+id/coverHolder"
+ android:layout_width="@dimen/thumbnail_length_queue_item"
+ android:layout_height="@dimen/thumbnail_length_queue_item"
+ android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding"
+ android:layout_marginTop="@dimen/listitem_threeline_verticalpadding"
+ android:layout_marginRight="@dimen/listitem_threeline_textleftpadding"
+ android:layout_marginEnd="@dimen/listitem_threeline_textleftpadding"
+ app:cardBackgroundColor="@color/non_square_icon_background"
+ app:cardCornerRadius="4dp"
+ app:cardPreventCornerOverlap="false"
+ app:cardElevation="0dp">
<RelativeLayout
- android:layout_width="match_parent"
- android:layout_height="match_parent">
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
<TextView
- android:id="@+id/txtvPlaceholder"
- android:layout_width="@dimen/thumbnail_length_queue_item"
- android:layout_height="@dimen/thumbnail_length_queue_item"
- android:layout_centerVertical="true"
- android:gravity="center"
- android:background="@color/light_gray"
- android:maxLines="3"
- android:padding="2dp"
- android:ellipsize="end"/>
+ android:id="@+id/txtvPlaceholder"
+ android:layout_width="@dimen/thumbnail_length_queue_item"
+ android:layout_height="@dimen/thumbnail_length_queue_item"
+ android:layout_centerVertical="true"
+ android:gravity="center"
+ android:background="@color/light_gray"
+ android:maxLines="3"
+ android:padding="2dp"
+ android:ellipsize="end" />
+
<ImageView
- android:id="@+id/imgvCover"
- android:layout_width="@dimen/thumbnail_length_queue_item"
- android:layout_height="@dimen/thumbnail_length_queue_item"
- android:layout_centerVertical="true"
- android:importantForAccessibility="no"
- tools:src="@tools:sample/avatars"/>
+ android:id="@+id/imgvCover"
+ android:layout_width="@dimen/thumbnail_length_queue_item"
+ android:layout_height="@dimen/thumbnail_length_queue_item"
+ android:layout_centerVertical="true"
+ android:importantForAccessibility="no"
+ tools:src="@tools:sample/avatars" />
</RelativeLayout>
+
</androidx.cardview.widget.CardView>
<LinearLayout
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding"
- android:layout_marginRight="@dimen/listitem_threeline_textrightpadding"
- android:layout_marginEnd="@dimen/listitem_threeline_textrightpadding"
- android:layout_marginTop="@dimen/listitem_threeline_verticalpadding"
- android:layout_weight="1"
- tools:background="@android:color/holo_red_dark"
- android:orientation="vertical">
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/listitem_threeline_verticalpadding"
+ android:layout_marginRight="@dimen/listitem_threeline_textrightpadding"
+ android:layout_marginEnd="@dimen/listitem_threeline_textrightpadding"
+ android:layout_marginTop="@dimen/listitem_threeline_verticalpadding"
+ android:layout_weight="1"
+ android:orientation="vertical"
+ tools:background="@android:color/holo_red_dark">
<LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:id="@+id/status"
- android:orientation="horizontal"
- android:gravity="center_vertical">
+ android:id="@+id/status"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
<TextView
- android:text="@string/new_label"
- style="@style/AntennaPod.TextView.UnreadIndicator"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:id="@+id/statusUnread"
- android:layout_marginRight="4dp"
- android:layout_marginEnd="4dp"
- tools:text="@sample/episodes.json/data/status_label"/>
+ android:id="@+id/statusUnread"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/new_label"
+ android:layout_marginRight="4dp"
+ android:layout_marginEnd="4dp"
+ style="@style/AntennaPod.TextView.UnreadIndicator"
+ tools:text="@sample/episodes.json/data/status_label" />
<ImageView
- android:layout_width="14sp"
- android:layout_height="14sp"
- app:srcCompat="@drawable/ic_videocam"
- android:contentDescription="@string/media_type_video_label"
- android:id="@+id/ivIsVideo"/>
+ android:id="@+id/ivIsVideo"
+ android:layout_width="14sp"
+ android:layout_height="14sp"
+ android:contentDescription="@string/media_type_video_label"
+ app:srcCompat="@drawable/ic_videocam" />
<ImageView
- android:layout_width="14sp"
- android:layout_height="14sp"
- app:srcCompat="@drawable/ic_star"
- android:contentDescription="@string/is_favorite_label"
- android:id="@+id/isFavorite"/>
+ android:id="@+id/isFavorite"
+ android:layout_width="14sp"
+ android:layout_height="14sp"
+ android:contentDescription="@string/is_favorite_label"
+ app:srcCompat="@drawable/ic_star" />
<ImageView
- android:layout_width="14sp"
- android:layout_height="14sp"
- app:srcCompat="@drawable/ic_playlist"
- android:contentDescription="@string/in_queue_label"
- android:id="@+id/ivInPlaylist"/>
+ android:id="@+id/ivInPlaylist"
+ android:layout_width="14sp"
+ android:layout_height="14sp"
+ android:contentDescription="@string/in_queue_label"
+ app:srcCompat="@drawable/ic_playlist" />
<TextView
- android:id="@+id/separatorIcons"
- style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginLeft="4dp"
- android:layout_marginStart="4dp"
- android:layout_marginRight="4dp"
- android:layout_marginEnd="4dp"
- android:text="·"
- android:importantForAccessibility="no"
- tools:background="@android:color/holo_blue_light"/>
+ android:id="@+id/separatorIcons"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="4dp"
+ android:layout_marginStart="4dp"
+ android:layout_marginRight="4dp"
+ android:layout_marginEnd="4dp"
+ android:text="·"
+ android:importantForAccessibility="no"
+ style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
+ tools:background="@android:color/holo_blue_light" />
<TextView
- android:id="@+id/txtvPubDate"
- style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginRight="4dp"
- android:layout_marginEnd="4dp"
- tools:text="@sample/episodes.json/data/published_at"/>
+ android:id="@+id/txtvPubDate"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="4dp"
+ android:layout_marginEnd="4dp"
+ style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
+ tools:text="@sample/episodes.json/data/published_at" />
<TextView
- style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginRight="4dp"
- android:layout_marginEnd="4dp"
- android:text="·"
- android:importantForAccessibility="no"
- tools:background="@android:color/holo_blue_light"/>
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="4dp"
+ android:layout_marginEnd="4dp"
+ android:text="·"
+ android:importantForAccessibility="no"
+ style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
+ tools:background="@android:color/holo_blue_light" />
<TextView
- android:id="@+id/size"
- style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
- android:layout_marginRight="4dp"
- android:layout_marginEnd="4dp"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- tools:text="10 MB"/>
+ android:id="@+id/size"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="4dp"
+ android:layout_marginEnd="4dp"
+ style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
+ tools:text="10 MB" />
</LinearLayout>
@@ -181,55 +189,57 @@
Keep this in mind when changing the order of this layout!
-->
<TextView
- android:id="@+id/txtvTitle"
- style="@style/AntennaPod.TextView.ListItemPrimaryTitle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- tools:text="@sample/episodes.json/data/title"
- android:importantForAccessibility="no"
- android:ellipsize="end"
- tools:background="@android:color/holo_blue_light"/>
+ android:id="@+id/txtvTitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:importantForAccessibility="no"
+ android:ellipsize="end"
+ style="@style/AntennaPod.TextView.ListItemPrimaryTitle"
+ tools:text="@sample/episodes.json/data/title"
+ tools:background="@android:color/holo_blue_light" />
<LinearLayout
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:id="@+id/progress"
- android:orientation="horizontal"
- android:gravity="center_vertical">
+ android:id="@+id/progress"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_vertical">
<TextView
- android:id="@+id/txtvPosition"
- style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="0dp"
- tools:text="00:42:23"
- tools:background="@android:color/holo_blue_light"/>
+ android:id="@+id/txtvPosition"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="0dp"
+ style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
+ tools:text="00:42:23"
+ tools:background="@android:color/holo_blue_light" />
<ProgressBar
- android:id="@+id/progressBar"
- style="?attr/progressBarTheme"
- android:layout_width="0dp"
- android:layout_weight="1"
- android:layout_height="4dp"
- android:max="100"
- android:layout_margin="4dp"
- tools:background="@android:color/holo_blue_light"/>
+ android:id="@+id/progressBar"
+ android:layout_width="0dp"
+ android:layout_height="4dp"
+ android:layout_weight="1"
+ android:max="100"
+ android:layout_margin="4dp"
+ style="?attr/progressBarTheme"
+ tools:background="@android:color/holo_blue_light" />
<TextView
- android:id="@+id/txtvDuration"
- style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginBottom="0dp"
- tools:text="@sample/episodes.json/data/duration"
- tools:background="@android:color/holo_blue_light"/>
+ android:id="@+id/txtvDuration"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="0dp"
+ style="@style/AntennaPod.TextView.ListItemSecondaryTitle"
+ tools:text="@sample/episodes.json/data/duration"
+ tools:background="@android:color/holo_blue_light" />
</LinearLayout>
</LinearLayout>
- <include layout="@layout/secondary_action"/>
+ <include
+ layout="@layout/secondary_action" />
</LinearLayout>
+
</FrameLayout>
diff --git a/app/src/main/res/layout/multi_select_speed_dial.xml b/app/src/main/res/layout/multi_select_speed_dial.xml
new file mode 100644
index 000000000..159b9a955
--- /dev/null
+++ b/app/src/main/res/layout/multi_select_speed_dial.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<merge
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <com.leinardi.android.speeddial.SpeedDialOverlayLayout
+ android:id="@+id/fabSDOverlay"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:importantForAccessibility="no" />
+
+ <!-- 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_gravity="bottom|right"
+ android:elevation="@dimen/sd_open_elevation">
+
+ <com.leinardi.android.speeddial.SpeedDialView
+ android:id="@+id/fabSD"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:accessibilityTraversalBefore="@android:id/list"
+ android:contentDescription="@string/apply_action"
+ android:visibility="gone"
+ app:sdMainFabClosedSrc="@drawable/ic_fab_edit"
+ app:sdOverlayLayout="@id/fabSDOverlay" />
+
+ </ScrollView>
+
+</merge>
diff --git a/app/src/main/res/menu/feeditemlist_context.xml b/app/src/main/res/menu/feeditemlist_context.xml
index 84e45d4c9..f55afd3f8 100644
--- a/app/src/main/res/menu/feeditemlist_context.xml
+++ b/app/src/main/res/menu/feeditemlist_context.xml
@@ -1,6 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
-
<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/multi_select"
+ android:menuCategory="container"
+ android:title="@string/multi_select"
+ android:visible="false"/>
<item
android:id="@id/skip_episode_item"
@@ -16,6 +20,7 @@
android:id="@+id/mark_read_item"
android:menuCategory="container"
android:title="@string/mark_read_label" />
+
<item
android:id="@+id/mark_unread_item"
android:menuCategory="container"
@@ -25,10 +30,12 @@
android:id="@+id/add_to_queue_item"
android:menuCategory="container"
android:title="@string/add_to_queue_label" />
+
<item
android:id="@+id/remove_from_queue_item"
android:menuCategory="container"
android:title="@string/remove_from_queue_label" />
+
<item
android:id="@+id/remove_item"
android:menuCategory="container"
@@ -38,6 +45,7 @@
android:id="@+id/add_to_favorites_item"
android:menuCategory="container"
android:title="@string/add_to_favorite_label" />
+
<item
android:id="@+id/remove_from_favorites_item"
android:menuCategory="container"
@@ -52,6 +60,7 @@
android:id="@+id/activate_auto_download"
android:menuCategory="container"
android:title="@string/activate_auto_download" />
+
<item
android:id="@+id/deactivate_auto_download"
android:menuCategory="container"
@@ -61,6 +70,7 @@
android:id="@+id/visit_website_item"
android:menuCategory="container"
android:title="@string/visit_website_label" />
+
<item
android:id="@+id/share_item"
android:menuCategory="container"
diff --git a/app/src/main/res/menu/feedlist.xml b/app/src/main/res/menu/feedlist.xml
index 85e7a95ba..306be66d1 100644
--- a/app/src/main/res/menu/feedlist.xml
+++ b/app/src/main/res/menu/feedlist.xml
@@ -38,13 +38,6 @@
android:title="@string/search_label"/>
<item
- android:id="@+id/episode_actions"
- android:menuCategory="container"
- android:icon="@drawable/ic_check_multiple"
- android:title="@string/multi_select"
- custom:showAsAction="collapseActionView">
- </item>
- <item
android:id="@+id/visit_website_item"
android:icon="@drawable/ic_web"
android:menuCategory="container"
diff --git a/app/src/main/res/menu/multi_select_context_popup.xml b/app/src/main/res/menu/multi_select_context_popup.xml
new file mode 100644
index 000000000..730b01016
--- /dev/null
+++ b/app/src/main/res/menu/multi_select_context_popup.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/select_all_above"
+ android:title="@string/select_all_above">
+ </item>
+ <item
+ android:id="@+id/select_all_below"
+ android:title="@string/select_all_below">
+ </item>
+</menu> \ No newline at end of file
diff --git a/app/src/main/res/menu/multi_select_options.xml b/app/src/main/res/menu/multi_select_options.xml
new file mode 100644
index 000000000..5cb2b7602
--- /dev/null
+++ b/app/src/main/res/menu/multi_select_options.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ <item
+ android:id="@+id/select_toggle"
+ android:title="@string/select_all_label"
+ app:showAsAction="always"/>
+</menu>