Feed multi select (#5261)

This commit is contained in:
seeto 2021-08-23 14:46:38 -07:00 committed by GitHub
parent 91967409cb
commit eacc90af29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 769 additions and 293 deletions

View File

@ -76,21 +76,19 @@ public class EpisodeItemListAdapter extends SelectableAdapter<EpisodeItemViewHol
int position = ArrayUtils.indexOf(ids, item.getId());
activity.loadChildFragment(ItemPagerFragment.newInstance(ids, position));
} else {
toggleSelection(pos);
toggleSelection(holder.getBindingAdapterPosition());
}
});
holder.itemView.setOnCreateContextMenuListener(this);
holder.itemView.setOnLongClickListener(v -> {
longPressedItem = item;
longPressedPosition = pos;
longPressedItem = getItem(holder.getBindingAdapterPosition());
longPressedPosition = holder.getBindingAdapterPosition();
return false;
});
if (inActionMode()) {
holder.secondaryActionButton.setVisibility(View.GONE);
holder.selectCheckBox.setOnClickListener(v -> {
toggleSelection(pos);
});
holder.selectCheckBox.setOnClickListener(v -> toggleSelection(holder.getBindingAdapterPosition()));
holder.selectCheckBox.setChecked(isSelected(pos));
holder.selectCheckBox.setVisibility(View.VISIBLE);
} else {

View File

@ -1,152 +0,0 @@
package de.danoeh.antennapod.adapter;
import android.content.Context;
import androidx.core.text.TextUtilsCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
import java.lang.ref.WeakReference;
import java.text.NumberFormat;
import java.util.Locale;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.core.feed.LocalFeedUpdater;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
import jp.shts.android.library.TriangleLabelView;
/**
* Adapter for subscriptions
*/
public class SubscriptionsAdapter extends BaseAdapter implements AdapterView.OnItemClickListener {
/** the position in the view that holds the add item; 0 is the first, -1 is the last position */
private static final String TAG = "SubscriptionsAdapter";
private final WeakReference<MainActivity> mainActivityRef;
private final ItemAccess itemAccess;
public SubscriptionsAdapter(MainActivity mainActivity, ItemAccess itemAccess) {
this.mainActivityRef = new WeakReference<>(mainActivity);
this.itemAccess = itemAccess;
}
@Override
public int getCount() {
return itemAccess.getCount();
}
@Override
public Object getItem(int position) {
return itemAccess.getItem(position);
}
@Override
public boolean hasStableIds() {
return true;
}
@Override
public long getItemId(int position) {
return ((NavDrawerData.DrawerItem) getItem(position)).id;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Holder holder;
if (convertView == null) {
holder = new Holder();
LayoutInflater layoutInflater =
(LayoutInflater) mainActivityRef.get().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
convertView = layoutInflater.inflate(R.layout.subscription_item, parent, false);
holder.feedTitle = convertView.findViewById(R.id.txtvTitle);
holder.imageView = convertView.findViewById(R.id.imgvCover);
holder.count = convertView.findViewById(R.id.triangleCountView);
convertView.setTag(holder);
} else {
holder = (Holder) convertView.getTag();
}
final NavDrawerData.DrawerItem drawerItem = (NavDrawerData.DrawerItem) getItem(position);
if (drawerItem == null) {
return null;
}
holder.feedTitle.setText(drawerItem.getTitle());
holder.imageView.setContentDescription(drawerItem.getTitle());
holder.feedTitle.setVisibility(View.VISIBLE);
// Fix TriangleLabelView corner for RTL
if (TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault())
== ViewCompat.LAYOUT_DIRECTION_RTL) {
holder.count.setCorner(TriangleLabelView.Corner.TOP_LEFT);
}
if (drawerItem.getCounter() > 0) {
holder.count.setPrimaryText(NumberFormat.getInstance().format(drawerItem.getCounter()));
holder.count.setVisibility(View.VISIBLE);
} else {
holder.count.setVisibility(View.GONE);
}
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
boolean textAndImageCombined = feed.isLocalFeed()
&& LocalFeedUpdater.getDefaultIconUrl(convertView.getContext()).equals(feed.getImageUrl());
new CoverLoader(mainActivityRef.get())
.withUri(feed.getImageUrl())
.withPlaceholderView(holder.feedTitle, textAndImageCombined)
.withCoverView(holder.imageView)
.load();
} else {
new CoverLoader(mainActivityRef.get())
.withResource(R.drawable.ic_folder)
.withPlaceholderView(holder.feedTitle, true)
.withCoverView(holder.imageView)
.load();
}
return convertView;
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final NavDrawerData.DrawerItem drawerItem = (NavDrawerData.DrawerItem) getItem(position);
if (drawerItem == null) {
return;
}
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
Fragment fragment = FeedItemlistFragment.newInstance(feed.getId());
mainActivityRef.get().loadChildFragment(fragment);
} else if (drawerItem.type == NavDrawerData.DrawerItem.Type.FOLDER) {
Fragment fragment = SubscriptionFragment.newInstance(drawerItem.getTitle());
mainActivityRef.get().loadChildFragment(fragment);
}
}
static class Holder {
public TextView feedTitle;
public ImageView imageView;
public TriangleLabelView count;
}
public interface ItemAccess {
int getCount();
NavDrawerData.DrawerItem getItem(int position);
}
}

View File

@ -0,0 +1,245 @@
package de.danoeh.antennapod.adapter;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.text.TextUtilsCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.RecyclerView;
import java.lang.ref.WeakReference;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.feed.LocalFeedUpdater;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.fragment.SubscriptionFragment;
import de.danoeh.antennapod.model.feed.Feed;
import jp.shts.android.library.TriangleLabelView;
/**
* Adapter for subscriptions
*/
public class SubscriptionsRecyclerAdapter extends SelectableAdapter<SubscriptionsRecyclerAdapter.SubscriptionViewHolder>
implements View.OnCreateContextMenuListener {
private final WeakReference<MainActivity> mainActivityRef;
private List<NavDrawerData.DrawerItem> listItems;
private Feed selectedFeed = null;
int longPressedPosition = 0; // used to init actionMode
public SubscriptionsRecyclerAdapter(MainActivity mainActivity) {
super(mainActivity);
this.mainActivityRef = new WeakReference<>(mainActivity);
this.listItems = new ArrayList<>();
setHasStableIds(true);
}
public Object getItem(int position) {
return listItems.get(position);
}
public Feed getSelectedFeed() {
return selectedFeed;
}
@NonNull
@Override
public SubscriptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = LayoutInflater.from(mainActivityRef.get()).inflate(R.layout.subscription_item, parent, false);
return new SubscriptionViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull SubscriptionViewHolder holder, int position) {
NavDrawerData.DrawerItem drawerItem = listItems.get(position);
boolean isFeed = drawerItem.type == NavDrawerData.DrawerItem.Type.FEED;
holder.bind(drawerItem);
holder.itemView.setOnCreateContextMenuListener(this);
if (inActionMode()) {
if (isFeed) {
holder.selectCheckbox.setVisibility(View.VISIBLE);
holder.selectView.setVisibility(View.VISIBLE);
}
holder.selectCheckbox.setChecked((isSelected(position)));
holder.selectCheckbox.setOnCheckedChangeListener((buttonView, isChecked)
-> setSelected(holder.getBindingAdapterPosition(), isChecked));
holder.imageView.setAlpha(0.6f);
holder.count.setVisibility(View.GONE);
} else {
holder.selectView.setVisibility(View.GONE);
holder.imageView.setAlpha(1.0f);
}
holder.itemView.setOnLongClickListener(v -> {
if (!inActionMode()) {
if (isFeed) {
selectedFeed = ((NavDrawerData.FeedDrawerItem) getItem(holder.getBindingAdapterPosition())).feed;
longPressedPosition = holder.getBindingAdapterPosition();
} else {
selectedFeed = null;
}
}
return false;
});
holder.itemView.setOnClickListener(v -> {
if (isFeed) {
if (inActionMode()) {
holder.selectCheckbox.setChecked(!isSelected(holder.getBindingAdapterPosition()));
} else {
Fragment fragment = FeedItemlistFragment
.newInstance(((NavDrawerData.FeedDrawerItem) drawerItem).feed.getId());
mainActivityRef.get().loadChildFragment(fragment);
}
} else if (!inActionMode()) {
Fragment fragment = SubscriptionFragment.newInstance(drawerItem.getTitle());
mainActivityRef.get().loadChildFragment(fragment);
}
});
}
@Override
public int getItemCount() {
return listItems.size();
}
@Override
public long getItemId(int position) {
return listItems.get(position).id;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
if (selectedFeed != null && !inActionMode()) {
MenuInflater inflater = mainActivityRef.get().getMenuInflater();
inflater.inflate(R.menu.nav_feed_context, menu);
menu.setHeaderTitle(selectedFeed.getTitle());
}
}
public boolean onContextItemSelected(MenuItem item) {
if (item.getItemId() == R.id.multi_select) {
startSelectMode(longPressedPosition);
return true;
}
return false;
}
public List<Feed> getSelectedItems() {
List<Feed> items = new ArrayList<>();
for (int i = 0; i < getItemCount(); i++) {
if (isSelected(i)) {
NavDrawerData.DrawerItem drawerItem = listItems.get(i);
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
items.add(feed);
}
}
}
return items;
}
public void setItems(List<NavDrawerData.DrawerItem> listItems) {
this.listItems = listItems;
}
@Override
public void setSelected(int pos, boolean selected) {
NavDrawerData.DrawerItem drawerItem = listItems.get(pos);
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
super.setSelected(pos, selected);
}
}
public class SubscriptionViewHolder extends RecyclerView.ViewHolder {
private final TextView feedTitle;
private final ImageView imageView;
private final TriangleLabelView count;
private final FrameLayout selectView;
private final CheckBox selectCheckbox;
public SubscriptionViewHolder(@NonNull View itemView) {
super(itemView);
feedTitle = itemView.findViewById(R.id.txtvTitle);
imageView = itemView.findViewById(R.id.imgvCover);
count = itemView.findViewById(R.id.triangleCountView);
selectView = itemView.findViewById(R.id.selectView);
selectCheckbox = itemView.findViewById(R.id.selectCheckBox);
}
public void bind(NavDrawerData.DrawerItem drawerItem) {
feedTitle.setText(drawerItem.getTitle());
imageView.setContentDescription(drawerItem.getTitle());
feedTitle.setVisibility(View.VISIBLE);
if (TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault())
== ViewCompat.LAYOUT_DIRECTION_RTL) {
count.setCorner(TriangleLabelView.Corner.TOP_LEFT);
}
if (drawerItem.getCounter() > 0) {
count.setPrimaryText(NumberFormat.getInstance().format(drawerItem.getCounter()));
count.setVisibility(View.VISIBLE);
} else {
count.setVisibility(View.GONE);
}
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
Feed feed = ((NavDrawerData.FeedDrawerItem) drawerItem).feed;
boolean textAndImageCombind = feed.isLocalFeed()
&& LocalFeedUpdater.getDefaultIconUrl(itemView.getContext()).equals(feed.getImageUrl());
new CoverLoader(mainActivityRef.get())
.withUri(feed.getImageUrl())
.withPlaceholderView(feedTitle, textAndImageCombind)
.withCoverView(imageView)
.load();
} else {
new CoverLoader(mainActivityRef.get())
.withResource(R.drawable.ic_folder)
.withPlaceholderView(feedTitle, true)
.withCoverView(imageView)
.load();
}
}
}
public static float convertDpToPixel(Context context, float dp) {
return dp * context.getResources().getDisplayMetrics().density;
}
public static class GridDividerItemDecorator extends RecyclerView.ItemDecoration {
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) {
super.onDraw(c, parent, state);
}
@Override
public void getItemOffsets(@NonNull Rect outRect,
@NonNull View view,
@NonNull RecyclerView parent,
@NonNull RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
Context context = parent.getContext();
int insetOffset = (int) convertDpToPixel(context, 1f);
outRect.set(insetOffset, insetOffset, insetOffset, insetOffset);
}
}
}

View File

@ -4,6 +4,10 @@ import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.util.Log;
import java.util.Collections;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.model.feed.Feed;
@ -16,10 +20,17 @@ public class RemoveFeedDialog {
private static final String TAG = "RemoveFeedDialog";
public static void show(Context context, Feed feed, Runnable onSuccess) {
int messageId = feed.isLocalFeed() ? R.string.feed_delete_confirmation_local_msg
: R.string.feed_delete_confirmation_msg;
String message = context.getString(messageId, feed.getTitle());
List<Feed> feeds = Collections.singletonList(feed);
String message = getMessageId(context, feeds);
showDialog(context, feeds, message, onSuccess);
}
public static void show(Context context, List<Feed> feeds, Runnable onSuccess) {
String message = getMessageId(context, feeds);
showDialog(context, feeds, message, onSuccess);
}
private static void showDialog(Context context, List<Feed> feeds, String message, Runnable onSuccess) {
ConfirmationDialog dialog = new ConfirmationDialog(context, R.string.remove_feed_label, message) {
@Override
public void onConfirmButtonPressed(DialogInterface clickedDialog) {
@ -31,12 +42,17 @@ public class RemoveFeedDialog {
progressDialog.setCancelable(false);
progressDialog.show();
Completable.fromCallable(() -> DBWriter.deleteFeed(context, feed.getId()).get())
Completable.fromCallable(() -> {
for (Feed feed : feeds) {
DBWriter.deleteFeed(context, feed.getId()).get();
}
return null;
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
() -> {
Log.d(TAG, "Feed was deleted");
Log.d(TAG, "Feed(s) deleted");
if (onSuccess != null) {
onSuccess.run();
}
@ -49,4 +65,17 @@ public class RemoveFeedDialog {
};
dialog.createNewDialog().show();
}
private static String getMessageId(Context context, List<Feed> feeds) {
if (feeds.size() == 1) {
if (feeds.get(0).isLocalFeed()) {
return context.getString(R.string.feed_delete_confirmation_local_msg);
} else {
return context.getString(R.string.feed_delete_confirmation_msg, feeds.get(0).getTitle());
}
} else {
return context.getString(R.string.feed_delete_confirmation_msg_batch);
}
}
}

View File

@ -7,41 +7,44 @@ import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import android.util.Log;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.TextView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.joanzapata.iconify.Iconify;
import com.leinardi.android.speeddial.SpeedDialView;
import de.danoeh.antennapod.dialog.TagSettingsDialog;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.Callable;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.adapter.SubscriptionsAdapter;
import de.danoeh.antennapod.adapter.SubscriptionsRecyclerAdapter;
import de.danoeh.antennapod.core.dialog.ConfirmationDialog;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.core.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.dialog.TagSettingsDialog;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.download.DownloadService;
import de.danoeh.antennapod.core.storage.DBReader;
@ -49,24 +52,24 @@ import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.dialog.RemoveFeedDialog;
import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog;
import de.danoeh.antennapod.dialog.FeedSortDialog;
import de.danoeh.antennapod.dialog.RemoveFeedDialog;
import de.danoeh.antennapod.dialog.RenameFeedDialog;
import de.danoeh.antennapod.dialog.SubscriptionsFilterDialog;
import de.danoeh.antennapod.fragment.actions.FeedMultiSelectActionHandler;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.view.EmptyViewHandler;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
/**
* Fragment for displaying feed subscriptions
*/
public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItemClickListener {
public class SubscriptionFragment extends Fragment
implements Toolbar.OnMenuItemClickListener,
SubscriptionsRecyclerAdapter.OnSelectModeListener {
public static final String TAG = "SubscriptionFragment";
private static final String PREFS = "SubscriptionFragment";
private static final String PREF_NUM_COLUMNS = "columns";
@ -80,23 +83,24 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
R.id.subscription_num_columns_4,
R.id.subscription_num_columns_5};
private GridView subscriptionGridLayout;
private List<NavDrawerData.DrawerItem> listItems;
private SubscriptionsAdapter subscriptionAdapter;
private RecyclerView subscriptionRecycler;
private SubscriptionsRecyclerAdapter subscriptionAdapter;
private FloatingActionButton subscriptionAddButton;
private ProgressBar progressBar;
private EmptyViewHandler emptyView;
private TextView feedsFilteredMsg;
private Toolbar toolbar;
private String displayedFolder = null;
private Feed selectedFeed = null;
private boolean isUpdatingFeeds = false;
private boolean displayUpArrow;
private Disposable disposable;
private SharedPreferences prefs;
private SpeedDialView speedDialView;
private List<NavDrawerData.DrawerItem> listItems;
public static SubscriptionFragment newInstance(String folderTitle) {
SubscriptionFragment fragment = new SubscriptionFragment();
Bundle args = new Bundle();
@ -139,9 +143,15 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
}
}
subscriptionGridLayout = root.findViewById(R.id.subscriptions_grid);
subscriptionGridLayout.setNumColumns(prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns()));
registerForContextMenu(subscriptionGridLayout);
subscriptionRecycler = root.findViewById(R.id.subscriptions_grid);
GridLayoutManager gridLayoutManager = new GridLayoutManager(getContext(),
prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns()),
RecyclerView.VERTICAL,
false);
subscriptionRecycler.setLayoutManager(gridLayoutManager);
subscriptionRecycler.addItemDecoration(new SubscriptionsRecyclerAdapter.GridDividerItemDecorator());
gridLayoutManager.setSpanCount(prefs.getInt(PREF_NUM_COLUMNS, getDefaultNumOfColumns()));
registerForContextMenu(subscriptionRecycler);
subscriptionAddButton = root.findViewById(R.id.subscriptions_add);
progressBar = root.findViewById(R.id.progLoading);
@ -155,6 +165,25 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
new Handler(Looper.getMainLooper()).postDelayed(() -> swipeRefreshLayout.setRefreshing(false),
getResources().getInteger(R.integer.swipe_to_refresh_duration_in_ms));
});
speedDialView = root.findViewById(R.id.fabSD);
speedDialView.inflate(R.menu.nav_feed_action_speeddial);
speedDialView.setOnChangeListener(new SpeedDialView.OnChangeListener() {
@Override
public boolean onMainActionSelected() {
return false;
}
@Override
public void onToggleChanged(boolean isOpen) {
}
});
speedDialView.setOnActionSelectedListener(actionItem -> {
new FeedMultiSelectActionHandler((MainActivity) getActivity(), subscriptionAdapter.getSelectedItems())
.handleAction(actionItem.getId());
return true;
});
return root;
}
@ -204,7 +233,9 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
}
private void setColumnNumber(int columns) {
subscriptionGridLayout.setNumColumns(columns);
GridLayoutManager gridLayoutManager = (GridLayoutManager) subscriptionRecycler.getLayoutManager();
gridLayoutManager.setSpanCount(columns);
subscriptionAdapter.notifyDataSetChanged();
prefs.edit().putInt(PREF_NUM_COLUMNS, columns).apply();
refreshToolbarState();
}
@ -214,18 +245,16 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
emptyView.setIcon(R.drawable.ic_folder);
emptyView.setTitle(R.string.no_subscriptions_head_label);
emptyView.setMessage(R.string.no_subscriptions_label);
emptyView.attachToListView(subscriptionGridLayout);
emptyView.attachToRecyclerView(subscriptionRecycler);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
subscriptionAdapter = new SubscriptionsAdapter((MainActivity) getActivity(), itemAccess);
subscriptionGridLayout.setAdapter(subscriptionAdapter);
subscriptionGridLayout.setOnItemClickListener(subscriptionAdapter);
subscriptionAdapter = new SubscriptionsRecyclerAdapter((MainActivity) getActivity());
subscriptionAdapter.setOnSelectModeListener(this);
subscriptionRecycler.setAdapter(subscriptionAdapter);
setupEmptyView();
subscriptionAddButton.setOnClickListener(view -> {
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).loadChildFragment(new AddFeedFragment());
@ -247,6 +276,10 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
if (disposable != null) {
disposable.dispose();
}
if (subscriptionAdapter != null) {
subscriptionAdapter.endSelectMode();
}
}
private void loadSubscriptions() {
@ -271,6 +304,7 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
.subscribe(
result -> {
listItems = result;
subscriptionAdapter.setItems(result);
subscriptionAdapter.notifyDataSetChanged();
emptyView.updateVisibility();
progressBar.setVisibility(View.GONE); // Keep hidden to avoid flickering while refreshing
@ -292,34 +326,13 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
return getResources().getInteger(R.integer.subscriptions_default_num_of_columns);
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
if (menuInfo == null) {
return;
}
AdapterView.AdapterContextMenuInfo adapterInfo = (AdapterView.AdapterContextMenuInfo) menuInfo;
int position = adapterInfo.position;
NavDrawerData.DrawerItem selectedObject = (NavDrawerData.DrawerItem) subscriptionAdapter.getItem(position);
if (selectedObject.type == NavDrawerData.DrawerItem.Type.FEED) {
MenuInflater inflater = requireActivity().getMenuInflater();
inflater.inflate(R.menu.nav_feed_context, menu);
selectedFeed = ((NavDrawerData.FeedDrawerItem) selectedObject).feed;
}
menu.setHeaderTitle(selectedObject.getTitle());
}
@Override
public boolean onContextItemSelected(MenuItem item) {
if (selectedFeed == null) {
Feed feed = subscriptionAdapter.getSelectedFeed();
if (feed == null) {
return false;
}
Feed feed = selectedFeed;
selectedFeed = null;
final int itemId = item.getItemId();
int itemId = item.getItemId();
if (itemId == R.id.remove_all_new_flags_item) {
displayConfirmationDialog(
R.string.remove_all_new_flags_label,
@ -335,6 +348,9 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
} else if (itemId == R.id.remove_item) {
RemoveFeedDialog.show(getContext(), feed, null);
return true;
} else if (itemId == R.id.multi_select) {
speedDialView.setVisibility(View.VISIBLE);
return subscriptionAdapter.onContextItemSelected(item);
}
return super.onContextItemSelected(item);
}
@ -376,23 +392,23 @@ public class SubscriptionFragment extends Fragment implements Toolbar.OnMenuItem
private final MenuItemUtils.UpdateRefreshMenuItemChecker updateRefreshMenuItemChecker =
() -> DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFeeds();
private final SubscriptionsAdapter.ItemAccess itemAccess = new SubscriptionsAdapter.ItemAccess() {
@Override
public int getCount() {
if (listItems != null) {
return listItems.size();
} else {
return 0;
}
}
@Override
public void onEndSelectMode() {
speedDialView.close();
speedDialView.setVisibility(View.GONE);
subscriptionAdapter.setItems(listItems);
subscriptionAdapter.notifyDataSetChanged();
}
@Override
public NavDrawerData.DrawerItem getItem(int position) {
if (listItems != null && 0 <= position && position < listItems.size()) {
return listItems.get(position);
} else {
return null;
@Override
public void onStartSelectMode() {
List<NavDrawerData.DrawerItem> feedsOnly = new ArrayList<>();
for (NavDrawerData.DrawerItem item : listItems) {
if (item.type == NavDrawerData.DrawerItem.Type.FEED) {
feedsOnly.add(item);
}
}
};
subscriptionAdapter.setItems(feedsOnly);
subscriptionAdapter.notifyDataSetChanged();
}
}

View File

@ -0,0 +1,139 @@
package de.danoeh.antennapod.fragment.actions;
import android.util.Log;
import androidx.annotation.PluralsRes;
import androidx.core.util.Consumer;
import com.google.android.material.snackbar.Snackbar;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.List;
import java.util.Locale;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.dialog.RemoveFeedDialog;
import de.danoeh.antennapod.fragment.preferences.dialog.PreferenceListDialog;
import de.danoeh.antennapod.fragment.preferences.dialog.PreferenceSwitchDialog;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedPreferences;
public class FeedMultiSelectActionHandler {
private static final String TAG = "FeedSelectHandler";
private final MainActivity activity;
private final List<Feed> selectedItems;
public FeedMultiSelectActionHandler(MainActivity activity, List<Feed> selectedItems) {
this.activity = activity;
this.selectedItems = selectedItems;
}
public void handleAction(int id) {
if (id == R.id.remove_item) {
RemoveFeedDialog.show(activity, selectedItems, null);
} else if (id == R.id.keep_updated) {
keepUpdatedPrefHandler();
} else if (id == R.id.autodownload) {
autoDownloadPrefHandler();
} else if (id == R.id.autoDeleteDownload) {
autoDeleteEpisodesPrefHandler();
} else if (id == R.id.playback_speed) {
playbackSpeedPrefHandler();
} else {
Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=" + id);
}
}
private void autoDownloadPrefHandler() {
PreferenceSwitchDialog preferenceSwitchDialog = new PreferenceSwitchDialog(activity,
activity.getString(R.string.auto_download_settings_label),
activity.getString(R.string.auto_download_label));
preferenceSwitchDialog.setOnPreferenceChangedListener(new PreferenceSwitchDialog.OnPreferenceChangedListener() {
@Override
public void preferenceChanged(boolean enabled) {
saveFeedPreferences(feedPreferences -> feedPreferences.setAutoDownload(enabled));
}
});
preferenceSwitchDialog.openDialog();
}
private static final DecimalFormat SPEED_FORMAT =
new DecimalFormat("0.00", DecimalFormatSymbols.getInstance(Locale.US));
private void playbackSpeedPrefHandler() {
final String[] speeds = activity.getResources().getStringArray(R.array.playback_speed_values);
String[] values = new String[speeds.length + 1];
values[0] = SPEED_FORMAT.format(FeedPreferences.SPEED_USE_GLOBAL);
String[] entries = new String[speeds.length + 1];
entries[0] = activity.getString(R.string.feed_auto_download_global);
System.arraycopy(speeds, 0, values, 1, speeds.length);
System.arraycopy(speeds, 0, entries, 1, speeds.length);
PreferenceListDialog preferenceListDialog = new PreferenceListDialog(activity,
activity.getString(R.string.playback_speed));
preferenceListDialog.openDialog(entries);
preferenceListDialog.setOnPreferenceChangedListener(pos -> {
saveFeedPreferences(feedPreferences -> {
feedPreferences.setFeedPlaybackSpeed(Float.parseFloat((String) values[pos]));
});
});
}
private void autoDeleteEpisodesPrefHandler() {
PreferenceListDialog preferenceListDialog = new PreferenceListDialog(activity,
"Auto delete episodes");
String[] items = activity.getResources().getStringArray(R.array.spnAutoDeleteItems);
String[] values = activity.getResources().getStringArray(R.array.spnAutoDeleteValues);
preferenceListDialog.openDialog(items);
preferenceListDialog.setOnPreferenceChangedListener(which -> {
FeedPreferences.AutoDeleteAction autoDeleteAction = null;
switch (values[which]) {
case "global":
autoDeleteAction = FeedPreferences.AutoDeleteAction.GLOBAL;
break;
case "always":
autoDeleteAction = FeedPreferences.AutoDeleteAction.YES;
break;
case "never":
autoDeleteAction = FeedPreferences.AutoDeleteAction.NO;
break;
default:
}
FeedPreferences.AutoDeleteAction finalAutoDeleteAction = autoDeleteAction;
saveFeedPreferences(feedPreferences -> {
feedPreferences.setAutoDeleteAction(finalAutoDeleteAction);
});
});
}
private void keepUpdatedPrefHandler() {
PreferenceSwitchDialog preferenceSwitchDialog = new PreferenceSwitchDialog(activity,
activity.getString(R.string.kept_updated),
activity.getString(R.string.keep_updated_summary));
preferenceSwitchDialog.setOnPreferenceChangedListener(keepUpdated -> {
saveFeedPreferences(feedPreferences -> {
feedPreferences.setKeepUpdated(keepUpdated);
});
});
preferenceSwitchDialog.openDialog();
}
private void showMessage(@PluralsRes int msgId, int numItems) {
activity.showSnackbarAbovePlayer(activity.getResources()
.getQuantityString(msgId, numItems, numItems), Snackbar.LENGTH_LONG);
}
private void saveFeedPreferences(Consumer<FeedPreferences> preferencesConsumer) {
for (Feed feed : selectedItems) {
preferencesConsumer.accept(feed.getPreferences());
DBWriter.setFeedPreferences(feed.getPreferences());
}
showMessage(R.plurals.updated_feeds_batch_label, selectedItems.size());
}
}

View File

@ -0,0 +1,49 @@
package de.danoeh.antennapod.fragment.preferences.dialog;
import android.content.Context;
import androidx.appcompat.app.AlertDialog;
import de.danoeh.antennapod.R;
public class PreferenceListDialog {
protected Context context;
private String title;
private OnPreferenceChangedListener onPreferenceChangedListener;
private int selectedPos = 0;
public PreferenceListDialog(Context context, String title) {
this.context = context;
this.title = title;
}
public interface OnPreferenceChangedListener {
/**
* Notified when user confirms preference
*
* @param pos The index of the item that was selected
*/
void preferenceChanged(int pos);
}
public void openDialog(String[] items) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(title);
builder.setSingleChoiceItems(items, selectedPos, (dialog, which) -> {
selectedPos = which;
});
builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
if (onPreferenceChangedListener != null && selectedPos >= 0) {
onPreferenceChangedListener.preferenceChanged(selectedPos);
}
});
builder.setNegativeButton(R.string.cancel_label, null);
builder.create().show();
}
public void setOnPreferenceChangedListener(OnPreferenceChangedListener onPreferenceChangedListener) {
this.onPreferenceChangedListener = onPreferenceChangedListener;
}
}

View File

@ -0,0 +1,57 @@
package de.danoeh.antennapod.fragment.preferences.dialog;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SwitchCompat;
import de.danoeh.antennapod.R;
public class PreferenceSwitchDialog {
protected Context context;
private String title;
private String text;
private OnPreferenceChangedListener onPreferenceChangedListener;
public PreferenceSwitchDialog(Context context, String title, String text) {
this.context = context;
this.title = title;
this.text = text;
}
public interface OnPreferenceChangedListener {
/**
* Notified when user confirms preference
*
* @param enabled The preference
*/
void preferenceChanged(boolean enabled);
}
public void openDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(title);
LayoutInflater inflater = LayoutInflater.from(this.context);
View layout = inflater.inflate(R.layout.dialog_switch_preference, null, false);
SwitchCompat switchButton = layout.findViewById(R.id.dialogSwitch);
switchButton.setText(text);
builder.setView(layout);
builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
if (onPreferenceChangedListener != null) {
onPreferenceChangedListener.preferenceChanged(switchButton.isChecked());
}
});
builder.setNegativeButton(R.string.cancel_label, null);
builder.create().show();
}
public void setOnPreferenceChangedListener(OnPreferenceChangedListener onPreferenceChangedListener) {
this.onPreferenceChangedListener = onPreferenceChangedListener;
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="24dp">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/dialogSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Switch" />
</LinearLayout>

View File

@ -1,33 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
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"
android:orientation="vertical">
<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"
app:title="@string/subscriptions_label"
app:navigationIcon="?homeAsUpIndicator"
android:id="@+id/toolbar"/>
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:title="@string/subscriptions_label"
app:navigationIcon="?homeAsUpIndicator" />
<TextView
android:id="@+id/feeds_filtered_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:background="?android:attr/selectableItemBackground"
android:gravity="start"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingBottom="8dp"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_small" />
android:id="@+id/feeds_filtered_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:background="?android:attr/selectableItemBackground"
android:gravity="start"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingBottom="8dp"
android:textColor="?android:attr/textColorSecondary"
android:textSize="@dimen/text_size_small" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefresh"
@ -35,35 +36,36 @@
android:layout_height="match_parent"
android:layout_below="@id/feeds_filtered_message">
<GridView
android:id="@+id/subscriptions_grid"
android:layout_width="match_parent"
android:numColumns="3"
android:horizontalSpacing="2dp"
android:verticalSpacing="2dp"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:paddingBottom="88dp"
android:clipToPadding="false"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/subscriptions_grid"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:paddingBottom="88dp"
android:clipToPadding="false" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<ProgressBar
android:id="@+id/progLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminateOnly="true"
android:visibility="visible"/>
android:id="@+id/progLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:indeterminateOnly="true"
android:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/subscriptions_add"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_margin="16dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:contentDescription="@string/add_feed_label"
app:srcCompat="@drawable/ic_add"/>
android:id="@+id/subscriptions_add"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_margin="16dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:contentDescription="@string/add_feed_label"
app:srcCompat="@drawable/ic_add" />
<include
layout="@layout/multi_select_speed_dial" />
</RelativeLayout>

View File

@ -1,11 +1,11 @@
<?xml version='1.0' encoding='utf-8'?>
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground">
<de.danoeh.antennapod.ui.common.SquareImageView
@ -15,7 +15,7 @@
android:background="@color/non_square_icon_background"
android:scaleType="fitCenter"
squareImageView:direction="width"
tools:src="@mipmap/ic_launcher_round" />
tools:src="@tools:sample/avatars" />
<com.joanzapata.iconify.widget.IconTextView
android:id="@+id/txtvTitle"
@ -46,4 +46,20 @@
app:primaryTextColor="?attr/colorOnSecondary"
app:primaryTextSize="12sp" />
<FrameLayout
android:id="@+id/selectView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_checkbox_background">
<CheckBox
android:id="@+id/selectCheckBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="0dp"
android:minHeight="0dp"
android:layout_margin="8dp" />
</FrameLayout>
</RelativeLayout>

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/remove_item"
android:menuCategory="container"
android:title="@string/remove_feed_label"
android:icon="@drawable/ic_delete"/>
<item
android:id="@+id/keep_updated"
android:menuCategory="container"
android:title="@string/keep_updated"
android:icon="@drawable/ic_refresh"/>
<item
android:id="@+id/autodownload"
android:menuCategory="container"
android:title="@string/auto_download_label"
android:icon="@drawable/ic_download"/>
<item
android:id="@+id/autoDeleteDownload"
android:menuCategory="container"
android:title="@string/auto_delete_label"
android:icon="@drawable/ic_delete_auto"/>
<item
android:id="@+id/playback_speed"
android:menuCategory="container"
android:title="@string/playback_speed"
android:icon="@drawable/ic_playback_speed"/>
</menu>

View File

@ -21,4 +21,8 @@
android:menuCategory="container"
android:title="@string/remove_feed_label" />
<item
android:id="@+id/multi_select"
android:menuCategory="container"
android:title="@string/multi_select" />
</menu>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="56dp"
android:height="56dp"
android:viewportWidth="15.1"
android:viewportHeight="15.1">
<path
android:pathData="M-7.4167,3.2465a11.2253,11.8267 0,1 0,22.4506 0a11.2253,11.8267 0,1 0,-22.4506 0z"
android:strokeWidth="0"
android:fillColor="#BBFFFFFF"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M15,2l-3.5,0l-1,-1l-5,0l-1,1l-3.5,0l0,2l14,0z"
android:fillColor="#000000"/>
<path
android:pathData="M16,9c-0.7,0 -1.37,0.1 -2,0.29V5H2v12c0,1.1 0.9,2 2,2h5.68c1.12,2.36 3.53,4 6.32,4c3.87,0 7,-3.13 7,-7C23,12.13 19.87,9 16,9zM9,16c0,0.34 0.03,0.67 0.08,1H4V7h8v3.26C10.19,11.53 9,13.62 9,16zM16,21c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5s5,2.24 5,5S18.76,21 16,21z"
android:fillColor="#000000"/>
<path
android:pathData="M16.5,12l-1.5,0l0,5l3.6,2.1l0.8,-1.2l-2.9,-1.7z"
android:fillColor="#000000"/>
</vector>

View File

@ -171,6 +171,7 @@
<string name="share_website_url_label">Website address</string>
<string name="share_feed_url_label">Podcast feed URL</string>
<string name="feed_delete_confirmation_msg">Please confirm that you want to delete the podcast \"%1$s\" and ALL its episodes (including downloaded episodes).</string>
<string name="feed_delete_confirmation_msg_batch">Please confirm that you want to remove the selected podcasts and ALL their episodes (including downloaded episodes).</string>
<string name="feed_delete_confirmation_local_msg">Please confirm that you want to remove the podcast \"%1$s\". The files in the local source folder will not be deleted.</string>
<string name="feed_remover_msg">Removing podcast</string>
<string name="load_complete_feed">Refresh complete podcast</string>
@ -182,6 +183,10 @@
<string name="open_podcast">Open Podcast</string>
<string name="please_wait_for_data">Please wait until the data is loaded</string>
<string name="updates_disabled_label">Updates disabled</string>
<plurals name="updated_feeds_batch_label">
<item quantity="one">%d subscription updated.</item>
<item quantity="other">%d subscriptions updated.</item>
</plurals>
<string name="add_to_folder">Add to folder</string>
<!-- actions on feeditems -->