Swipe actions (#5191)

This commit is contained in:
ueen 2021-07-12 00:03:32 +02:00 committed by GitHub
parent c1efd51be9
commit ca9ad0d2d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1296 additions and 152 deletions

View File

@ -149,6 +149,8 @@ dependencies {
implementation 'com.github.mfietz:fyydlin:v0.5.0'
implementation 'com.github.ByteHamster:SearchPreference:v2.0.0'
implementation 'com.github.skydoves:balloon:1.1.5'
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.2.3'
implementation 'com.annimon:stream:1.2.2'
// Non-free dependencies:
playImplementation 'com.google.android.play:core:1.8.0'

View File

@ -27,6 +27,7 @@ import de.danoeh.antennapod.fragment.preferences.NetworkPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.NotificationPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.PlaybackPreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.StoragePreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.SwipePreferencesFragment;
import de.danoeh.antennapod.fragment.preferences.UserInterfacePreferencesFragment;
/**
@ -80,6 +81,8 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe
prefFragment = new PlaybackPreferencesFragment();
} else if (screen == R.xml.preferences_notifications) {
prefFragment = new NotificationPreferencesFragment();
} else if (screen == R.xml.preferences_swipe) {
prefFragment = new SwipePreferencesFragment();
}
return prefFragment;
}
@ -104,6 +107,8 @@ public class PreferenceActivity extends AppCompatActivity implements SearchPrefe
return R.string.notification_pref_fragment;
case R.xml.feed_settings:
return R.string.feed_settings_label;
case R.xml.preferences_swipe:
return R.string.swipeactions_label;
default:
return R.string.settings_label;
}

View File

@ -6,10 +6,11 @@ import android.view.ContextMenu;
import android.view.MenuInflater;
import android.view.MotionEvent;
import android.view.View;
import androidx.recyclerview.widget.ItemTouchHelper;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.fragment.swipeactions.SwipeActions;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
/**
@ -18,13 +19,13 @@ import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
public class QueueRecyclerAdapter extends EpisodeItemListAdapter {
private static final String TAG = "QueueRecyclerAdapter";
private final ItemTouchHelper itemTouchHelper;
private final SwipeActions swipeActions;
private boolean dragDropEnabled;
public QueueRecyclerAdapter(MainActivity mainActivity, ItemTouchHelper itemTouchHelper) {
public QueueRecyclerAdapter(MainActivity mainActivity, SwipeActions swipeActions) {
super(mainActivity);
this.itemTouchHelper = itemTouchHelper;
this.swipeActions = swipeActions;
dragDropEnabled = ! (UserPreferences.isQueueKeepSorted() || UserPreferences.isQueueLocked());
}
@ -39,7 +40,7 @@ public class QueueRecyclerAdapter extends EpisodeItemListAdapter {
View.OnTouchListener startDragTouchListener = (v1, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
Log.d(TAG, "startDrag()");
itemTouchHelper.startDrag(holder);
swipeActions.startDrag(holder);
}
return false;
};

View File

@ -19,7 +19,7 @@ abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends Recy
private ActionMode actionMode;
private final HashSet<Long> selectedIds = new HashSet<>();
private final Activity activity;
private OnEndSelectModeListener onEndSelectModeListener;
private OnSelectModeListener onSelectModeListener;
public SelectableAdapter(Activity activity) {
this.activity = activity;
@ -30,6 +30,10 @@ abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends Recy
endSelectMode();
}
if (onSelectModeListener != null) {
onSelectModeListener.onStartSelectMode();
}
selectedIds.clear();
selectedIds.add(getItemId(pos));
notifyDataSetChanged();
@ -152,17 +156,19 @@ abstract class SelectableAdapter<T extends RecyclerView.ViewHolder> extends Recy
selectedIds.size(), getItemCount()));
}
public void setOnEndSelectModeListener(OnEndSelectModeListener onEndSelectModeListener) {
this.onEndSelectModeListener = onEndSelectModeListener;
public void setOnSelectModeListener(OnSelectModeListener onSelectModeListener) {
this.onSelectModeListener = onSelectModeListener;
}
private void callOnEndSelectMode() {
if (onEndSelectModeListener != null) {
onEndSelectModeListener.onEndSelectMode();
if (onSelectModeListener != null) {
onSelectModeListener.onEndSelectMode();
}
}
public interface OnEndSelectModeListener {
public interface OnSelectModeListener {
void onStartSelectMode();
void onEndSelectMode();
}
}

View File

@ -0,0 +1,188 @@
package de.danoeh.antennapod.dialog;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.view.LayoutInflater;
import android.view.View;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.core.graphics.drawable.DrawableCompat;
import androidx.gridlayout.widget.GridLayout;
import com.annimon.stream.Stream;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.databinding.FeeditemlistItemBinding;
import de.danoeh.antennapod.databinding.SwipeactionsDialogBinding;
import de.danoeh.antennapod.databinding.SwipeactionsPickerBinding;
import de.danoeh.antennapod.databinding.SwipeactionsPickerItemBinding;
import de.danoeh.antennapod.databinding.SwipeactionsRowBinding;
import de.danoeh.antennapod.fragment.EpisodesFragment;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.swipeactions.SwipeAction;
import de.danoeh.antennapod.fragment.swipeactions.SwipeActions;
import de.danoeh.antennapod.ui.common.ThemeUtils;
public class SwipeActionsDialog {
private static final int LEFT = 1;
private static final int RIGHT = 0;
private final Context context;
private final String tag;
private SwipeAction rightAction;
private SwipeAction leftAction;
private List<SwipeAction> keys;
public SwipeActionsDialog(Context context, String tag) {
this.context = context;
this.tag = tag;
}
public void show(Callback prefsChanged) {
SwipeActions.Actions actions = SwipeActions.getPrefsWithDefaults(context, tag);
leftAction = actions.left;
rightAction = actions.right;
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
keys = SwipeActions.swipeActions;
String forFragment = "";
switch (tag) {
/*case InboxFragment.TAG:
forFragment = context.getString(R.string.inbox_label);
break;*/
case EpisodesFragment.TAG:
forFragment = context.getString(R.string.episodes_label);
break;
case FeedItemlistFragment.TAG:
forFragment = context.getString(R.string.feeds_label);
break;
case QueueFragment.TAG:
forFragment = context.getString(R.string.queue_label);
keys = Stream.of(keys).filter(a -> !a.getId().equals(SwipeAction.ADD_TO_QUEUE)
&& !a.getId().equals(SwipeAction.REMOVE_FROM_INBOX)).toList();
break;
default: break;
}
if (!tag.equals(QueueFragment.TAG)) {
keys = Stream.of(keys).filter(a -> !a.getId().equals(SwipeAction.REMOVE_FROM_QUEUE)).toList();
}
builder.setTitle(context.getString(R.string.swipeactions_label) + " - " + forFragment);
SwipeactionsDialogBinding viewBinding = SwipeactionsDialogBinding.inflate(LayoutInflater.from(context));
builder.setView(viewBinding.getRoot());
viewBinding.enableSwitch.setOnCheckedChangeListener((compoundButton, b) -> {
viewBinding.actionLeftContainer.getRoot().setAlpha(b ? 1.0f : 0.4f);
viewBinding.actionRightContainer.getRoot().setAlpha(b ? 1.0f : 0.4f);
});
viewBinding.enableSwitch.setChecked(SwipeActions.isSwipeActionEnabled(context, tag));
setupSwipeDirectionView(viewBinding.actionLeftContainer, LEFT);
setupSwipeDirectionView(viewBinding.actionRightContainer, RIGHT);
builder.setPositiveButton(R.string.confirm_label, (dialog, which) -> {
savePrefs(tag, rightAction.getId(), leftAction.getId());
saveActionsEnabledPrefs(viewBinding.enableSwitch.isChecked());
prefsChanged.onCall();
});
builder.setNegativeButton(R.string.cancel_label, null);
builder.create().show();
}
private void setupSwipeDirectionView(SwipeactionsRowBinding view, int direction) {
SwipeAction action = direction == LEFT ? leftAction : rightAction;
view.swipeDirectionLabel.setText(direction == LEFT ? R.string.swipe_left : R.string.swipe_right);
view.swipeActionLabel.setText(action.getTitle(context));
populateMockEpisode(view.mockEpisode);
if (direction == RIGHT && view.previewContainer.getChildAt(0) != view.swipeIcon) {
view.previewContainer.removeView(view.swipeIcon);
view.previewContainer.addView(view.swipeIcon, 0);
}
view.swipeIcon.setImageResource(action.getActionIcon());
view.swipeIcon.setColorFilter(ThemeUtils.getColorFromAttr(context, action.getActionColor()));
view.changeButton.setOnClickListener(v -> showPicker(view, direction));
view.previewContainer.setOnClickListener(v -> showPicker(view, direction));
}
private void showPicker(SwipeactionsRowBinding view, int direction) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(direction == LEFT ? R.string.swipe_left : R.string.swipe_right);
SwipeactionsPickerBinding picker = SwipeactionsPickerBinding.inflate(LayoutInflater.from(context));
builder.setView(picker.getRoot());
builder.setNegativeButton(R.string.cancel_label, null);
AlertDialog dialog = builder.show();
for (int i = 0; i < keys.size(); i++) {
final int actionIndex = i;
SwipeAction action = keys.get(actionIndex);
SwipeactionsPickerItemBinding item = SwipeactionsPickerItemBinding.inflate(LayoutInflater.from(context));
item.swipeActionLabel.setText(action.getTitle(context));
Drawable icon = DrawableCompat.wrap(AppCompatResources.getDrawable(context, action.getActionIcon()));
DrawableCompat.setTintMode(icon, PorterDuff.Mode.SRC_ATOP);
if ((direction == LEFT && leftAction == action) || (direction == RIGHT && rightAction == action)) {
DrawableCompat.setTint(icon, ThemeUtils.getColorFromAttr(context, action.getActionColor()));
item.swipeActionLabel.setTextColor(ThemeUtils.getColorFromAttr(context, action.getActionColor()));
} else {
DrawableCompat.setTint(icon, ThemeUtils.getColorFromAttr(context, R.attr.action_icon_color));
}
item.swipeIcon.setImageDrawable(icon);
item.getRoot().setOnClickListener(v -> {
if (direction == LEFT) {
leftAction = keys.get(actionIndex);
} else {
rightAction = keys.get(actionIndex);
}
setupSwipeDirectionView(view, direction);
dialog.dismiss();
});
GridLayout.LayoutParams param = new GridLayout.LayoutParams(
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.BASELINE),
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f));
param.width = 0;
picker.pickerGridLayout.addView(item.getRoot(), param);
}
picker.pickerGridLayout.setColumnCount(2);
picker.pickerGridLayout.setRowCount((keys.size() + 1) / 2);
}
private void populateMockEpisode(FeeditemlistItemBinding view) {
view.container.setAlpha(0.3f);
view.secondaryActionButton.secondaryActionButton.setVisibility(View.GONE);
view.dragHandle.setVisibility(View.GONE);
view.statusUnread.setText("███");
view.txtvTitle.setText("███████");
view.txtvPosition.setText("█████");
}
private void savePrefs(String tag, String right, String left) {
SharedPreferences prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE);
prefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + tag, right + "," + left).apply();
}
private void saveActionsEnabledPrefs(Boolean enabled) {
SharedPreferences prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE);
prefs.edit().putBoolean(SwipeActions.KEY_PREFIX_NO_ACTION + tag, enabled).apply();
}
public interface Callback {
void onCall();
}
}

View File

@ -54,7 +54,7 @@ import java.util.List;
* Displays all completed downloads and provides a button to delete them.
*/
public class CompletedDownloadsFragment extends Fragment implements
EpisodeItemListAdapter.OnEndSelectModeListener {
EpisodeItemListAdapter.OnSelectModeListener {
private static final String TAG = CompletedDownloadsFragment.class.getSimpleName();
@ -79,7 +79,7 @@ public class CompletedDownloadsFragment extends Fragment implements
recyclerView = root.findViewById(R.id.recyclerView);
recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool());
adapter = new CompletedDownloadsListAdapter((MainActivity) getActivity());
adapter.setOnEndSelectModeListener(this);
adapter.setOnSelectModeListener(this);
recyclerView.setAdapter(adapter);
progressBar = root.findViewById(R.id.progLoading);
@ -107,7 +107,6 @@ public class CompletedDownloadsFragment extends Fragment implements
speedDialView.setOnActionSelectedListener(actionItem -> {
new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), adapter.getSelectedItems())
.handleAction(actionItem.getId());
onEndSelectMode();
adapter.endSelectMode();
return true;
});
@ -171,9 +170,6 @@ public class CompletedDownloadsFragment extends Fragment implements
Log.i(TAG, "Selected item at current position was null, ignoring selection");
return super.onContextItemSelected(item);
}
if (item.getItemId() == R.id.multi_select) {
speedDialView.setVisibility(View.VISIBLE);
}
if (adapter.onContextItemSelected(item)) {
return true;
}
@ -258,6 +254,11 @@ public class CompletedDownloadsFragment extends Fragment implements
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
@Override
public void onStartSelectMode() {
speedDialView.setVisibility(View.VISIBLE);
}
@Override
public void onEndSelectMode() {
speedDialView.close();

View File

@ -51,15 +51,14 @@ import de.danoeh.antennapod.adapter.EpisodeItemListAdapter;
import de.danoeh.antennapod.core.dialog.DownloadRequestErrorDialogCreator;
import de.danoeh.antennapod.core.event.DownloadEvent;
import de.danoeh.antennapod.core.event.DownloaderUpdate;
import de.danoeh.antennapod.core.event.FavoritesEvent;
import de.danoeh.antennapod.core.event.FeedItemEvent;
import de.danoeh.antennapod.core.event.FeedListUpdateEvent;
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
import de.danoeh.antennapod.core.event.PlayerStatusEvent;
import de.danoeh.antennapod.core.event.QueueEvent;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.core.feed.FeedEvent;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.core.glide.ApGlideSettings;
import de.danoeh.antennapod.core.glide.FastBlurTransformation;
import de.danoeh.antennapod.core.service.download.DownloadService;
@ -74,10 +73,14 @@ import de.danoeh.antennapod.core.util.gui.MoreContentListFooterUtil;
import de.danoeh.antennapod.dialog.FilterDialog;
import de.danoeh.antennapod.dialog.RemoveFeedDialog;
import de.danoeh.antennapod.dialog.RenameFeedDialog;
import de.danoeh.antennapod.fragment.swipeactions.SwipeActions;
import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.menuhandler.FeedMenuHandler;
import de.danoeh.antennapod.menuhandler.MenuItemUtils;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.view.EpisodeItemListRecyclerView;
import de.danoeh.antennapod.view.ToolbarIconTintManager;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
@ -90,12 +93,13 @@ import io.reactivex.schedulers.Schedulers;
* Displays a list of FeedItems.
*/
public class FeedItemlistFragment extends Fragment implements AdapterView.OnItemClickListener,
Toolbar.OnMenuItemClickListener, EpisodeItemListAdapter.OnEndSelectModeListener {
private static final String TAG = "ItemlistFragment";
Toolbar.OnMenuItemClickListener, EpisodeItemListAdapter.OnSelectModeListener {
public static final String TAG = "ItemlistFragment";
private static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id";
private static final String KEY_UP_ARROW = "up_arrow";
private FeedItemListAdapter adapter;
private SwipeActions swipeActions;
private MoreContentListFooterUtil nextPageLoader;
private ProgressBar progressBar;
@ -166,6 +170,7 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
recyclerView.setRecycledViewPool(((MainActivity) getActivity()).getRecycledViewPool());
progressBar = root.findViewById(R.id.progLoading);
progressBar.setVisibility(View.VISIBLE);
txtvTitle = root.findViewById(R.id.txtvTitle);
txtvAuthor = root.findViewById(R.id.txtvAuthor);
imgvBackground = root.findViewById(R.id.imgvBackground);
@ -252,7 +257,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
speedDialView.setOnActionSelectedListener(actionItem -> {
new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), adapter.getSelectedItems())
.handleAction(actionItem.getId());
onEndSelectMode();
adapter.endSelectMode();
return true;
});
@ -348,16 +352,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
Log.i(TAG, "Selected item at current position was null, ignoring selection");
return super.onContextItemSelected(item);
}
if (item.getItemId() == R.id.multi_select) {
if (feed.isLocalFeed()) {
speedDialView.removeActionItemById(R.id.download_batch);
speedDialView.removeActionItemById(R.id.delete_batch);
}
speedDialView.setVisibility(View.VISIBLE);
refreshToolbarState();
// Do not return: Let adapter handle its actions, too.
}
if (adapter.onContextItemSelected(item)) {
return true;
}
@ -432,10 +426,32 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void favoritesChanged(FavoritesEvent event) {
updateUi();
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onQueueChanged(QueueEvent event) {
updateUi();
}
@Override
public void onStartSelectMode() {
swipeActions.detach();
if (feed.isLocalFeed()) {
speedDialView.removeActionItemById(R.id.download_batch);
speedDialView.removeActionItemById(R.id.delete_batch);
}
speedDialView.setVisibility(View.VISIBLE);
refreshToolbarState();
}
@Override
public void onEndSelectMode() {
speedDialView.close();
speedDialView.setVisibility(View.GONE);
swipeActions.attachTo(recyclerView);
}
private void updateUi() {
@ -478,12 +494,14 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
if (adapter == null) {
recyclerView.setAdapter(null);
adapter = new FeedItemListAdapter((MainActivity) getActivity());
adapter.setOnEndSelectModeListener(this);
adapter.setOnSelectModeListener(this);
recyclerView.setAdapter(adapter);
swipeActions = new SwipeActions(this, TAG).attachTo(recyclerView);
}
progressBar.setVisibility(View.GONE);
if (feed != null) {
adapter.updateItems(feed.getItems());
swipeActions.setFilter(feed.getItemFilter());
}
refreshToolbarState();
@ -597,7 +615,6 @@ public class FeedItemlistFragment extends Fragment implements AdapterView.OnItem
if (disposable != null) {
disposable.dispose();
}
progressBar.setVisibility(View.VISIBLE);
disposable = Observable.fromCallable(this::loadData)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())

View File

@ -39,6 +39,7 @@ import de.danoeh.antennapod.core.event.PlayerStatusEvent;
import de.danoeh.antennapod.core.event.QueueEvent;
import de.danoeh.antennapod.core.event.UnreadItemsUpdateEvent;
import de.danoeh.antennapod.fragment.actions.EpisodeMultiSelectActionHandler;
import de.danoeh.antennapod.fragment.swipeactions.SwipeActions;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.core.feed.util.PlaybackSpeedUtils;
import de.danoeh.antennapod.core.preferences.UserPreferences;
@ -48,6 +49,7 @@ import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.DownloadRequester;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.SortOrder;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
@ -70,7 +72,7 @@ import java.util.Locale;
* Shows all items in the queue.
*/
public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickListener,
EpisodeItemListAdapter.OnEndSelectModeListener {
EpisodeItemListAdapter.OnSelectModeListener {
public static final String TAG = "QueueFragment";
private static final String KEY_UP_ARROW = "up_arrow";
@ -90,7 +92,7 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
private static final String PREF_SHOW_LOCK_WARNING = "show_lock_warning";
private Disposable disposable;
private ItemTouchHelper itemTouchHelper;
private SwipeActions swipeActions;
private SharedPreferences prefs;
private SpeedDialView speedDialView;
@ -398,12 +400,6 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
Log.i(TAG, "Selected item no longer exist, ignoring selection");
return super.onContextItemSelected(item);
}
if (item.getItemId() == R.id.multi_select) {
speedDialView.setVisibility(View.VISIBLE);
refreshToolbarState();
infoBar.setVisibility(View.GONE);
// Do not return: Let adapter handle its actions, too.
}
if (recyclerAdapter.onContextItemSelected(item)) {
return true;
}
@ -455,83 +451,9 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
getResources().getInteger(R.integer.swipe_to_refresh_duration_in_ms));
});
itemTouchHelper = new ItemTouchHelper(
new ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP | ItemTouchHelper.DOWN,
ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) {
// Position tracking whilst dragging
int dragFrom = -1;
int dragTo = -1;
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getAdapterPosition();
int toPosition = target.getAdapterPosition();
// Update tracked position
if (dragFrom == -1) {
dragFrom = fromPosition;
}
dragTo = toPosition;
int from = viewHolder.getAdapterPosition();
int to = target.getAdapterPosition();
Log.d(TAG, "move(" + from + ", " + to + ") in memory");
if (from >= queue.size() || to >= queue.size() || from < 0 || to < 0) {
return false;
}
queue.add(to, queue.remove(from));
recyclerAdapter.notifyItemMoved(from, to);
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
if (disposable != null) {
disposable.dispose();
}
final int position = viewHolder.getAdapterPosition();
Log.d(TAG, "remove(" + position + ")");
final FeedItem item = queue.get(position);
DBWriter.removeQueueItem(getActivity(), true, item);
((MainActivity) getActivity()).showSnackbarAbovePlayer(
getResources().getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1),
Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo), v ->
DBWriter.addQueueItemAt(getActivity(), item.getId(), position, false));
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return !UserPreferences.isQueueLocked();
}
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
// Check if drag finished
if (dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) {
reallyMoved(dragFrom, dragTo);
}
dragFrom = dragTo = -1;
}
private void reallyMoved(int from, int to) {
// Write drag operation to database
Log.d(TAG, "Write to database move(" + from + ", " + to + ")");
DBWriter.moveQueueItem(from, to, true);
}
}
);
itemTouchHelper.attachToRecyclerView(recyclerView);
swipeActions = new QueueSwipeActions();
swipeActions.setFilter(new FeedItemFilter(FeedItemFilter.QUEUED));
swipeActions.attachTo(recyclerView);
emptyView = new EmptyViewHandler(getContext());
emptyView.attachToRecyclerView(recyclerView);
@ -565,7 +487,6 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
speedDialView.setOnActionSelectedListener(actionItem -> {
new EpisodeMultiSelectActionHandler(((MainActivity) getActivity()), recyclerAdapter.getSelectedItems())
.handleAction(actionItem.getId());
onEndSelectMode();
recyclerAdapter.endSelectMode();
return true;
});
@ -582,8 +503,8 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
if (queue != null && queue.size() > 0) {
if (recyclerAdapter == null) {
MainActivity activity = (MainActivity) getActivity();
recyclerAdapter = new QueueRecyclerAdapter(activity, itemTouchHelper);
recyclerAdapter.setOnEndSelectModeListener(this);
recyclerAdapter = new QueueRecyclerAdapter(activity, swipeActions);
recyclerAdapter.setOnSelectModeListener(this);
recyclerView.setAdapter(recyclerAdapter);
emptyView.updateAdapter(recyclerAdapter);
}
@ -648,10 +569,91 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
}
@Override
public void onStartSelectMode() {
swipeActions.detach();
speedDialView.setVisibility(View.VISIBLE);
refreshToolbarState();
infoBar.setVisibility(View.GONE);
}
@Override
public void onEndSelectMode() {
speedDialView.close();
speedDialView.setVisibility(View.GONE);
infoBar.setVisibility(View.VISIBLE);
swipeActions.attachTo(recyclerView);
}
private class QueueSwipeActions extends SwipeActions {
// Position tracking whilst dragging
int dragFrom = -1;
int dragTo = -1;
public QueueSwipeActions() {
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, QueueFragment.this, TAG);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder target) {
int fromPosition = viewHolder.getBindingAdapterPosition();
int toPosition = target.getBindingAdapterPosition();
// Update tracked position
if (dragFrom == -1) {
dragFrom = fromPosition;
}
dragTo = toPosition;
int from = viewHolder.getBindingAdapterPosition();
int to = target.getBindingAdapterPosition();
Log.d(TAG, "move(" + from + ", " + to + ") in memory");
if (from >= queue.size() || to >= queue.size() || from < 0 || to < 0) {
return false;
}
queue.add(to, queue.remove(from));
recyclerAdapter.notifyItemMoved(from, to);
return true;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
if (disposable != null) {
disposable.dispose();
}
//SwipeActions
super.onSwiped(viewHolder, direction);
}
@Override
public boolean isLongPressDragEnabled() {
return false;
}
@Override
public boolean isItemViewSwipeEnabled() {
return !UserPreferences.isQueueLocked();
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
// Check if drag finished
if (dragFrom != -1 && dragTo != -1 && dragFrom != dragTo) {
reallyMoved(dragFrom, dragTo);
}
dragFrom = dragTo = -1;
}
private void reallyMoved(int from, int to) {
// Write drag operation to database
Log.d(TAG, "Write to database move(" + from + ", " + to + ")");
DBWriter.moveQueueItem(from, to, true);
}
}
}

View File

@ -148,5 +148,8 @@ public class MainPreferencesFragment extends PreferenceFragmentCompat {
.addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_notifications));
config.index(R.xml.feed_settings)
.addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.feed_settings));
config.index(R.xml.preferences_swipe)
.addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_user_interface))
.addBreadcrumb(PreferenceActivity.getTitleOfPage(R.xml.preferences_swipe));
}
}

View File

@ -0,0 +1,37 @@
package de.danoeh.antennapod.fragment.preferences;
import android.os.Bundle;
import androidx.preference.PreferenceFragmentCompat;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.PreferenceActivity;
import de.danoeh.antennapod.dialog.SwipeActionsDialog;
import de.danoeh.antennapod.fragment.FeedItemlistFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
public class SwipePreferencesFragment extends PreferenceFragmentCompat {
private static final String PREF_SWIPE_FEED = "prefSwipeFeed";
private static final String PREF_SWIPE_QUEUE = "prefSwipeQueue";
//private static final String PREF_SWIPE_INBOX = "prefSwipeInbox";
//private static final String PREF_SWIPE_EPISODES = "prefSwipeEpisodes";
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
addPreferencesFromResource(R.xml.preferences_swipe);
findPreference(PREF_SWIPE_FEED).setOnPreferenceClickListener(preference -> {
new SwipeActionsDialog(requireContext(), FeedItemlistFragment.TAG).show(() -> { });
return true;
});
findPreference(PREF_SWIPE_QUEUE).setOnPreferenceClickListener(preference -> {
new SwipeActionsDialog(requireContext(), QueueFragment.TAG).show(() -> { });
return true;
});
}
@Override
public void onStart() {
super.onStart();
((PreferenceActivity) getActivity()).getSupportActionBar().setTitle(R.string.swipeactions_label);
}
}

View File

@ -21,6 +21,7 @@ import org.greenrobot.eventbus.EventBus;
import java.util.List;
public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat {
private static final String PREF_SWIPE = "prefSwipe";
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
@ -98,6 +99,11 @@ public class UserInterfacePreferencesFragment extends PreferenceFragmentCompat {
FeedSortDialog.showDialog(requireContext());
return true;
}));
findPreference(PREF_SWIPE)
.setOnPreferenceClickListener(preference -> {
((PreferenceActivity) getActivity()).openScreen(R.xml.preferences_swipe);
return true;
});
if (Build.VERSION.SDK_INT >= 26) {
findPreference(UserPreferences.PREF_EXPANDED_NOTIFICATION).setVisible(false);

View File

@ -0,0 +1,47 @@
package de.danoeh.antennapod.fragment.swipeactions;
import android.content.Context;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
public class AddToQueueSwipeAction implements SwipeAction {
@Override
public String getId() {
return ADD_TO_QUEUE;
}
@Override
public int getActionIcon() {
return R.drawable.ic_playlist;
}
@Override
public int getActionColor() {
return R.attr.colorAccent;
}
@Override
public String getTitle(Context context) {
return context.getString(R.string.add_to_queue_label);
}
@Override
public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) {
if (!item.isTagged(FeedItem.TAG_QUEUE)) {
DBWriter.addQueueItem(fragment.requireContext(), item);
} else {
new RemoveFromQueueSwipeAction().performAction(item, fragment, filter);
}
}
@Override
public boolean willRemove(FeedItemFilter filter) {
return filter.showQueued || filter.showNew;
}
}

View File

@ -0,0 +1,43 @@
package de.danoeh.antennapod.fragment.swipeactions;
import android.content.Context;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
public class MarkFavoriteSwipeAction implements SwipeAction {
@Override
public String getId() {
return MARK_FAV;
}
@Override
public int getActionIcon() {
return R.drawable.ic_star;
}
@Override
public int getActionColor() {
return R.attr.icon_yellow;
}
@Override
public String getTitle(Context context) {
return context.getString(R.string.add_to_favorite_label);
}
@Override
public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) {
DBWriter.toggleFavoriteItem(item);
}
@Override
public boolean willRemove(FeedItemFilter filter) {
return filter.showIsFavorite || filter.showNotFavorite;
}
}

View File

@ -0,0 +1,46 @@
package de.danoeh.antennapod.fragment.swipeactions;
import android.content.Context;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
public class MarkPlayedSwipeAction implements SwipeAction {
@Override
public String getId() {
return MARK_PLAYED;
}
@Override
public int getActionIcon() {
return R.drawable.ic_mark_played;
}
@Override
public int getActionColor() {
return R.attr.icon_gray;
}
@Override
public String getTitle(Context context) {
return context.getString(R.string.mark_read_label);
}
@Override
public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) {
int togglePlayState =
item.getPlayState() != FeedItem.PLAYED ? FeedItem.PLAYED : FeedItem.UNPLAYED;
FeedItemMenuHandler.markReadWithUndo(fragment,
item, togglePlayState, willRemove(filter));
}
@Override
public boolean willRemove(FeedItemFilter filter) {
return filter.showUnplayed || filter.showPlayed;
}
}

View File

@ -0,0 +1,44 @@
package de.danoeh.antennapod.fragment.swipeactions;
import android.content.Context;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.menuhandler.FeedItemMenuHandler;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
public class RemoveFromInboxSwipeAction implements SwipeAction {
@Override
public String getId() {
return REMOVE_FROM_INBOX;
}
@Override
public int getActionIcon() {
return R.drawable.ic_check;
}
@Override
public int getActionColor() {
return R.attr.icon_purple;
}
@Override
public String getTitle(Context context) {
return context.getString(R.string.remove_new_flag_label);
}
@Override
public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) {
FeedItemMenuHandler.markReadWithUndo(fragment,
item, FeedItem.UNPLAYED, willRemove(filter));
}
@Override
public boolean willRemove(FeedItemFilter filter) {
return filter.showUnplayed;
}
}

View File

@ -0,0 +1,57 @@
package de.danoeh.antennapod.fragment.swipeactions;
import android.content.Context;
import androidx.fragment.app.Fragment;
import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
public class RemoveFromQueueSwipeAction implements SwipeAction {
@Override
public String getId() {
return REMOVE_FROM_QUEUE;
}
@Override
public int getActionIcon() {
return R.drawable.ic_playlist_remove;
}
@Override
public int getActionColor() {
return R.attr.colorAccent;
}
@Override
public String getTitle(Context context) {
return context.getString(R.string.remove_from_queue_label);
}
@Override
public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) {
int position = DBReader.getQueueIDList().indexOf(item.getId());
DBWriter.removeQueueItem(fragment.requireActivity(), true, item);
if (willRemove(filter)) {
((MainActivity) fragment.requireActivity()).showSnackbarAbovePlayer(
fragment.getResources().getQuantityString(R.plurals.removed_from_queue_batch_label, 1, 1),
Snackbar.LENGTH_LONG)
.setAction(fragment.getString(R.string.undo), v ->
DBWriter.addQueueItemAt(fragment.requireActivity(), item.getId(), position, false));
}
}
@Override
public boolean willRemove(FeedItemFilter filter) {
return filter.showQueued || filter.showNotQueued;
}
}

View File

@ -0,0 +1,42 @@
package de.danoeh.antennapod.fragment.swipeactions;
import android.content.Context;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
public class ShowFirstSwipeDialogAction implements SwipeAction {
@Override
public String getId() {
return "SHOW_FIRST_SWIPE_DIALOG";
}
@Override
public int getActionIcon() {
return R.drawable.ic_settings;
}
@Override
public int getActionColor() {
return R.attr.icon_gray;
}
@Override
public String getTitle(Context context) {
return "";
}
@Override
public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) {
//handled in SwipeActions
}
@Override
public boolean willRemove(FeedItemFilter filter) {
return false;
}
}

View File

@ -0,0 +1,44 @@
package de.danoeh.antennapod.fragment.swipeactions;
import android.content.Context;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.actionbutton.DownloadActionButton;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
public class StartDownloadSwipeAction implements SwipeAction {
@Override
public String getId() {
return START_DOWNLOAD;
}
@Override
public int getActionIcon() {
return R.drawable.ic_download;
}
@Override
public int getActionColor() {
return R.attr.icon_green;
}
@Override
public String getTitle(Context context) {
return context.getString(R.string.download_label);
}
@Override
public void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter) {
if (!item.isDownloaded() && !item.getFeed().isLocalFeed()) {
new DownloadActionButton(item)
.onClick(fragment.requireContext());
}
}
@Override
public boolean willRemove(FeedItemFilter filter) {
return false;
}
}

View File

@ -0,0 +1,34 @@
package de.danoeh.antennapod.fragment.swipeactions;
import android.content.Context;
import androidx.annotation.AttrRes;
import androidx.annotation.DrawableRes;
import androidx.fragment.app.Fragment;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
public interface SwipeAction {
String ADD_TO_QUEUE = "ADD_TO_QUEUE";
String REMOVE_FROM_INBOX = "REMOVE_FROM_INBOX";
String START_DOWNLOAD = "START_DOWNLOAD";
String MARK_FAV = "MARK_FAV";
String MARK_PLAYED = "MARK_PLAYED";
String REMOVE_FROM_QUEUE = "REMOVE_FROM_QUEUE";
String getId();
String getTitle(Context context);
@DrawableRes
int getActionIcon();
@AttrRes
int getActionColor();
void performAction(FeedItem item, Fragment fragment, FeedItemFilter filter);
boolean willRemove(FeedItemFilter filter);
}

View File

@ -0,0 +1,258 @@
package de.danoeh.antennapod.fragment.swipeactions;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Canvas;
import androidx.annotation.NonNull;
import androidx.core.graphics.ColorUtils;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleObserver;
import androidx.lifecycle.OnLifecycleEvent;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.annimon.stream.Stream;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.dialog.SwipeActionsDialog;
import de.danoeh.antennapod.fragment.EpisodesFragment;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import de.danoeh.antennapod.view.viewholder.EpisodeItemViewHolder;
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator;
public class SwipeActions extends ItemTouchHelper.SimpleCallback implements LifecycleObserver {
public static final String PREF_NAME = "SwipeActionsPrefs";
public static final String KEY_PREFIX_SWIPEACTIONS = "PrefSwipeActions6543";
public static final String KEY_PREFIX_NO_ACTION = "PrefNoSwipeAction6543";
public static final List<SwipeAction> swipeActions = Collections.unmodifiableList(
Arrays.asList(new AddToQueueSwipeAction(), new RemoveFromInboxSwipeAction(),
new StartDownloadSwipeAction(), new MarkFavoriteSwipeAction(),
new MarkPlayedSwipeAction(), new RemoveFromQueueSwipeAction())
);
private final Fragment fragment;
private final String tag;
private FeedItemFilter filter = null;
Actions actions;
boolean swipeOutEnabled = true;
int swipedOutTo = 0;
private final ItemTouchHelper itemTouchHelper = new ItemTouchHelper(this);
public SwipeActions(int dragDirs, Fragment fragment, String tag) {
super(dragDirs, ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT);
this.fragment = fragment;
this.tag = tag;
reloadPreference();
fragment.getLifecycle().addObserver(this);
}
public SwipeActions(Fragment fragment, String tag) {
this(0, fragment, tag);
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
public void reloadPreference() {
actions = getPrefs(fragment.requireContext(), tag);
}
public void setFilter(FeedItemFilter filter) {
this.filter = filter;
}
public SwipeActions attachTo(RecyclerView recyclerView) {
itemTouchHelper.attachToRecyclerView(recyclerView);
return this;
}
public void detach() {
itemTouchHelper.attachToRecyclerView(null);
}
private static Actions getPrefs(Context context, String tag, String defaultActions) {
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
String prefsString = prefs.getString(KEY_PREFIX_SWIPEACTIONS + tag, defaultActions);
return new Actions(prefsString);
}
private static Actions getPrefs(Context context, String tag) {
return getPrefs(context, tag, "");
}
public static Actions getPrefsWithDefaults(Context context, String tag) {
String defaultActions;
switch (tag) {
/*case InboxFragment.TAG:
defaultActions = new int[] {ADD_TO_QUEUE, MARK_UNPLAYED};
break;*/
case QueueFragment.TAG:
defaultActions = SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE;
break;
default:
case EpisodesFragment.TAG:
defaultActions = SwipeAction.MARK_FAV + "," + SwipeAction.START_DOWNLOAD;
break;
}
return getPrefs(context, tag, defaultActions);
}
public static boolean isSwipeActionEnabled(Context context, String tag) {
SharedPreferences prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
return prefs.getBoolean(KEY_PREFIX_NO_ACTION + tag, true);
}
private boolean isSwipeActionEnabled() {
return isSwipeActionEnabled(fragment.requireContext(), tag);
}
@Override
public boolean onMove(@NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
@NonNull RecyclerView.ViewHolder target) {
return false;
}
@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int swipeDir) {
if (!actions.hasActions()) {
//open settings dialog if no prefs are set
new SwipeActionsDialog(fragment.requireContext(), tag).show(this::reloadPreference);
return;
}
FeedItem item = ((EpisodeItemViewHolder) viewHolder).getFeedItem();
(swipeDir == ItemTouchHelper.RIGHT ? actions.right : actions.left)
.performAction(item, fragment, filter);
}
@Override
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
@NonNull RecyclerView.ViewHolder viewHolder,
float dx, float dy, int actionState, boolean isCurrentlyActive) {
SwipeAction right;
SwipeAction left;
if (actions.hasActions()) {
right = actions.right;
left = actions.left;
} else {
right = left = new ShowFirstSwipeDialogAction();
}
//check if it will be removed
boolean rightWillRemove = right.willRemove(filter);
boolean leftWillRemove = left.willRemove(filter);
boolean wontLeave = (dx > 0 && !rightWillRemove) || (dx < 0 && !leftWillRemove);
//Limit swipe if it's not removed
int maxMovement = recyclerView.getWidth() * 2 / 5;
float sign = dx > 0 ? 1 : -1;
float limitMovement = Math.min(maxMovement, sign * dx);
float displacementPercentage = limitMovement / maxMovement;
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && wontLeave) {
swipeOutEnabled = false;
boolean swipeThresholdReached = displacementPercentage == 1;
// Move slower when getting near the maxMovement
dx = sign * maxMovement * (float) Math.sin((Math.PI / 2) * displacementPercentage);
if (isCurrentlyActive) {
int dir = dx > 0 ? ItemTouchHelper.RIGHT : ItemTouchHelper.LEFT;
swipedOutTo = swipeThresholdReached ? dir : 0;
}
} else {
swipeOutEnabled = true;
}
//add color and icon
Context context = fragment.requireContext();
int themeColor = ThemeUtils.getColorFromAttr(context, android.R.attr.windowBackground);
int actionColor = ThemeUtils.getColorFromAttr(context,
dx > 0 ? right.getActionColor() : left.getActionColor());
RecyclerViewSwipeDecorator.Builder builder = new RecyclerViewSwipeDecorator.Builder(
c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive)
.addSwipeRightActionIcon(right.getActionIcon())
.addSwipeLeftActionIcon(left.getActionIcon())
.addSwipeRightBackgroundColor(ThemeUtils.getColorFromAttr(context, R.attr.background_elevated))
.addSwipeLeftBackgroundColor(ThemeUtils.getColorFromAttr(context, R.attr.background_elevated))
.setActionIconTint(
ColorUtils.blendARGB(themeColor,
actionColor,
Math.max(0.5f, displacementPercentage)));
builder.create().decorate();
super.onChildDraw(c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive);
}
@Override
public float getSwipeEscapeVelocity(float defaultValue) {
return swipeOutEnabled ? defaultValue : Float.MAX_VALUE;
}
@Override
public float getSwipeVelocityThreshold(float defaultValue) {
return swipeOutEnabled ? defaultValue : 0;
}
@Override
public float getSwipeThreshold(@NonNull RecyclerView.ViewHolder viewHolder) {
return swipeOutEnabled ? 0.6f : 1.0f;
}
@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
if (swipedOutTo != 0) {
onSwiped(viewHolder, swipedOutTo);
swipedOutTo = 0;
}
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
if (!isSwipeActionEnabled()) {
return makeMovementFlags(getDragDirs(recyclerView, viewHolder), 0);
} else {
return super.getMovementFlags(recyclerView, viewHolder);
}
}
public void startDrag(EpisodeItemViewHolder holder) {
itemTouchHelper.startDrag(holder);
}
public static class Actions {
public SwipeAction right = null;
public SwipeAction left = null;
public Actions(String prefs) {
String[] actions = prefs.split(",");
if (actions.length == 2) {
this.right = Stream.of(swipeActions)
.filter(a -> a.getId().equals(actions[0])).single();;
this.left = Stream.of(swipeActions)
.filter(a -> a.getId().equals(actions[1])).single();
}
}
public boolean hasActions() {
return right != null && left != null;
}
}
}

View File

@ -6,25 +6,26 @@ import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import com.google.android.material.snackbar.Snackbar;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.google.android.material.snackbar.Snackbar;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.activity.MainActivity;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.core.preferences.GpodnetPreferences;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.sync.SyncService;
import de.danoeh.antennapod.net.sync.model.EpisodeAction;
import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.ShareUtils;
import de.danoeh.antennapod.dialog.ShareDialog;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.net.sync.model.EpisodeAction;
/**
* Handles interactions with the FeedItemMenu.
@ -220,15 +221,16 @@ public class FeedItemMenuHandler {
* Undo is useful for Remove new flag, given there is no UI to undo it otherwise
* ,i.e., there is (context) menu item for add new flag
*/
public static void removeNewFlagWithUndo(@NonNull Fragment fragment, FeedItem item) {
public static void markReadWithUndo(@NonNull Fragment fragment, FeedItem item,
int playState, boolean showSnackbar) {
if (item == null) {
return;
}
Log.d(TAG, "removeNewFlagWithUndo(" + item.getId() + ")");
Log.d(TAG, "markReadWithUndo(" + item.getId() + ")");
// we're marking it as unplayed since the user didn't actually play it
// but they don't want it considered 'NEW' anymore
DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId());
DBWriter.markItemPlayed(playState, item.getId());
final Handler h = new Handler(fragment.requireContext().getMainLooper());
final Runnable r = () -> {
@ -238,15 +240,40 @@ public class FeedItemMenuHandler {
}
};
int playStateStringRes;
switch (playState) {
default:
case FeedItem.UNPLAYED:
if (item.getPlayState() == FeedItem.NEW) {
//was new
playStateStringRes = R.string.removed_new_flag_label;
} else {
//was played
playStateStringRes = R.string.marked_as_unplayed_label;
}
break;
case FeedItem.PLAYED:
playStateStringRes = R.string.marked_as_played_label;
break;
}
Snackbar snackbar = ((MainActivity) fragment.getActivity()).showSnackbarAbovePlayer(
R.string.removed_new_flag_label, Snackbar.LENGTH_LONG)
.setAction(fragment.getString(R.string.undo), v -> {
DBWriter.markItemPlayed(FeedItem.NEW, item.getId());
// don't forget to cancel the thing that's going to remove the media
h.removeCallbacks(r);
});
h.postDelayed(r, (int) Math.ceil(snackbar.getDuration() * 1.05f));
int duration = Snackbar.LENGTH_LONG;
if (showSnackbar) {
((MainActivity) fragment.getActivity()).showSnackbarAbovePlayer(
playStateStringRes, duration)
.setAction(fragment.getString(R.string.undo), v -> {
DBWriter.markItemPlayed(item.getPlayState(), item.getId());
// don't forget to cancel the thing that's going to remove the media
h.removeCallbacks(r);
});
}
h.postDelayed(r, (int) Math.ceil(duration * 1.05f));
}
public static void removeNewFlagWithUndo(@NonNull Fragment fragment, FeedItem item) {
markReadWithUndo(fragment, item, FeedItem.UNPLAYED, false);
}
}

View File

@ -11,6 +11,9 @@ import de.danoeh.antennapod.R;
import de.danoeh.antennapod.core.preferences.UserPreferences;
import de.danoeh.antennapod.core.preferences.UserPreferences.EnqueueLocation;
import de.danoeh.antennapod.core.util.download.AutoUpdateManager;
import de.danoeh.antennapod.fragment.QueueFragment;
import de.danoeh.antennapod.fragment.swipeactions.SwipeAction;
import de.danoeh.antennapod.fragment.swipeactions.SwipeActions;
public class PreferenceUpgrader {
private static final String PREF_CONFIGURED_VERSION = "version_code";
@ -28,12 +31,12 @@ public class PreferenceUpgrader {
AutoUpdateManager.restartUpdateAlarm(context);
CrashReportWriter.getFile().delete();
upgrade(oldVersion);
upgrade(oldVersion, context);
upgraderPrefs.edit().putInt(PREF_CONFIGURED_VERSION, newVersion).apply();
}
}
private static void upgrade(int oldVersion) {
private static void upgrade(int oldVersion, Context context) {
if (oldVersion == -1) {
//New installation
if (UserPreferences.getUsageCountingDateMillis() < 0) {
@ -104,5 +107,10 @@ public class PreferenceUpgrader {
String.valueOf(KeyEvent.KEYCODE_MEDIA_PREVIOUS)).apply();
}
}
if (oldVersion < 2040000) {
SharedPreferences prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE);
prefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + QueueFragment.TAG,
SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE).apply();
}
}
}

View File

@ -4,7 +4,8 @@
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
tools:ignore="MergeRootFrame">
<!--
This parent FrameLayout is necessary because RecyclerView's ItemAnimator changes alpha values,
@ -47,7 +48,8 @@
<CheckBox
android:id="@+id/selectCheckBox"
android:layout_width="60dp"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:visibility="gone" />
</LinearLayout>
@ -238,6 +240,7 @@
</LinearLayout>
<include
android:id="@+id/secondaryActionButton"
layout="@layout/secondary_action" />
</LinearLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/enableSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checked="true"
android:text="@string/enable_swipeactions" />
<include
android:id="@+id/actionLeftContainer"
layout="@layout/swipeactions_row" />
<include
android:id="@+id/actionRightContainer"
layout="@layout/swipeactions_row" />
</LinearLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.gridlayout.widget.GridLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:grid="http://schemas.android.com/apk/res-auto"
android:id="@+id/pickerGridLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
grid:columnCount="2"
grid:alignmentMode="alignBounds" />

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:grid="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:background="?attr/selectableItemBackground"
android:padding="8dp">
<ImageView
android:id="@+id/swipeIcon"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="8dp"
app:srcCompat="@drawable/ic_add" />
<TextView
android:id="@+id/swipeActionLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/add_to_queue_label"
android:textSize="14sp"
android:textAlignment="center"
android:textColor="?android:attr/textColorPrimary" />
</LinearLayout>

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp">
<TextView
android:id="@+id/swipeDirectionLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/swipe_left"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp" />
<TextView
android:id="@+id/swipeActionLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_below="@+id/swipeDirectionLabel"
android:textSize="14sp"
tools:text="@string/add_to_queue_label" />
<Button
android:id="@+id/changeButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:text="@string/change_setting"
style="@style/Widget.MaterialComponents.Button.TextButton" />
</RelativeLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/background_elevated" />
<LinearLayout
android:id="@+id/previewContainer"
android:layout_width="match_parent"
android:layout_height="76dp"
android:gravity="center"
android:foreground="?attr/selectableItemBackground"
android:orientation="horizontal">
<include
android:id="@+id/mockEpisode"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.7"
layout="@layout/feeditemlist_item" />
<ImageView
android:id="@+id/swipeIcon"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="0.3"
android:background="?attr/background_elevated"
android:padding="22dp"
app:srcCompat="@drawable/ic_add" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?attr/background_elevated" />
</LinearLayout>

View File

@ -14,21 +14,21 @@
android:title="@string/download_label"
/>
<item android:id="@+id/mark_unread_batch"
android:icon="@drawable/ic_cancel"
android:icon="@drawable/ic_mark_unplayed"
android:title="@string/mark_unread_label"
/>
<item
android:id="@+id/mark_read_batch"
android:icon="@drawable/ic_check"
android:icon="@drawable/ic_mark_played"
android:title="@string/mark_read_label"
/>
<item android:id="@+id/remove_from_queue_batch"
android:icon="@drawable/ic_remove"
android:icon="@drawable/ic_playlist_remove"
android:title="@string/remove_from_queue_label"
/>
<item
android:id="@+id/add_to_queue_batch"
android:icon="@drawable/ic_add"
android:icon="@drawable/ic_playlist"
android:title="@string/add_to_queue_label"
/>
</menu>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:key="prefSwipeFeed"
android:title="@string/feeds_label"/>
<Preference
android:key="prefSwipeQueue"
android:title="@string/queue_label"/>
</PreferenceScreen>

View File

@ -78,5 +78,9 @@
android:title="@string/pref_back_button_behavior_title"
android:summary="@string/pref_back_button_behavior_sum"
android:defaultValue="default"/>
<Preference
android:key="prefSwipe"
android:summary="@string/swipeactions_summary"
android:title="@string/swipeactions_label"/>
</PreferenceCategory>
</PreferenceScreen>

View File

@ -535,6 +535,14 @@ public class DBWriter {
}
}
public static Future<?> toggleFavoriteItem(final FeedItem item) {
if (item.isTagged(FeedItem.TAG_FAVORITE)) {
return removeFavoriteItem(item);
} else {
return addFavoriteItem(item);
}
}
public static Future<?> addFavoriteItem(final FeedItem item) {
return dbExec.submit(() -> {
final PodDBAdapter adapter = PodDBAdapter.getInstance().open();

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/action_icon_color"
android:pathData="m19.89 12.43-1.9 1.15c-4.13-1.8-8.31 1.81-8 5.15l-4.3 2.74 0.01-18.05zm-5.45 8.68-2.75-3 1.16-1.16 1.59 1.58 3.59-3.58 1.16 1.41-4.75 4.75"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/action_icon_color"
android:pathData="m19.89 12.43-1.9 1.15c-4.13-1.8-8.31 1.81-8 5.15l-4.3 2.74 0.01-18.05m7.46 11.99-0.69 0.69 1.82 1.82-1.82 1.82 0.69 0.69 1.82-1.82 1.81 1.82 0.69-0.69-1.82-1.82 1.82-1.82-0.69-0.69-1.81 1.82z"/>
</vector>

View File

@ -0,0 +1,8 @@
<!-- drawable/playlist_remove.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="?attr/action_icon_color" android:pathData="M2,6V8H14V6H2M2,10V12H11V10H2M14.17,10.76L12.76,12.17L15.59,15L12.76,17.83L14.17,19.24L17,16.41L19.83,19.24L21.24,17.83L18.41,15L21.24,12.17L19.83,10.76L17,13.59L14.17,10.76M2,14V16H11V14H2Z" />
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?attr/action_icon_color" android:pathData="M19,13H5v-2h14v2z"/>
</vector>

View File

@ -12,4 +12,9 @@
<attr name="filter_dialog_clear" format="color"/>
<attr name="filter_dialog_button_background" format="reference"/>
<attr name="seek_background" format="color" />
<attr name="icon_red" format="color" />
<attr name="icon_yellow" format="color" />
<attr name="icon_green" format="color" />
<attr name="icon_purple" format="color" />
<attr name="icon_gray" format="color" />
</resources>

View File

@ -35,6 +35,14 @@
<!-- Google Assistant -->
<string name="app_action_not_found">\"%1$s\" not found</string>
<!-- SwipeActions -->
<string name="swipeactions_label">Swipe Actions</string>
<string name="swipeactions_summary">Choose what happens when swiping an episode in a list</string>
<string name="swipe_right">Swipe Right</string>
<string name="swipe_left">Swipe Left</string>
<string name="enable_swipeactions">Enable Swipe Actions for this Screen</string>
<string name="change_setting">Change</string>
<!-- Statistics fragment -->
<string name="total_time_listened_to_podcasts">Total time of episodes played:</string>
<string name="statistics_details_dialog">%1$d out of %2$d episodes started.\n\nPlayed %3$s out of %4$s.</string>
@ -195,6 +203,8 @@
<string name="remove_new_flag_label">Remove \"new\" flag</string>
<string name="removed_new_flag_label">Removed \"new\" flag</string>
<string name="mark_read_label">Mark as played</string>
<string name="marked_as_played_label">Marked as played</string>
<string name="marked_as_unplayed_label">Marked as unplayed</string>
<string name="mark_read_no_media_label">Mark as read</string>
<string name="play_this_to_seek_position">To jump to positions, you need to play the episode</string>
<plurals name="marked_read_batch_label">

View File

@ -27,6 +27,11 @@
<item name="scrollbar_thumb">@drawable/scrollbar_thumb_light</item>
<item name="filter_dialog_clear">@color/filter_dialog_clear_light</item>
<item name="filter_dialog_button_background">@drawable/filter_dialog_background_light</item>
<item name="icon_red">#CF1800</item>
<item name="icon_yellow">#F59F00</item>
<item name="icon_green">#008537</item>
<item name="icon_purple">#5F1984</item>
<item name="icon_gray">#25365A</item>
</style>
<style name="Theme.AntennaPod.Dark" parent="Theme.Base.AntennaPod.Dark">
@ -56,6 +61,11 @@
<item name="scrollbar_thumb">@drawable/scrollbar_thumb_dark</item>
<item name="filter_dialog_clear">@color/filter_dialog_clear_dark</item>
<item name="filter_dialog_button_background">@drawable/filter_dialog_background_dark</item>
<item name="icon_red">#CF1800</item>
<item name="icon_yellow">#F59F00</item>
<item name="icon_green">#008537</item>
<item name="icon_purple">#AA55D8</item>
<item name="icon_gray">#CDD9E4</item>
</style>
<style name="Theme.AntennaPod.TrueBlack" parent="Theme.Base.AntennaPod.TrueBlack">

View File

@ -248,6 +248,9 @@ public class FeedItem extends FeedComponent implements Serializable {
return state == NEW;
}
public int getPlayState() {
return state;
}
public void setNew() {
state = NEW;
@ -377,6 +380,10 @@ public class FeedItem extends FeedComponent implements Serializable {
return failedAttempts;
}
public boolean isDownloaded() {
return media != null && media.isDownloaded();
}
public boolean isAutoDownloadable() {
if (media == null || media.isDownloaded() || autoDownload == 0) {
return false;