diff options
author | ueen <ueli.sarnighausen@online.de> | 2021-07-12 00:03:32 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-12 00:03:32 +0200 |
commit | ca9ad0d2d33ce8c4299525e32664da33e28b9d91 (patch) | |
tree | 8938c065f47726b6bb29e5ef83cf13d95144fb18 | |
parent | c1efd51be976e289dab59cd46b79bab9c79fcce6 (diff) | |
download | AntennaPod-ca9ad0d2d33ce8c4299525e32664da33e28b9d91.zip |
Swipe actions (#5191)
39 files changed, 1296 insertions, 152 deletions
diff --git a/app/build.gradle b/app/build.gradle index 34fd1dd5d..c54699c4b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' diff --git a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java index 3b500506c..4a37ec25f 100644 --- a/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/app/src/main/java/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -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; } diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java index 0e238eae2..6055582a3 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/QueueRecyclerAdapter.java @@ -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; }; diff --git a/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java b/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java index 670357d16..43f749ff3 100644 --- a/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java +++ b/app/src/main/java/de/danoeh/antennapod/adapter/SelectableAdapter.java @@ -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(); } } diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java new file mode 100644 index 000000000..53c2697ce --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SwipeActionsDialog.java @@ -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(); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java index b166bbd96..f02c1a6ac 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/CompletedDownloadsFragment.java @@ -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; } @@ -259,6 +255,11 @@ public class CompletedDownloadsFragment extends Fragment implements } @Override + public void onStartSelectMode() { + speedDialView.setVisibility(View.VISIBLE); + } + + @Override public void onEndSelectMode() { speedDialView.close(); speedDialView.setVisibility(View.GONE); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java index 1eb561979..a3b58fc07 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/FeedItemlistFragment.java @@ -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()) diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java index b3c3d8567..31681252a 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/QueueFragment.java @@ -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); } @@ -649,9 +570,90 @@ public class QueueFragment extends Fragment implements Toolbar.OnMenuItemClickLi } @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); + } + } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java index baf4c7c57..cc09acbca 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/MainPreferencesFragment.java @@ -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)); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java new file mode 100644 index 000000000..3d9709f74 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/SwipePreferencesFragment.java @@ -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); + } + +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java index 4d1b79965..7c79d0962 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/preferences/UserInterfacePreferencesFragment.java @@ -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); diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java new file mode 100644 index 000000000..514ba9764 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/AddToQueueSwipeAction.java @@ -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; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java new file mode 100644 index 000000000..2458657a0 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkFavoriteSwipeAction.java @@ -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; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkPlayedSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkPlayedSwipeAction.java new file mode 100644 index 000000000..b820d8a65 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/MarkPlayedSwipeAction.java @@ -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; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromInboxSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromInboxSwipeAction.java new file mode 100644 index 000000000..9852269fb --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromInboxSwipeAction.java @@ -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; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java new file mode 100644 index 000000000..87cf97f56 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/RemoveFromQueueSwipeAction.java @@ -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; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/ShowFirstSwipeDialogAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/ShowFirstSwipeDialogAction.java new file mode 100644 index 000000000..7d626134d --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/ShowFirstSwipeDialogAction.java @@ -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; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/StartDownloadSwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/StartDownloadSwipeAction.java new file mode 100644 index 000000000..2c0110822 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/StartDownloadSwipeAction.java @@ -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; + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeAction.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeAction.java new file mode 100644 index 000000000..e6d002b2b --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeAction.java @@ -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); +} diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java new file mode 100644 index 000000000..b8b39c578 --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/fragment/swipeactions/SwipeActions.java @@ -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; + } + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java index a9747e987..e542f94d2 100644 --- a/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java +++ b/app/src/main/java/de/danoeh/antennapod/menuhandler/FeedItemMenuHandler.java @@ -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; + } + + int duration = Snackbar.LENGTH_LONG; - 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)); + 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); } } diff --git a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java index 03a8edbf0..84c738632 100644 --- a/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java +++ b/app/src/main/java/de/danoeh/antennapod/preferences/PreferenceUpgrader.java @@ -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(); + } } } diff --git a/app/src/main/res/layout/feeditemlist_item.xml b/app/src/main/res/layout/feeditemlist_item.xml index 0202e0e34..b876f079d 100644 --- a/app/src/main/res/layout/feeditemlist_item.xml +++ b/app/src/main/res/layout/feeditemlist_item.xml @@ -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> diff --git a/app/src/main/res/layout/swipeactions_dialog.xml b/app/src/main/res/layout/swipeactions_dialog.xml new file mode 100644 index 000000000..a1f0b7ae6 --- /dev/null +++ b/app/src/main/res/layout/swipeactions_dialog.xml @@ -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> diff --git a/app/src/main/res/layout/swipeactions_picker.xml b/app/src/main/res/layout/swipeactions_picker.xml new file mode 100644 index 000000000..e473888b2 --- /dev/null +++ b/app/src/main/res/layout/swipeactions_picker.xml @@ -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" /> diff --git a/app/src/main/res/layout/swipeactions_picker_item.xml b/app/src/main/res/layout/swipeactions_picker_item.xml new file mode 100644 index 000000000..b497efb14 --- /dev/null +++ b/app/src/main/res/layout/swipeactions_picker_item.xml @@ -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> diff --git a/app/src/main/res/layout/swipeactions_row.xml b/app/src/main/res/layout/swipeactions_row.xml new file mode 100644 index 000000000..df55d3f89 --- /dev/null +++ b/app/src/main/res/layout/swipeactions_row.xml @@ -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> diff --git a/app/src/main/res/menu/episodes_apply_action_speeddial.xml b/app/src/main/res/menu/episodes_apply_action_speeddial.xml index a2f509ec5..c9bc4b4df 100644 --- a/app/src/main/res/menu/episodes_apply_action_speeddial.xml +++ b/app/src/main/res/menu/episodes_apply_action_speeddial.xml @@ -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> diff --git a/app/src/main/res/xml/preferences_swipe.xml b/app/src/main/res/xml/preferences_swipe.xml new file mode 100644 index 000000000..eb238ac14 --- /dev/null +++ b/app/src/main/res/xml/preferences_swipe.xml @@ -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> diff --git a/app/src/main/res/xml/preferences_user_interface.xml b/app/src/main/res/xml/preferences_user_interface.xml index f8e80cdff..0b2707a18 100644 --- a/app/src/main/res/xml/preferences_user_interface.xml +++ b/app/src/main/res/xml/preferences_user_interface.xml @@ -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> diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index 8517b73a2..9e60f4a4d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -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(); diff --git a/core/src/main/res/drawable/ic_mark_played.xml b/core/src/main/res/drawable/ic_mark_played.xml new file mode 100644 index 000000000..dc0a2bb58 --- /dev/null +++ b/core/src/main/res/drawable/ic_mark_played.xml @@ -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> diff --git a/core/src/main/res/drawable/ic_mark_unplayed.xml b/core/src/main/res/drawable/ic_mark_unplayed.xml new file mode 100644 index 000000000..60b07e97f --- /dev/null +++ b/core/src/main/res/drawable/ic_mark_unplayed.xml @@ -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> diff --git a/core/src/main/res/drawable/ic_playlist_remove.xml b/core/src/main/res/drawable/ic_playlist_remove.xml new file mode 100644 index 000000000..940744924 --- /dev/null +++ b/core/src/main/res/drawable/ic_playlist_remove.xml @@ -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>
\ No newline at end of file diff --git a/core/src/main/res/drawable/ic_remove.xml b/core/src/main/res/drawable/ic_remove.xml deleted file mode 100644 index da2ea9f1a..000000000 --- a/core/src/main/res/drawable/ic_remove.xml +++ /dev/null @@ -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> diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml index baab3be7a..596b1cca2 100644 --- a/core/src/main/res/values/attrs.xml +++ b/core/src/main/res/values/attrs.xml @@ -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> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 442f95832..578af2900 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -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"> diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml index 2f287155f..33ff270b8 100644 --- a/core/src/main/res/values/styles.xml +++ b/core/src/main/res/values/styles.xml @@ -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"> diff --git a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java index 235c3fb8a..460f50f88 100644 --- a/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java +++ b/model/src/main/java/de/danoeh/antennapod/model/feed/FeedItem.java @@ -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; |