Merge pull request #5872 from ByteHamster/multi-select-all-episodes

Multi-Select on 'All episodes' screen
This commit is contained in:
ByteHamster 2022-05-08 21:47:26 +02:00 committed by GitHub
commit 6e5004be22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 264 additions and 208 deletions

View File

@ -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;
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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;
});

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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;
});

View File

@ -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));
}
}

View File

@ -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;
});

View File

@ -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;
}

View File

@ -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) {

View File

@ -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>

View File

@ -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>

View File

@ -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.

View File

@ -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.
*/

View File

@ -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<>());

View File

@ -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";

View File

@ -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 " + ") ");

View File

@ -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>