diff options
author | peakvalleytech <65185819+peakvalleytech@users.noreply.github.com> | 2021-06-29 13:01:04 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-29 22:01:04 +0200 |
commit | 323f1f61424c39f8cde6076a4d30501bc75fc109 (patch) | |
tree | 824dcc1e70c00af5e5959bbe147e5c67ebd556aa /app/src/main | |
parent | 1fbc565bc3e436491c197ff8610a3bf5b8660f8d (diff) | |
download | AntennaPod-323f1f61424c39f8cde6076a4d30501bc75fc109.zip |
Contextual menu for multi selecting episodes (#5130)
Diffstat (limited to 'app/src/main')
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> |