diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogFragment.java new file mode 100644 index 000000000..15d12cba9 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogFragment.java @@ -0,0 +1,55 @@ +// Created by evermind-zz 2022, licensed GNU GPL version 3 or later + +package org.schabi.newpipe.fragments.list.search.filter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.schabi.newpipe.databinding.SearchFilterDialogFragmentBinding; +import org.schabi.newpipe.extractor.StreamingService; + +import java.util.ArrayList; + +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.DialogFragment; + +/** + * A search filter dialog that also looks like a dialog aka. 'dialog style'. + */ +public class SearchFilterDialogFragment extends BaseSearchFilterDialogFragment { + + private SearchFilterDialogFragmentBinding binding; + + public static DialogFragment newInstance( + final int serviceId, + final ArrayList userSelectedContentFilter, + final ArrayList userSelectedSortFilter) { + return initDialogArguments( + new SearchFilterDialogFragment(), + serviceId, + userSelectedContentFilter, + userSelectedSortFilter); + } + + @Override + protected BaseSearchFilterUiGenerator createSearchFilterDialogGenerator( + final StreamingService service, + final SearchFilterLogic.Callback callback) { + return new SearchFilterDialogGenerator(service, + binding.verticalScroll, requireContext(), callback); + } + + @Override + protected Toolbar getToolbar() { + return binding.toolbarLayout.toolbar; + } + + @Override + protected View getRootView(final LayoutInflater inflater, + final ViewGroup container) { + binding = SearchFilterDialogFragmentBinding + .inflate(inflater, container, false); + return binding.getRoot(); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogGenerator.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogGenerator.java new file mode 100644 index 000000000..475444e1a --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogGenerator.java @@ -0,0 +1,272 @@ +// Created by evermind-zz 2022, licensed GNU GPL version 3 or later + +package org.schabi.newpipe.fragments.list.search.filter; + +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.GridLayout; +import android.widget.Spinner; +import android.widget.TextView; + +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.search.filter.FilterGroup; +import org.schabi.newpipe.extractor.search.filter.FilterItem; +import org.schabi.newpipe.util.DeviceUtils; +import org.schabi.newpipe.util.ServiceHelper; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import androidx.appcompat.view.ContextThemeWrapper; + +public class SearchFilterDialogGenerator extends BaseSearchFilterUiDialogGenerator { + private final GridLayout globalLayout; + private final Map spinners = new HashMap<>(); + + public SearchFilterDialogGenerator(final StreamingService service, + final ViewGroup root, + final Context context, + final SearchFilterLogic.Callback callback) { + super(service.getSearchQHFactory(), callback, context); + this.globalLayout = createGridLayout(); + root.addView(globalLayout); + } + + @Override + public void onResume() { + for (final Map.Entry spinner + : spinners.entrySet()) { + spinner.getKey().setOnItemSelectedListener(spinner.getValue()); + } + super.onResume(); + } + + @Override + public void onPause() { + for (final Map.Entry spinner + : spinners.entrySet()) { + spinner.getKey().setOnItemSelectedListener(null); + } + super.onPause(); + } + + @Override + protected void createTitle(final String name, + final List titleViewElements) { + final TextView titleView = createTitleText(name); + final View separatorLine = createSeparatorLine(); + final View separatorLine2 = createSeparatorLine(); + + globalLayout.addView(separatorLine); + globalLayout.addView(titleView); + globalLayout.addView(separatorLine2); + + titleViewElements.add(titleView); + titleViewElements.add(separatorLine); + titleViewElements.add(separatorLine2); + } + + @Override + protected void createFilterGroup(final FilterGroup filterGroup, + final UiWrapperMapDelegate wrapperDelegate, + final UiSelectorDelegate selectorDelegate) { + final GridLayout.LayoutParams layoutParams = getLayoutParamsViews(); + boolean doSpanDataOverMultipleCells = false; + final UiItemWrapperViews viewsWrapper = new UiItemWrapperViews( + filterGroup.getIdentifier()); + + if (filterGroup.getName() != null) { + final TextView filterLabel = new TextView(context); + + filterLabel.setId(filterGroup.getIdentifier()); + filterLabel.setText( + ServiceHelper.getTranslatedFilterString(filterGroup.getName(), context)); + filterLabel.setGravity(Gravity.CENTER_VERTICAL); + setDefaultMargin(layoutParams); + setZeroPadding(filterLabel); + + filterLabel.setLayoutParams(layoutParams); + globalLayout.addView(filterLabel); + viewsWrapper.add(filterLabel); + } else { + doSpanDataOverMultipleCells = true; + } + + if (filterGroup.isOnlyOneCheckable()) { + + final Spinner filterDataSpinner = new Spinner(context, Spinner.MODE_DROPDOWN); + + final GridLayout.LayoutParams spinnerLp = + clipFreeRightColumnLayoutParams(doSpanDataOverMultipleCells); + setDefaultMargin(spinnerLp); + filterDataSpinner.setLayoutParams(spinnerLp); + setZeroPadding(filterDataSpinner); + + createUiElementsForSingleSelectableItemsFilterGroup( + filterGroup, wrapperDelegate, selectorDelegate, filterDataSpinner); + + viewsWrapper.add(filterDataSpinner); + globalLayout.addView(filterDataSpinner); + + } else { // multiple items in FilterGroup selectable + final ChipGroup chipGroup = new ChipGroup(context); + viewsWrapper.add(chipGroup); + globalLayout.addView(chipGroup); + chipGroup.setLayoutParams( + clipFreeRightColumnLayoutParams(doSpanDataOverMultipleCells)); + chipGroup.setSingleLine(false); + + createUiElementsForMultipleSelectableItemsFilterGroup( + filterGroup, wrapperDelegate, selectorDelegate, chipGroup); + } + + wrapperDelegate.put(filterGroup.getIdentifier(), viewsWrapper); + } + + private void createUiElementsForSingleSelectableItemsFilterGroup( + final FilterGroup filterGroup, + final UiWrapperMapDelegate wrapperDelegate, + final UiSelectorDelegate selectorDelegate, + final Spinner filterDataSpinner) { + filterDataSpinner.setAdapter(new SearchFilterDialogSpinnerAdapter( + context, filterGroup, wrapperDelegate, filterDataSpinner)); + + final AdapterView.OnItemSelectedListener listener; + listener = new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(final AdapterView parent, final View view, + final int position, final long id) { + if (view != null) { + selectorDelegate.selectFilter(view.getId()); + } + } + + @Override + public void onNothingSelected(final AdapterView parent) { + // we are only interested onItemSelected() -> no implementation here + } + }; + + filterDataSpinner.setOnItemSelectedListener(listener); + spinners.put(filterDataSpinner, listener); + } + + private void createUiElementsForMultipleSelectableItemsFilterGroup( + final FilterGroup filterGroup, + final UiWrapperMapDelegate wrapperDelegate, + final UiSelectorDelegate selectorDelegate, + final ChipGroup chipGroup) { + for (final FilterItem item : filterGroup.getFilterItems()) { + final Chip chip = new Chip(new ContextThemeWrapper( + context, R.style.Theme_MaterialComponents_Light)); + chip.setText(ServiceHelper.getTranslatedFilterString(item.getName(), context)); + chip.setId(item.getIdentifier()); + chip.setCheckable(true); + final View.OnClickListener listener; + listener = view -> selectorDelegate.selectFilter(view.getId()); + + chip.setOnClickListener(listener); + chipGroup.addView(chip); + viewListeners.put(chip, listener); + wrapperDelegate.put(item.getIdentifier(), new UiItemWrapperChip( + item, chip, chipGroup)); + } + } + + private View createSeparatorLine() { + return createSeparatorLine(clipFreeRightColumnLayoutParams(true)); + } + + private TextView createTitleText(final String name) { + final TextView title = createTitleText(name, + clipFreeRightColumnLayoutParams(true)); + title.setGravity(Gravity.CENTER); + return title; + } + + private GridLayout createGridLayout() { + final GridLayout layout = new GridLayout(context); + + layout.setColumnCount(2); + + final GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + setDefaultMargin(layoutParams); + layout.setLayoutParams(layoutParams); + + return layout; + } + + private GridLayout.LayoutParams clipFreeRightColumnLayoutParams(final boolean doColumnSpan) { + final GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(); + // https://stackoverflow.com/questions/37744672/gridlayout-children-are-being-clipped + layoutParams.width = 0; + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.setGravity(Gravity.FILL_HORIZONTAL | Gravity.CENTER_VERTICAL); + setDefaultMargin(layoutParams); + + if (doColumnSpan) { + layoutParams.columnSpec = GridLayout.spec(0, 2, 1.0f); + } + + return layoutParams; + } + + private GridLayout.LayoutParams getLayoutParamsViews() { + final GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams(); + layoutParams.setGravity(Gravity.TOP); + setDefaultMargin(layoutParams); + return layoutParams; + } + + private GridLayout.LayoutParams setDefaultMargin(final GridLayout.LayoutParams layoutParams) { + layoutParams.setMargins( + DeviceUtils.dpToPx(4, context), + DeviceUtils.dpToPx(2, context), + DeviceUtils.dpToPx(4, context), + DeviceUtils.dpToPx(2, context) + ); + return layoutParams; + } + + private View setZeroPadding(final View view) { + view.setPadding(0, 0, 0, 0); + return view; + } + + public static class UiItemWrapperChip extends BaseUiItemWrapper { + + private final ChipGroup chipGroup; + + public UiItemWrapperChip(final FilterItem item, + final View view, + final ChipGroup chipGroup) { + super(item, view); + this.chipGroup = chipGroup; + } + + @Override + public boolean isChecked() { + return ((Chip) view).isChecked(); + } + + @Override + public void setChecked(final boolean checked) { + view.setSelected(checked); + ((Chip) view).setChecked(checked); + + if (checked) { + chipGroup.check(view.getId()); + } + } + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogSpinnerAdapter.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogSpinnerAdapter.java new file mode 100644 index 000000000..be56776c6 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterDialogSpinnerAdapter.java @@ -0,0 +1,218 @@ +// Created by evermind-zz 2022, licensed GNU GPL version 3 or later + +package org.schabi.newpipe.fragments.list.search.filter; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Spinner; +import android.widget.TextView; + +import org.schabi.newpipe.extractor.search.filter.FilterContainer; +import org.schabi.newpipe.extractor.search.filter.FilterGroup; +import org.schabi.newpipe.extractor.search.filter.FilterItem; +import org.schabi.newpipe.util.DeviceUtils; +import org.schabi.newpipe.util.ServiceHelper; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + + +public class SearchFilterDialogSpinnerAdapter extends BaseAdapter { + + private final Context context; + private final FilterGroup group; + private final BaseSearchFilterUiGenerator.UiWrapperMapDelegate wrapperDelegate; + private final Spinner spinner; + private final Map id2PosMap = new HashMap<>(); + private final Map + viewWrapperMap = new HashMap<>(); + + public SearchFilterDialogSpinnerAdapter( + final Context context, + final FilterGroup group, + final BaseSearchFilterUiGenerator.UiWrapperMapDelegate wrapperDelegate, + final Spinner filterDataSpinner) { + this.context = context; + this.group = group; + this.wrapperDelegate = wrapperDelegate; + this.spinner = filterDataSpinner; + + createViewWrappers(); + } + + @Override + public int getCount() { + return group.getFilterItems().length; + } + + @Override + public Object getItem(final int position) { + return group.getFilterItems()[position]; + } + + @Override + public long getItemId(final int position) { + return position; + } + + @Override + public View getView(final int position, final View convertView, final ViewGroup parent) { + final FilterItem item = group.getFilterItems()[position]; + final TextView view; + + if (convertView != null) { + view = (TextView) convertView; + } else { + view = createViewItem(); + } + + initViewWithData(position, item, view); + return view; + } + + @SuppressLint("WrongConstant") + private void initViewWithData(final int position, + final FilterItem item, + final TextView view) { + final UiItemWrapperSpinner wrappedView = + viewWrapperMap.get(position); + Objects.requireNonNull(wrappedView); + + view.setId(item.getIdentifier()); + view.setText(ServiceHelper.getTranslatedFilterString(item.getName(), context)); + view.setVisibility(wrappedView.getVisibility()); + view.setEnabled(wrappedView.isEnabled()); + + if (item instanceof FilterItem.DividerItem) { + wrappedView.setEnabled(false); + view.setEnabled(wrappedView.isEnabled()); + final String menuDividerTitle = ">>>" + + ServiceHelper.getTranslatedFilterString(item.getName(), context) + "<<<"; + view.setText(menuDividerTitle); + } + } + + private void createViewWrappers() { + int position = 0; + for (final FilterItem item : this.group.getFilterItems()) { + final int initialVisibility = View.VISIBLE; + final boolean isInitialEnabled = true; + + final UiItemWrapperSpinner wrappedView = + new UiItemWrapperSpinner( + item, + initialVisibility, + isInitialEnabled, + spinner); + + if (item instanceof FilterItem.DividerItem) { + wrappedView.setEnabled(false); + } + + // store wrapper also locally as we refer here regularly + viewWrapperMap.put(position, wrappedView); + // store wrapper globally in SearchFilterLogic + wrapperDelegate.put(item.getIdentifier(), wrappedView); + id2PosMap.put(item.getIdentifier(), position); + position++; + } + } + + private TextView createViewItem() { + final TextView view = new TextView(context); + view.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + view.setGravity(Gravity.CENTER_VERTICAL); + view.setPadding( + DeviceUtils.dpToPx(8, context), + DeviceUtils.dpToPx(4, context), + DeviceUtils.dpToPx(8, context), + DeviceUtils.dpToPx(4, context) + ); + return view; + } + + public int getItemPositionForFilterId(final int id) { + return id2PosMap.get(id); + } + + @Override + public boolean isEnabled(final int position) { + final UiItemWrapperSpinner wrappedView = + viewWrapperMap.get(position); + Objects.nonNull(wrappedView); + return wrappedView.isEnabled(); + } + + private static class UiItemWrapperSpinner + extends BaseItemWrapper { + private final Spinner spinner; + + /** + * We have to store the visibility of the view and if it is enabled. + *

+ * Reason: the Spinner adapter reuses {@link View} elements through the parameter + * convertView in {@link SearchFilterDialogSpinnerAdapter#getView(int, View, ViewGroup)} + * -> this is the Android Adapter's time saving characteristic to rather reuse + * than to recreate a {@link View}. + * -> so we reuse what Android gives us in above mentioned method. + */ + private int visibility; + private boolean enabled; + + UiItemWrapperSpinner(final FilterItem item, + final int initialVisibility, + final boolean isInitialEnabled, + final Spinner spinner) { + super(item); + this.spinner = spinner; + + this.visibility = initialVisibility; + this.enabled = isInitialEnabled; + } + + @Override + public void setVisible(final boolean visible) { + if (visible) { + visibility = View.VISIBLE; + } else { + visibility = View.GONE; + } + } + + @Override + public boolean isChecked() { + return spinner.getSelectedItem() == item; + } + + @Override + public void setChecked(final boolean checked) { + if (super.getItemId() != FilterContainer.ITEM_IDENTIFIER_UNKNOWN) { + final SearchFilterDialogSpinnerAdapter adapter = + (SearchFilterDialogSpinnerAdapter) spinner.getAdapter(); + spinner.setSelection(adapter.getItemPositionForFilterId(super.getItemId())); + } + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + } + + public int getVisibility() { + return visibility; + } + + public void setVisibility(final int visibility) { + this.visibility = visibility; + } + } +} diff --git a/app/src/main/res/layout/search_filter_dialog_fragment.xml b/app/src/main/res/layout/search_filter_dialog_fragment.xml new file mode 100644 index 000000000..0747e8e36 --- /dev/null +++ b/app/src/main/res/layout/search_filter_dialog_fragment.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_search_filter_dialog_fragment.xml b/app/src/main/res/menu/menu_search_filter_dialog_fragment.xml new file mode 100644 index 000000000..7f0ec2009 --- /dev/null +++ b/app/src/main/res/menu/menu_search_filter_dialog_fragment.xml @@ -0,0 +1,15 @@ +

+ + + + +