Merge pull request #5872 from ByteHamster/multi-select-all-episodes
Multi-Select on 'All episodes' screen
This commit is contained in:
commit
6e5004be22
|
@ -48,6 +48,7 @@ public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHol
|
|||
public void updateItems(List<FeedItem> items) {
|
||||
episodes = items;
|
||||
notifyDataSetChanged();
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -194,6 +195,7 @@ public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHol
|
|||
setSelected(0, longPressedPosition, true);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.select_all_below) {
|
||||
shouldSelectLazyLoadedItems = true;
|
||||
setSelected(longPressedPosition + 1, getItemCount(), true);
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -15,11 +15,14 @@ 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> {
|
||||
public abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<T> {
|
||||
public static final int COUNT_AUTOMATICALLY = -1;
|
||||
private ActionMode actionMode;
|
||||
private final HashSet<Long> selectedIds = new HashSet<>();
|
||||
private final Activity activity;
|
||||
private OnSelectModeListener onSelectModeListener;
|
||||
boolean shouldSelectLazyLoadedItems = false;
|
||||
private int totalNumberOfItems = COUNT_AUTOMATICALLY;
|
||||
|
||||
public SelectableAdapter(Activity activity) {
|
||||
this.activity = activity;
|
||||
|
@ -34,6 +37,7 @@ abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends Recy
|
|||
onSelectModeListener.onStartSelectMode();
|
||||
}
|
||||
|
||||
shouldSelectLazyLoadedItems = false;
|
||||
selectedIds.clear();
|
||||
selectedIds.add(getItemId(pos));
|
||||
notifyDataSetChanged();
|
||||
|
@ -56,9 +60,10 @@ abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends Recy
|
|||
@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);
|
||||
boolean selectAll = selectedIds.size() != getItemCount();
|
||||
shouldSelectLazyLoadedItems = selectAll;
|
||||
setSelected(0, getItemCount(), selectAll);
|
||||
toggleSelectAllIcon(item, selectAll);
|
||||
updateTitle();
|
||||
return true;
|
||||
}
|
||||
|
@ -69,6 +74,7 @@ abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends Recy
|
|||
public void onDestroyActionMode(ActionMode mode) {
|
||||
callOnEndSelectMode();
|
||||
actionMode = null;
|
||||
shouldSelectLazyLoadedItems = false;
|
||||
selectedIds.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
@ -147,13 +153,21 @@ abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends Recy
|
|||
}
|
||||
}
|
||||
|
||||
private void updateTitle() {
|
||||
void updateTitle() {
|
||||
if (actionMode == null) {
|
||||
return;
|
||||
}
|
||||
int totalCount = getItemCount();
|
||||
int selectedCount = selectedIds.size();
|
||||
if (totalNumberOfItems != COUNT_AUTOMATICALLY) {
|
||||
totalCount = totalNumberOfItems;
|
||||
if (shouldSelectLazyLoadedItems) {
|
||||
selectedCount += (totalNumberOfItems - getItemCount());
|
||||
}
|
||||
}
|
||||
actionMode.setTitle(activity.getResources()
|
||||
.getQuantityString(R.plurals.num_selected_label, selectedIds.size(),
|
||||
selectedIds.size(), getItemCount()));
|
||||
selectedCount, totalCount));
|
||||
}
|
||||
|
||||
public void setOnSelectModeListener(OnSelectModeListener onSelectModeListener) {
|
||||
|
@ -166,6 +180,18 @@ abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends Recy
|
|||
}
|
||||
}
|
||||
|
||||
public boolean shouldSelectLazyLoadedItems() {
|
||||
return shouldSelectLazyLoadedItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of items that could be lazy-loaded.
|
||||
* Can also be set to {@link #COUNT_AUTOMATICALLY} to simply use {@link #getItemCount}
|
||||
*/
|
||||
public void setTotalNumberOfItems(int totalNumberOfItems) {
|
||||
this.totalNumberOfItems = totalNumberOfItems;
|
||||
}
|
||||
|
||||
public interface OnSelectModeListener {
|
||||
void onStartSelectMode();
|
||||
|
||||
|
|
|
@ -66,7 +66,6 @@ public class AllEpisodesFragment extends EpisodesListFragment {
|
|||
public void onPrepareOptionsMenu(@NonNull Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
menu.findItem(R.id.filter_items).setVisible(true);
|
||||
menu.findItem(R.id.mark_all_read_item).setVisible(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -102,7 +101,12 @@ public class AllEpisodesFragment extends EpisodesListFragment {
|
|||
|
||||
@NonNull
|
||||
@Override
|
||||
protected List<FeedItem> loadMoreData() {
|
||||
protected List<FeedItem> loadMoreData(int page) {
|
||||
return DBReader.getRecentlyPublishedEpisodes((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE, feedItemFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int loadTotalItemCount() {
|
||||
return DBReader.getTotalEpisodeCount(feedItemFilter);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -120,8 +120,8 @@ public class CompletedDownloadsFragment extends Fragment
|
|||
}
|
||||
});
|
||||
speedDialView.setOnActionSelectedListener(actionItem -> {
|
||||
new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), adapter.getSelectedItems())
|
||||
.handleAction(actionItem.getId());
|
||||
new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItem.getId())
|
||||
.handleAction(adapter.getSelectedItems());
|
||||
adapter.endSelectMode();
|
||||
return true;
|
||||
});
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.os.Bundle;
|
|||
import android.view.ContextMenu;
|
||||
import android.view.KeyEvent;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator;
|
||||
|
@ -22,15 +23,20 @@ import android.widget.ProgressBar;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.leinardi.android.speeddial.SpeedDialView;
|
||||
import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
|
||||
import de.danoeh.antennapod.adapter.SelectableAdapter;
|
||||
import de.danoeh.antennapod.event.FeedListUpdateEvent;
|
||||
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
|
||||
import de.danoeh.antennapod.event.PlayerStatusEvent;
|
||||
import de.danoeh.antennapod.event.UnreadItemsUpdateEvent;
|
||||
import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
|
||||
import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler;
|
||||
import de.danoeh.antennapod.ui.common.PagedToolbarFragment;
|
||||
import de.danoeh.antennapod.view.EpisodeItemListRecyclerView;
|
||||
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
|
||||
import io.reactivex.Completable;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
@ -59,7 +65,7 @@ import io.reactivex.schedulers.Schedulers;
|
|||
/**
|
||||
* Shows unread or recently published episodes
|
||||
*/
|
||||
public abstract class EpisodesListFragment extends Fragment {
|
||||
public abstract class EpisodesListFragment extends Fragment implements EpisodeItemListAdapter.OnSelectModeListener {
|
||||
|
||||
public static final String TAG = "EpisodesListFragment";
|
||||
protected static final int EPISODES_PER_PAGE = 150;
|
||||
|
@ -72,6 +78,7 @@ public abstract class EpisodesListFragment extends Fragment {
|
|||
ProgressBar progLoading;
|
||||
View loadingMoreView;
|
||||
EmptyViewHandler emptyView;
|
||||
SpeedDialView speedDialView;
|
||||
|
||||
@NonNull
|
||||
List<FeedItem> episodes = new ArrayList<>();
|
||||
|
@ -130,21 +137,6 @@ public abstract class EpisodesListFragment extends Fragment {
|
|||
if (itemId == R.id.refresh_item) {
|
||||
AutoUpdateManager.runImmediate(requireContext());
|
||||
return true;
|
||||
} else if (itemId == R.id.mark_all_read_item) {
|
||||
ConfirmationDialog markAllReadConfirmationDialog = new ConfirmationDialog(getActivity(),
|
||||
R.string.mark_all_read_label,
|
||||
R.string.mark_all_read_confirmation_msg) {
|
||||
|
||||
@Override
|
||||
public void onConfirmButtonPressed(DialogInterface dialog) {
|
||||
dialog.dismiss();
|
||||
DBWriter.markAllItemsRead();
|
||||
((MainActivity) getActivity()).showSnackbarAbovePlayer(
|
||||
R.string.mark_all_read_msg, Toast.LENGTH_SHORT);
|
||||
}
|
||||
};
|
||||
markAllReadConfirmationDialog.createNewDialog().show();
|
||||
return true;
|
||||
} else if (itemId == R.id.remove_all_inbox_item) {
|
||||
ConfirmationDialog removeAllNewFlagsConfirmationDialog = new ConfirmationDialog(getActivity(),
|
||||
R.string.remove_all_inbox_label,
|
||||
|
@ -174,17 +166,13 @@ public abstract class EpisodesListFragment extends Fragment {
|
|||
// The method is called on all fragments in a ViewPager, so this needs to be ignored in invisible ones.
|
||||
// Apparently, none of the visibility check method works reliably on its own, so we just use all.
|
||||
return false;
|
||||
}
|
||||
if (item.getItemId() == R.id.share_item) {
|
||||
return true; // avoids that the position is reset when we need it in the submenu
|
||||
}
|
||||
|
||||
if (listAdapter.getLongPressedItem() == null) {
|
||||
} else if (listAdapter.getLongPressedItem() == null) {
|
||||
Log.i(TAG, "Selected item or listAdapter was null, ignoring selection");
|
||||
return super.onContextItemSelected(item);
|
||||
} else if (listAdapter.onContextItemSelected(item)) {
|
||||
return true;
|
||||
}
|
||||
FeedItem selectedItem = listAdapter.getLongPressedItem();
|
||||
|
||||
return FeedItemMenuHandler.onMenuItemClicked(this, item.getItemId(), selectedItem);
|
||||
}
|
||||
|
||||
|
@ -225,9 +213,72 @@ public abstract class EpisodesListFragment extends Fragment {
|
|||
createRecycleAdapter(recyclerView, emptyView);
|
||||
emptyView.hide();
|
||||
|
||||
speedDialView = root.findViewById(R.id.fabSD);
|
||||
speedDialView.setOverlayLayout(root.findViewById(R.id.fabSDOverlay));
|
||||
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 && listAdapter.getSelectedCount() == 0) {
|
||||
((MainActivity) getActivity()).showSnackbarAbovePlayer(R.string.no_items_selected,
|
||||
Snackbar.LENGTH_SHORT);
|
||||
speedDialView.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
speedDialView.setOnActionSelectedListener(actionItem -> {
|
||||
int confirmationString = 0;
|
||||
if (listAdapter.getSelectedItems().size() >= 25 || listAdapter.shouldSelectLazyLoadedItems()) {
|
||||
// Should ask for confirmation
|
||||
if (actionItem.getId() == R.id.mark_read_batch) {
|
||||
confirmationString = R.string.multi_select_mark_played_confirmation;
|
||||
} else if (actionItem.getId() == R.id.mark_unread_batch) {
|
||||
confirmationString = R.string.multi_select_mark_unplayed_confirmation;
|
||||
}
|
||||
}
|
||||
if (confirmationString == 0) {
|
||||
performMultiSelectAction(actionItem.getId());
|
||||
} else {
|
||||
new ConfirmationDialog(getActivity(), R.string.multi_select, confirmationString) {
|
||||
@Override
|
||||
public void onConfirmButtonPressed(DialogInterface dialog) {
|
||||
performMultiSelectAction(actionItem.getId());
|
||||
}
|
||||
}.createNewDialog().show();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private void performMultiSelectAction(int actionItemId) {
|
||||
EpisodeMultiSelectActionHandler handler =
|
||||
new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItemId);
|
||||
Completable.fromAction(
|
||||
() -> {
|
||||
handler.handleAction(listAdapter.getSelectedItems());
|
||||
if (listAdapter.shouldSelectLazyLoadedItems()) {
|
||||
int applyPage = page + 1;
|
||||
List<FeedItem> nextPage;
|
||||
do {
|
||||
nextPage = loadMoreData(applyPage);
|
||||
handler.handleAction(nextPage);
|
||||
applyPage++;
|
||||
} while (nextPage.size() == EPISODES_PER_PAGE);
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(() -> listAdapter.endSelectMode(),
|
||||
error -> Log.e(TAG, Log.getStackTraceString(error)));
|
||||
}
|
||||
|
||||
private void setupLoadMoreScrollListener() {
|
||||
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
|
@ -249,7 +300,7 @@ public abstract class EpisodesListFragment extends Fragment {
|
|||
}
|
||||
isLoadingMore = true;
|
||||
loadingMoreView.setVisibility(View.VISIBLE);
|
||||
disposable = Observable.fromCallable(this::loadMoreData)
|
||||
disposable = Observable.fromCallable(() -> loadMoreData(page))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(data -> {
|
||||
|
@ -258,6 +309,9 @@ public abstract class EpisodesListFragment extends Fragment {
|
|||
}
|
||||
episodes.addAll(data);
|
||||
onFragmentLoaded(episodes);
|
||||
if (listAdapter.shouldSelectLazyLoadedItems()) {
|
||||
listAdapter.setSelected(episodes.size() - data.size(), episodes.size(), true);
|
||||
}
|
||||
}, error -> Log.e(TAG, Log.getStackTraceString(error)),
|
||||
() -> {
|
||||
recyclerView.post(() -> isLoadingMore = false); // Make sure to not always load 2 pages at once
|
||||
|
@ -292,14 +346,38 @@ public abstract class EpisodesListFragment extends Fragment {
|
|||
@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);
|
||||
}
|
||||
MenuItemUtils.setOnClickListeners(menu, EpisodesListFragment.this::onContextItemSelected);
|
||||
}
|
||||
};
|
||||
listAdapter.setOnSelectModeListener(this);
|
||||
listAdapter.updateItems(episodes);
|
||||
recyclerView.setAdapter(listAdapter);
|
||||
emptyViewHandler.updateAdapter(listAdapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
if (listAdapter != null) {
|
||||
listAdapter.endSelectMode();
|
||||
}
|
||||
listAdapter = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartSelectMode() {
|
||||
speedDialView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEndSelectMode() {
|
||||
speedDialView.close();
|
||||
speedDialView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEventMainThread(FeedItemEvent event) {
|
||||
Log.d(TAG, "onEventMainThread() called with: " + "event = [" + event + "]");
|
||||
|
@ -395,14 +473,15 @@ public abstract class EpisodesListFragment extends Fragment {
|
|||
if (disposable != null) {
|
||||
disposable.dispose();
|
||||
}
|
||||
disposable = Observable.fromCallable(this::loadData)
|
||||
disposable = Observable.fromCallable(() -> new Pair<>(loadData(), loadTotalItemCount()))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(data -> {
|
||||
progLoading.setVisibility(View.GONE);
|
||||
loadingMoreView.setVisibility(View.GONE);
|
||||
hasMoreItems = true;
|
||||
episodes = data;
|
||||
episodes = data.first;
|
||||
listAdapter.setTotalNumberOfItems(data.second);
|
||||
onFragmentLoaded(episodes);
|
||||
if (getParentFragment() instanceof PagedToolbarFragment) {
|
||||
((PagedToolbarFragment) getParentFragment()).invalidateOptionsMenuIfActive(this);
|
||||
|
@ -422,5 +501,12 @@ public abstract class EpisodesListFragment extends Fragment {
|
|||
* @return The items from the next page of data
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract List<FeedItem> loadMoreData();
|
||||
protected abstract List<FeedItem> loadMoreData(int page);
|
||||
|
||||
/**
|
||||
* Returns the total number of items that would be returned if {@link #loadMoreData} was called often enough.
|
||||
*/
|
||||
protected int loadTotalItemCount() {
|
||||
return SelectableAdapter.COUNT_AUTOMATICALLY;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
|
||||
import de.danoeh.antennapod.activity.MainActivity;
|
||||
import de.danoeh.antennapod.model.feed.FeedItemFilter;
|
||||
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
|
||||
|
@ -47,7 +48,6 @@ public class FavoriteEpisodesFragment extends EpisodesListFragment {
|
|||
public void onPrepareOptionsMenu(@NonNull Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
menu.findItem(R.id.filter_items).setVisible(false);
|
||||
menu.findItem(R.id.mark_all_read_item).setVisible(false);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -98,7 +98,12 @@ public class FavoriteEpisodesFragment extends EpisodesListFragment {
|
|||
|
||||
@NonNull
|
||||
@Override
|
||||
protected List<FeedItem> loadMoreData() {
|
||||
protected List<FeedItem> loadMoreData(int page) {
|
||||
return DBReader.getFavoriteItemsList((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int loadTotalItemCount() {
|
||||
return DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.IS_FAVORITE));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -197,8 +197,8 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
|
|||
}
|
||||
});
|
||||
speedDialBinding.fabSD.setOnActionSelectedListener(actionItem -> {
|
||||
new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), adapter.getSelectedItems())
|
||||
.handleAction(actionItem.getId());
|
||||
new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItem.getId())
|
||||
.handleAction(adapter.getSelectedItems());
|
||||
adapter.endSelectMode();
|
||||
return true;
|
||||
});
|
||||
|
|
|
@ -67,6 +67,9 @@ public class InboxFragment extends EpisodesListFragment implements Toolbar.OnMen
|
|||
SwipeActions swipeActions = new SwipeActions(this, TAG).attachTo(recyclerView);
|
||||
swipeActions.setFilter(new FeedItemFilter(FeedItemFilter.NEW));
|
||||
|
||||
speedDialView.removeActionItemById(R.id.mark_unread_batch);
|
||||
speedDialView.removeActionItemById(R.id.remove_from_queue_batch);
|
||||
speedDialView.removeActionItemById(R.id.delete_batch);
|
||||
return inboxContainer;
|
||||
}
|
||||
|
||||
|
@ -113,7 +116,12 @@ public class InboxFragment extends EpisodesListFragment implements Toolbar.OnMen
|
|||
|
||||
@NonNull
|
||||
@Override
|
||||
protected List<FeedItem> loadMoreData() {
|
||||
protected List<FeedItem> loadMoreData(int page) {
|
||||
return DBReader.getNewItemsList((page - 1) * EPISODES_PER_PAGE, EPISODES_PER_PAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int loadTotalItemCount() {
|
||||
return DBReader.getTotalEpisodeCount(new FeedItemFilter(FeedItemFilter.NEW));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -505,8 +505,8 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
|
|||
}
|
||||
});
|
||||
speedDialView.setOnActionSelectedListener(actionItem -> {
|
||||
new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), recyclerAdapter.getSelectedItems())
|
||||
.handleAction(actionItem.getId());
|
||||
new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), actionItem.getId())
|
||||
.handleAction(recyclerAdapter.getSelectedItems());
|
||||
recyclerAdapter.endSelectMode();
|
||||
return true;
|
||||
});
|
||||
|
|
|
@ -21,35 +21,37 @@ 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;
|
||||
private final int actionId;
|
||||
private int totalNumItems = 0;
|
||||
private Snackbar snackbar = null;
|
||||
|
||||
public EpisodeMultiSelectActionHandler(MainActivity activity, List<FeedItem> selectedItems) {
|
||||
public EpisodeMultiSelectActionHandler(MainActivity activity, int actionId) {
|
||||
this.activity = activity;
|
||||
this.selectedItems = selectedItems;
|
||||
this.actionId = actionId;
|
||||
}
|
||||
|
||||
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();
|
||||
public void handleAction(List<FeedItem> items) {
|
||||
if (actionId == R.id.add_to_queue_batch) {
|
||||
queueChecked(items);
|
||||
} else if (actionId == R.id.remove_from_queue_batch) {
|
||||
removeFromQueueChecked(items);
|
||||
} else if (actionId == R.id.mark_read_batch) {
|
||||
markedCheckedPlayed(items);
|
||||
} else if (actionId == R.id.mark_unread_batch) {
|
||||
markedCheckedUnplayed(items);
|
||||
} else if (actionId == R.id.download_batch) {
|
||||
downloadChecked(items);
|
||||
} else if (actionId == R.id.delete_batch) {
|
||||
deleteChecked(items);
|
||||
} else {
|
||||
Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + id);
|
||||
Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + actionId);
|
||||
}
|
||||
}
|
||||
|
||||
private void queueChecked() {
|
||||
private void queueChecked(List<FeedItem> items) {
|
||||
// Check if an episode actually contains any media files before adding it to queue
|
||||
LongList toQueue = new LongList(selectedItems.size());
|
||||
for (FeedItem episode : selectedItems) {
|
||||
LongList toQueue = new LongList(items.size());
|
||||
for (FeedItem episode : items) {
|
||||
if (episode.hasMedia()) {
|
||||
toQueue.add(episode.getId());
|
||||
}
|
||||
|
@ -58,28 +60,28 @@ public class EpisodeMultiSelectActionHandler {
|
|||
showMessage(R.plurals.added_to_queue_batch_label, toQueue.size());
|
||||
}
|
||||
|
||||
private void removeFromQueueChecked() {
|
||||
long[] checkedIds = getSelectedIds();
|
||||
private void removeFromQueueChecked(List<FeedItem> items) {
|
||||
long[] checkedIds = getSelectedIds(items);
|
||||
DBWriter.removeQueueItem(activity, true, checkedIds);
|
||||
showMessage(R.plurals.removed_from_queue_batch_label, checkedIds.length);
|
||||
}
|
||||
|
||||
private void markedCheckedPlayed() {
|
||||
long[] checkedIds = getSelectedIds();
|
||||
private void markedCheckedPlayed(List<FeedItem> items) {
|
||||
long[] checkedIds = getSelectedIds(items);
|
||||
DBWriter.markItemPlayed(FeedItem.PLAYED, checkedIds);
|
||||
showMessage(R.plurals.marked_read_batch_label, checkedIds.length);
|
||||
}
|
||||
|
||||
private void markedCheckedUnplayed() {
|
||||
long[] checkedIds = getSelectedIds();
|
||||
private void markedCheckedUnplayed(List<FeedItem> items) {
|
||||
long[] checkedIds = getSelectedIds(items);
|
||||
DBWriter.markItemPlayed(FeedItem.UNPLAYED, checkedIds);
|
||||
showMessage(R.plurals.marked_unread_batch_label, checkedIds.length);
|
||||
}
|
||||
|
||||
private void downloadChecked() {
|
||||
private void downloadChecked(List<FeedItem> items) {
|
||||
// download the check episodes in the same order as they are currently displayed
|
||||
List<DownloadRequest> requests = new ArrayList<>();
|
||||
for (FeedItem episode : selectedItems) {
|
||||
for (FeedItem episode : items) {
|
||||
if (episode.hasMedia() && !episode.getFeed().isLocalFeed()) {
|
||||
requests.add(DownloadRequestCreator.create(episode.getMedia()).build());
|
||||
}
|
||||
|
@ -88,9 +90,9 @@ public class EpisodeMultiSelectActionHandler {
|
|||
showMessage(R.plurals.downloading_batch_label, requests.size());
|
||||
}
|
||||
|
||||
private void deleteChecked() {
|
||||
private void deleteChecked(List<FeedItem> items) {
|
||||
int countHasMedia = 0;
|
||||
for (FeedItem feedItem : selectedItems) {
|
||||
for (FeedItem feedItem : items) {
|
||||
if (feedItem.hasMedia() && feedItem.getMedia().isDownloaded()) {
|
||||
countHasMedia++;
|
||||
DBWriter.deleteFeedMediaOfItem(activity, feedItem.getMedia().getId());
|
||||
|
@ -100,14 +102,22 @@ public class EpisodeMultiSelectActionHandler {
|
|||
}
|
||||
|
||||
private void showMessage(@PluralsRes int msgId, int numItems) {
|
||||
activity.showSnackbarAbovePlayer(activity.getResources()
|
||||
.getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG);
|
||||
totalNumItems += numItems;
|
||||
activity.runOnUiThread(() -> {
|
||||
String text = activity.getResources().getQuantityString(msgId, totalNumItems, totalNumItems);
|
||||
if (snackbar != null) {
|
||||
snackbar.setText(text);
|
||||
snackbar.show(); // Resets the timeout
|
||||
} else {
|
||||
snackbar = activity.showSnackbarAbovePlayer(text, 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();
|
||||
private long[] getSelectedIds(List<FeedItem> items) {
|
||||
long[] checkedIds = new long[items.size()];
|
||||
for (int i = 0; i < items.size(); ++i) {
|
||||
checkedIds[i] = items.get(i).getId();
|
||||
}
|
||||
return checkedIds;
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package de.danoeh.antennapod.menuhandler;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import androidx.annotation.NonNull;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
|
||||
import de.danoeh.antennapod.core.storage.DBTasks;
|
||||
import de.danoeh.antennapod.core.storage.DBWriter;
|
||||
import de.danoeh.antennapod.core.util.IntentUtils;
|
||||
|
@ -56,19 +54,6 @@ public class FeedMenuHandler {
|
|||
DBTasks.forceRefreshCompleteFeed(context, selectedFeed);
|
||||
} else if (itemId == R.id.sort_items) {
|
||||
showSortDialog(context, selectedFeed);
|
||||
} else if (itemId == R.id.mark_all_read_item) {
|
||||
ConfirmationDialog conDialog = new ConfirmationDialog(context,
|
||||
R.string.mark_all_read_label,
|
||||
R.string.mark_all_read_feed_confirmation_msg) {
|
||||
|
||||
@Override
|
||||
public void onConfirmButtonPressed(
|
||||
DialogInterface dialog) {
|
||||
dialog.dismiss();
|
||||
DBWriter.markFeedRead(selectedFeed.getId());
|
||||
}
|
||||
};
|
||||
conDialog.createNewDialog().show();
|
||||
} else if (itemId == R.id.visit_website_item) {
|
||||
IntentUtils.openInBrowser(context, selectedFeed.getLink());
|
||||
} else if (itemId == R.id.share_item) {
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtvInformation"
|
||||
|
@ -23,17 +23,17 @@
|
|||
android:layout_below="@+id/txtvInformation"
|
||||
android:layout_above="@id/loadingMore">
|
||||
|
||||
<de.danoeh.antennapod.view.EpisodeItemListRecyclerView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:paddingTop="@dimen/list_vertical_padding"
|
||||
android:paddingBottom="@dimen/list_vertical_padding"
|
||||
android:paddingHorizontal="@dimen/additional_horizontal_spacing"
|
||||
tools:itemCount="13"
|
||||
tools:listitem="@layout/feeditemlist_item" />
|
||||
<de.danoeh.antennapod.view.EpisodeItemListRecyclerView
|
||||
android:id="@android:id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:paddingTop="@dimen/list_vertical_padding"
|
||||
android:paddingBottom="@dimen/list_vertical_padding"
|
||||
android:paddingHorizontal="@dimen/additional_horizontal_spacing"
|
||||
tools:itemCount="13"
|
||||
tools:listitem="@layout/feeditemlist_item" />
|
||||
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
|
@ -45,7 +45,7 @@
|
|||
android:indeterminateOnly="true"
|
||||
android:visibility="gone"
|
||||
android:layout_centerInParent="true"
|
||||
tools:background="@android:color/holo_red_light"/>
|
||||
tools:background="@android:color/holo_red_light" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/loadingMore"
|
||||
|
@ -76,4 +76,7 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
<include
|
||||
layout="@layout/multi_select_speed_dial" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
|
|
@ -24,12 +24,4 @@
|
|||
android:visible="false"
|
||||
custom:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/mark_all_read_item"
|
||||
android:title="@string/mark_all_read_label"
|
||||
android:menuCategory="container"
|
||||
custom:showAsAction="collapseActionView"
|
||||
android:visible="false"
|
||||
android:icon="@drawable/ic_check"/>
|
||||
|
||||
</menu>
|
||||
|
|
|
@ -402,6 +402,19 @@ public final class DBReader {
|
|||
}
|
||||
}
|
||||
|
||||
public static int getTotalEpisodeCount(FeedItemFilter filter) {
|
||||
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||
adapter.open();
|
||||
try (Cursor cursor = adapter.getTotalEpisodeCountCursor(filter)) {
|
||||
if (cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
}
|
||||
return -1;
|
||||
} finally {
|
||||
adapter.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the playback history from the database. A FeedItem is in the playback history if playback of the correpsonding episode
|
||||
* has been completed at least once.
|
||||
|
|
|
@ -731,36 +731,6 @@ public class DBWriter {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 'read'-attribute of all FeedItems of a specific Feed to PLAYED.
|
||||
*
|
||||
* @param feedId ID of the Feed.
|
||||
*/
|
||||
public static Future<?> markFeedRead(final long feedId) {
|
||||
return dbExec.submit(() -> {
|
||||
final PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||
adapter.open();
|
||||
adapter.setFeedItems(FeedItem.PLAYED, feedId);
|
||||
adapter.close();
|
||||
|
||||
EventBus.getDefault().post(new UnreadItemsUpdateEvent());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 'read'-attribute of all FeedItems to PLAYED.
|
||||
*/
|
||||
public static Future<?> markAllItemsRead() {
|
||||
return dbExec.submit(() -> {
|
||||
final PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||
adapter.open();
|
||||
adapter.setFeedItems(FeedItem.PLAYED);
|
||||
adapter.close();
|
||||
|
||||
EventBus.getDefault().post(new UnreadItemsUpdateEvent());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the 'read'-attribute of all NEW FeedItems to UNPLAYED.
|
||||
*/
|
||||
|
|
|
@ -751,34 +751,6 @@ public class DbWriterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMarkFeedRead() throws Exception {
|
||||
final int numItems = 10;
|
||||
Feed feed = new Feed("url", null, "title");
|
||||
feed.setItems(new ArrayList<>());
|
||||
for (int i = 0; i < numItems; i++) {
|
||||
FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i,
|
||||
new Date(), FeedItem.UNPLAYED, feed);
|
||||
feed.getItems().add(item);
|
||||
}
|
||||
|
||||
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||
adapter.open();
|
||||
adapter.setCompleteFeed(feed);
|
||||
adapter.close();
|
||||
|
||||
assertTrue(feed.getId() != 0);
|
||||
for (FeedItem item : feed.getItems()) {
|
||||
assertTrue(item.getId() != 0);
|
||||
}
|
||||
|
||||
DBWriter.markFeedRead(feed.getId()).get(TIMEOUT, TimeUnit.SECONDS);
|
||||
List<FeedItem> loadedItems = DBReader.getFeedItemList(feed);
|
||||
for (FeedItem item : loadedItems) {
|
||||
assertTrue(item.isPlayed());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveAllNewFlags() throws Exception {
|
||||
final int numItems = 10;
|
||||
|
@ -807,34 +779,6 @@ public class DbWriterTest {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMarkAllItemsReadSameFeed() throws Exception {
|
||||
final int numItems = 10;
|
||||
Feed feed = new Feed("url", null, "title");
|
||||
feed.setItems(new ArrayList<>());
|
||||
for (int i = 0; i < numItems; i++) {
|
||||
FeedItem item = new FeedItem(0, "title " + i, "id " + i, "link " + i,
|
||||
new Date(), FeedItem.UNPLAYED, feed);
|
||||
feed.getItems().add(item);
|
||||
}
|
||||
|
||||
PodDBAdapter adapter = PodDBAdapter.getInstance();
|
||||
adapter.open();
|
||||
adapter.setCompleteFeed(feed);
|
||||
adapter.close();
|
||||
|
||||
assertTrue(feed.getId() != 0);
|
||||
for (FeedItem item : feed.getItems()) {
|
||||
assertTrue(item.getId() != 0);
|
||||
}
|
||||
|
||||
DBWriter.markAllItemsRead().get(TIMEOUT, TimeUnit.SECONDS);
|
||||
List<FeedItem> loadedItems = DBReader.getFeedItemList(feed);
|
||||
for (FeedItem item : loadedItems) {
|
||||
assertTrue(item.isPlayed());
|
||||
}
|
||||
}
|
||||
|
||||
private static Feed createTestFeed(int numItems) {
|
||||
Feed feed = new Feed("url", null, "title");
|
||||
feed.setItems(new ArrayList<>());
|
||||
|
|
|
@ -1052,6 +1052,14 @@ public class PodDBAdapter {
|
|||
return db.rawQuery(query, null);
|
||||
}
|
||||
|
||||
public final Cursor getTotalEpisodeCountCursor(FeedItemFilter filter) {
|
||||
String filterQuery = FeedItemFilterQuery.generateFrom(filter);
|
||||
String whereClause = "".equals(filterQuery) ? "" : " WHERE " + filterQuery;
|
||||
final String query = "SELECT count(" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + ") FROM " + TABLE_NAME_FEED_ITEMS
|
||||
+ JOIN_FEED_ITEM_AND_MEDIA + whereClause;
|
||||
return db.rawQuery(query, null);
|
||||
}
|
||||
|
||||
public Cursor getDownloadedItemsCursor() {
|
||||
final String query = SELECT_FEED_ITEMS_AND_MEDIA
|
||||
+ "WHERE " + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " > 0";
|
||||
|
|
|
@ -34,6 +34,8 @@ public class FeedItemFilterQuery {
|
|||
statements.add(keyRead + " = 1 ");
|
||||
} else if (filter.showUnplayed) {
|
||||
statements.add(" NOT " + keyRead + " = 1 "); // Match "New" items (read = -1) as well
|
||||
} else if (filter.showNew) {
|
||||
statements.add(keyRead + " = -1 ");
|
||||
}
|
||||
if (filter.showPaused) {
|
||||
statements.add(" (" + keyPosition + " NOT NULL AND " + keyPosition + " > 0 " + ") ");
|
||||
|
|
|
@ -152,13 +152,11 @@
|
|||
<string name="new_episode_notification_group_text">Your subscriptions have new episodes.</string>
|
||||
|
||||
<!-- Actions on feeds -->
|
||||
<string name="mark_all_read_label">Mark all as played</string>
|
||||
<string name="mark_all_read_msg">Marked all Episodes as played</string>
|
||||
<string name="mark_all_read_confirmation_msg">Please confirm that you want to mark all episodes as being played.</string>
|
||||
<string name="mark_all_read_feed_confirmation_msg">Please confirm that you want to mark all episodes in this podcast as being played.</string>
|
||||
<string name="remove_all_inbox_label">Remove all from inbox</string>
|
||||
<string name="removed_all_inbox_msg">Removed all from inbox</string>
|
||||
<string name="remove_all_inbox_confirmation_msg">Please confirm that you want to remove all from the inbox.</string>
|
||||
<string name="multi_select_mark_played_confirmation">Please confirm that you want to mark all selected items as played.</string>
|
||||
<string name="multi_select_mark_unplayed_confirmation">Please confirm that you want to mark all selected items as unplayed.</string>
|
||||
<string name="show_info_label">Show information</string>
|
||||
<string name="show_feed_settings_label">Show podcast settings</string>
|
||||
<string name="feed_settings_label">Podcast settings</string>
|
||||
|
|
Loading…
Reference in New Issue