Rewrite include/exclude filter dialog (#6057)

This commit is contained in:
ByteHamster 2022-09-18 22:25:06 +02:00 committed by GitHub
parent bd0f54dbf6
commit 097a491504
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 275 additions and 170 deletions

View File

@ -0,0 +1,57 @@
package de.danoeh.antennapod.adapter;
import android.content.Context;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.chip.Chip;
import de.danoeh.antennapod.R;
import java.util.List;
public abstract class SimpleChipAdapter extends RecyclerView.Adapter<SimpleChipAdapter.ViewHolder> {
private final Context context;
public SimpleChipAdapter(Context context) {
this.context = context;
setHasStableIds(true);
}
protected abstract List<String> getChips();
protected abstract void onRemoveClicked(int position);
@Override
@NonNull
public SimpleChipAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Chip chip = new Chip(context);
chip.setCloseIconVisible(true);
chip.setCloseIconResource(R.drawable.ic_delete);
return new SimpleChipAdapter.ViewHolder(chip);
}
@Override
public void onBindViewHolder(@NonNull SimpleChipAdapter.ViewHolder holder, int position) {
holder.chip.setText(getChips().get(position));
holder.chip.setOnCloseIconClickListener(v -> onRemoveClicked(position));
}
@Override
public int getItemCount() {
return getChips().size();
}
@Override
public long getItemId(int position) {
return getChips().get(position).hashCode();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
Chip chip;
ViewHolder(Chip itemView) {
super(itemView);
chip = itemView;
}
}
}

View File

@ -1,75 +1,108 @@
package de.danoeh.antennapod.dialog;
import android.content.Context;
import android.view.View;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.RadioButton;
import android.content.DialogInterface;
import android.text.TextUtils;
import android.view.LayoutInflater;
import androidx.recyclerview.widget.GridLayoutManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.SimpleChipAdapter;
import de.danoeh.antennapod.databinding.EpisodeFilterDialogBinding;
import de.danoeh.antennapod.model.feed.FeedFilter;
import de.danoeh.antennapod.view.ItemOffsetDecoration;
import java.util.List;
/**
* Displays a dialog with a text box for filtering episodes and two radio buttons for exclusion/inclusion
*/
public abstract class EpisodeFilterDialog extends MaterialAlertDialogBuilder {
private final FeedFilter initialFilter;
private final EpisodeFilterDialogBinding viewBinding;
private final List<String> termList;
public EpisodeFilterDialog(Context context, FeedFilter filter) {
super(context);
initialFilter = filter;
viewBinding = EpisodeFilterDialogBinding.inflate(LayoutInflater.from(context));
setTitle(R.string.episode_filters_label);
View rootView = View.inflate(context, R.layout.episode_filter_dialog, null);
setView(rootView);
setView(viewBinding.getRoot());
final EditText etxtEpisodeFilterText = rootView.findViewById(R.id.etxtEpisodeFilterText);
final EditText etxtEpisodeFilterDurationText = rootView.findViewById(R.id.etxtEpisodeFilterDurationText);
final RadioButton radioInclude = rootView.findViewById(R.id.radio_filter_include);
final RadioButton radioExclude = rootView.findViewById(R.id.radio_filter_exclude);
final CheckBox checkboxDuration = rootView.findViewById(R.id.checkbox_filter_duration);
if (initialFilter.includeOnly()) {
radioInclude.setChecked(true);
etxtEpisodeFilterText.setText(initialFilter.getIncludeFilter());
} else if(initialFilter.excludeOnly()) {
radioExclude.setChecked(true);
etxtEpisodeFilterText.setText(initialFilter.getExcludeFilter());
} else {
radioExclude.setChecked(false);
radioInclude.setChecked(false);
etxtEpisodeFilterText.setText("");
}
if (initialFilter.hasMinimalDurationFilter()) {
checkboxDuration.setChecked(true);
if (filter.hasMinimalDurationFilter()) {
viewBinding.durationCheckBox.setChecked(true);
// Store minimal duration in seconds, show in minutes
etxtEpisodeFilterDurationText.setText(String.valueOf(initialFilter.getMinimalDurationFilter() / 60));
viewBinding.episodeFilterDurationText
.setText(String.valueOf(filter.getMinimalDurationFilter() / 60));
}
if (filter.excludeOnly()) {
termList = filter.getExcludeFilter();
viewBinding.excludeRadio.setChecked(true);
} else {
termList = filter.getIncludeFilter();
viewBinding.includeRadio.setChecked(true);
}
setupWordsList();
setNegativeButton(R.string.cancel_label, null);
setPositiveButton(R.string.confirm_label, (dialog, which) -> {
String includeString = "";
String excludeString = "";
int minimalDuration = -1;
if (radioInclude.isChecked()) {
includeString = etxtEpisodeFilterText.getText().toString();
} else {
excludeString = etxtEpisodeFilterText.getText().toString();
}
if (checkboxDuration.isChecked()) {
try {
// Store minimal duration in seconds
minimalDuration = Integer.parseInt(etxtEpisodeFilterDurationText.getText().toString()) * 60;
} catch (NumberFormatException e) {
// Do not change anything on error
}
}
onConfirmed(new FeedFilter(includeString, excludeString, minimalDuration));
}
);
setPositiveButton(R.string.confirm_label, this::onConfirmClick);
}
private void setupWordsList() {
viewBinding.termsRecycler.setLayoutManager(new GridLayoutManager(getContext(), 2));
viewBinding.termsRecycler.addItemDecoration(new ItemOffsetDecoration(getContext(), 4));
SimpleChipAdapter adapter = new SimpleChipAdapter(getContext()) {
@Override
protected List<String> getChips() {
return termList;
}
@Override
protected void onRemoveClicked(int position) {
termList.remove(position);
notifyDataSetChanged();
}
};
viewBinding.termsRecycler.setAdapter(adapter);
viewBinding.termsTextInput.setEndIconOnClickListener(v -> {
String newWord = viewBinding.termsTextInput.getEditText().getText().toString().replace("\"", "").trim();
if (TextUtils.isEmpty(newWord) || termList.contains(newWord)) {
return;
}
termList.add(newWord);
viewBinding.termsTextInput.getEditText().setText("");
adapter.notifyDataSetChanged();
});
}
protected abstract void onConfirmed(FeedFilter filter);
private void onConfirmClick(DialogInterface dialog, int which) {
int minimalDuration = -1;
if (viewBinding.durationCheckBox.isChecked()) {
try {
// Store minimal duration in seconds
minimalDuration = Integer.parseInt(
viewBinding.episodeFilterDurationText.getText().toString()) * 60;
} catch (NumberFormatException e) {
// Do not change anything on error
}
}
String excludeFilter = "";
String includeFilter = "";
if (viewBinding.includeRadio.isChecked()) {
includeFilter = toFilterString(termList);
} else {
excludeFilter = toFilterString(termList);
}
onConfirmed(new FeedFilter(includeFilter, excludeFilter, minimalDuration));
}
private String toFilterString(List<String> words) {
StringBuilder result = new StringBuilder();
for (String word : words) {
result.append("\"").append(word).append("\" ");
}
return result.toString();
}
}

View File

@ -6,21 +6,19 @@ import android.text.TextUtils;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import androidx.fragment.app.DialogFragment;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.chip.Chip;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.adapter.SimpleChipAdapter;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.NavDrawerData;
import de.danoeh.antennapod.databinding.EditTagsDialogBinding;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.view.ItemOffsetDecoration;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
@ -36,7 +34,7 @@ public class TagSettingsDialog extends DialogFragment {
private static final String ARG_FEED_PREFERENCES = "feed_preferences";
private List<String> displayedTags;
private EditTagsDialogBinding viewBinding;
private TagSelectionAdapter adapter;
private SimpleChipAdapter adapter;
public static TagSettingsDialog newInstance(List<FeedPreferences> preferencesList) {
TagSettingsDialog fragment = new TagSettingsDialog();
@ -62,12 +60,22 @@ public class TagSettingsDialog extends DialogFragment {
viewBinding = EditTagsDialogBinding.inflate(getLayoutInflater());
viewBinding.tagsRecycler.setLayoutManager(new GridLayoutManager(getContext(), 2));
viewBinding.tagsRecycler.addItemDecoration(new ItemOffsetDecoration(getContext(), 4));
adapter = new TagSelectionAdapter();
adapter.setHasStableIds(true);
adapter = new SimpleChipAdapter(getContext()) {
@Override
protected List<String> getChips() {
return displayedTags;
}
@Override
protected void onRemoveClicked(int position) {
displayedTags.remove(position);
notifyDataSetChanged();
}
};
viewBinding.tagsRecycler.setAdapter(adapter);
viewBinding.rootFolderCheckbox.setChecked(commonTags.contains(FeedPreferences.TAG_ROOT));
viewBinding.newTagButton.setOnClickListener(v ->
viewBinding.newTagTextInput.setEndIconOnClickListener(v ->
addTag(viewBinding.newTagEditText.getText().toString().trim()));
loadTags();
@ -140,44 +148,4 @@ public class TagSettingsDialog extends DialogFragment {
DBWriter.setFeedPreferences(preferences);
}
}
public class TagSelectionAdapter extends RecyclerView.Adapter<TagSelectionAdapter.ViewHolder> {
@Override
@NonNull
public TagSelectionAdapter.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Chip chip = new Chip(getContext());
chip.setCloseIconVisible(true);
chip.setCloseIconResource(R.drawable.ic_delete);
return new TagSelectionAdapter.ViewHolder(chip);
}
@Override
public void onBindViewHolder(@NonNull TagSelectionAdapter.ViewHolder holder, int position) {
holder.chip.setText(displayedTags.get(position));
holder.chip.setOnCloseIconClickListener(v -> {
displayedTags.remove(position);
notifyDataSetChanged();
});
}
@Override
public int getItemCount() {
return displayedTags.size();
}
@Override
public long getItemId(int position) {
return displayedTags.get(position).hashCode();
}
public class ViewHolder extends RecyclerView.ViewHolder {
Chip chip;
ViewHolder(Chip itemView) {
super(itemView);
chip = itemView;
}
}
}
}

View File

@ -28,25 +28,24 @@
android:layout_height="wrap_content"
android:text="@string/feed_folders_include_root" />
<LinearLayout
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/newTagTextInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
app:endIconMode="custom"
app:endIconDrawable="@drawable/ic_add"
app:endIconContentDescription="@string/add_tag"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<AutoCompleteTextView
android:id="@+id/newTagEditText"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="16dp"
android:inputType="text"
android:ems="10" />
<ImageButton
android:id="@+id/newTagButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_add" />
</LinearLayout>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@ -1,60 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<ScrollView
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="match_parent"
android:orientation="vertical"
android:padding="16dp">
<RadioGroup
android:id="@+id/radio_filter_group"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<RadioButton
android:id="@+id/radio_filter_include"
android:layout_width="wrap_content"
<RadioGroup
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/episode_filters_include" />
android:layout_marginBottom="8dp"
android:orientation="horizontal">
<RadioButton
android:id="@+id/radio_filter_exclude"
android:layout_width="wrap_content"
<RadioButton
android:id="@+id/includeRadio"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/include_terms" />
<RadioButton
android:id="@+id/excludeRadio"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/exclude_terms" />
</RadioGroup>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/termsRecycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/episode_filters_exclude" />
tools:itemCount="2" />
</RadioGroup>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/termsTextInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:endIconMode="custom"
app:endIconDrawable="@drawable/ic_add"
app:endIconContentDescription="@string/add_term"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<EditText
android:id="@+id/etxtEpisodeFilterText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:cursorVisible="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:hint="@string/episode_filters_hint"
android:lines="8"
android:maxLines="20"
android:minLines="1"
android:scrollbars="vertical" />
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="text"
android:singleLine="true" />
<CheckBox
android:id="@+id/checkbox_filter_duration"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/episode_filters_duration" />
</com.google.android.material.textfield.TextInputLayout>
<EditText
android:id="@+id/etxtEpisodeFilterDurationText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:cursorVisible="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:inputType="numberSigned"
android:lines="1" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:background="?android:attr/listDivider" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<CheckBox
android:id="@+id/durationCheckBox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/exclude_episodes_shorter_than" />
<EditText
android:id="@+id/episodeFilterDurationText"
android:layout_width="50dp"
android:layout_height="wrap_content"
android:cursorVisible="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:inputType="numberSigned"
android:lines="1" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/time_minutes" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -21,8 +21,8 @@ public class FeedFilterTest {
assertFalse(filter.excludeOnly());
assertFalse(filter.includeOnly());
assertEquals("", filter.getExcludeFilter());
assertEquals("", filter.getIncludeFilter());
assertEquals("", filter.getExcludeFilterRaw());
assertEquals("", filter.getIncludeFilterRaw());
assertTrue(filter.shouldAutoDownload(item));
}
@ -38,8 +38,8 @@ public class FeedFilterTest {
assertFalse(filter.excludeOnly());
assertTrue(filter.includeOnly());
assertEquals("", filter.getExcludeFilter());
assertEquals(includeFilter, filter.getIncludeFilter());
assertEquals("", filter.getExcludeFilterRaw());
assertEquals(includeFilter, filter.getIncludeFilterRaw());
assertTrue(filter.shouldAutoDownload(item));
assertFalse(filter.shouldAutoDownload(item2));
}
@ -56,8 +56,8 @@ public class FeedFilterTest {
assertTrue(filter.excludeOnly());
assertFalse(filter.includeOnly());
assertEquals(excludeFilter, filter.getExcludeFilter());
assertEquals("", filter.getIncludeFilter());
assertEquals(excludeFilter, filter.getExcludeFilterRaw());
assertEquals("", filter.getIncludeFilterRaw());
assertFalse(filter.shouldAutoDownload(item));
assertTrue(filter.shouldAutoDownload(item2));
}
@ -77,8 +77,8 @@ public class FeedFilterTest {
assertFalse(filter.excludeOnly());
assertTrue(filter.includeOnly());
assertEquals("", filter.getExcludeFilter());
assertEquals(includeFilter, filter.getIncludeFilter());
assertEquals("", filter.getExcludeFilterRaw());
assertEquals(includeFilter, filter.getIncludeFilterRaw());
assertTrue(filter.shouldAutoDownload(item));
assertFalse(filter.shouldAutoDownload(item2));
assertTrue(filter.shouldAutoDownload(item3));
@ -99,8 +99,8 @@ public class FeedFilterTest {
assertTrue(filter.excludeOnly());
assertFalse(filter.includeOnly());
assertEquals(excludeFilter, filter.getExcludeFilter());
assertEquals("", filter.getIncludeFilter());
assertEquals(excludeFilter, filter.getExcludeFilterRaw());
assertEquals("", filter.getIncludeFilterRaw());
assertFalse(filter.shouldAutoDownload(item));
assertTrue(filter.shouldAutoDownload(item2));
assertFalse(filter.shouldAutoDownload(item3));

View File

@ -102,14 +102,22 @@ public class FeedFilter implements Serializable {
return false;
}
public String getIncludeFilter() {
public String getIncludeFilterRaw() {
return includeFilter;
}
public String getExcludeFilter() {
public String getExcludeFilterRaw() {
return excludeFilter;
}
public List<String> getIncludeFilter() {
return includeFilter == null ? new ArrayList<>() : parseTerms(includeFilter);
}
public List<String> getExcludeFilter() {
return excludeFilter == null ? new ArrayList<>() : parseTerms(excludeFilter);
}
public int getMinimalDurationFilter() {
return minimalDuration;
}

View File

@ -448,8 +448,8 @@ public class PodDBAdapter {
values.put(KEY_FEED_VOLUME_ADAPTION, prefs.getVolumeAdaptionSetting().toInteger());
values.put(KEY_USERNAME, prefs.getUsername());
values.put(KEY_PASSWORD, prefs.getPassword());
values.put(KEY_INCLUDE_FILTER, prefs.getFilter().getIncludeFilter());
values.put(KEY_EXCLUDE_FILTER, prefs.getFilter().getExcludeFilter());
values.put(KEY_INCLUDE_FILTER, prefs.getFilter().getIncludeFilterRaw());
values.put(KEY_EXCLUDE_FILTER, prefs.getFilter().getExcludeFilterRaw());
values.put(KEY_MINIMAL_DURATION_FILTER, prefs.getFilter().getMinimalDurationFilter());
values.put(KEY_FEED_PLAYBACK_SPEED, prefs.getFeedPlaybackSpeed());
values.put(KEY_FEED_TAGS, prefs.getTagsAsString());

View File

@ -190,6 +190,7 @@
<item quantity="other">%d subscriptions updated.</item>
</plurals>
<string name="edit_tags">Edit tags</string>
<string name="add_tag">Add tag</string>
<string name="rename_tag_label">Rename tag</string>
<string name="confirm_mobile_feed_refresh_dialog_message">Refreshing podcasts over mobile data connection is disabled in the settings.\n\nDo you want to refresh anyway?</string>
@ -690,10 +691,10 @@
<string name="auto_download_settings_label">Auto Download Settings</string>
<string name="episode_filters_label">Episode Filter</string>
<string name="episode_filters_description">List of terms used to decide if an episode should be included or excluded when auto downloading</string>
<string name="episode_filters_include">Include</string>
<string name="episode_filters_exclude">Exclude</string>
<string name="episode_filters_duration">Minimal Duration (in minutes)</string>
<string name="episode_filters_hint">Single words \n\"Multiple Words\"</string>
<string name="add_term">Add Term</string>
<string name="exclude_terms">Exclude episodes containing any of the terms below</string>
<string name="include_terms">Include only episodes containing any of the terms below</string>
<string name="exclude_episodes_shorter_than">Exclude episodes shorter than</string>
<string name="keep_updated">Keep Updated</string>
<string name="keep_updated_summary">Include this podcast when (auto-)refreshing all podcasts</string>
<string name="auto_download_disabled_globally">Auto download is disabled in the main AntennaPod settings</string>