diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/BaseSearchFilterUiGenerator.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/BaseSearchFilterUiGenerator.java new file mode 100644 index 000000000..a2aed2f57 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/BaseSearchFilterUiGenerator.java @@ -0,0 +1,98 @@ +// 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.util.DisplayMetrics; +import android.util.TypedValue; + +import org.schabi.newpipe.R; +import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory; + +import static android.util.TypedValue.COMPLEX_UNIT_DIP; + +/** + * Base for any search filter UI. + *
+ * It extends SearchFilterLogic and is used as a base class to implement the UI interface + * for content and sort filter dialogs eg. {@link SearchFilterDialogGenerator} + * or {@link SearchFilterOptionMenuAlikeDialogGenerator}. + */ +public abstract class BaseSearchFilterUiGenerator extends SearchFilterLogic { + protected final ICreateUiForFiltersWorker contentFilterWorker; + protected final ICreateUiForFiltersWorker sortFilterWorker; + protected final Context context; + + protected BaseSearchFilterUiGenerator(final SearchQueryHandlerFactory linkHandlerFactory, + final Callback callback, + final Context context) { + super(linkHandlerFactory, callback); + this.context = context; + this.contentFilterWorker = createContentFilterWorker(); + this.sortFilterWorker = createSortFilterWorker(); + } + + // common helpers + public static float dipToPixels(final Context ctx, final float dipValue) { + final DisplayMetrics metrics = ctx.getResources().getDisplayMetrics(); + return TypedValue.applyDimension(COMPLEX_UNIT_DIP, dipValue, metrics); + } + + /** + * {@link ICreateUiForFiltersWorker}. + * + * @return the class that implements the UI for the content filters. + */ + protected abstract ICreateUiForFiltersWorker createContentFilterWorker(); + + /** + * {@link ICreateUiForFiltersWorker}. + * + * @return the class that implements the UI for the sort filters. + */ + protected abstract ICreateUiForFiltersWorker createSortFilterWorker(); + + protected int getSeparatorLineColorFromTheme() { + final TypedValue value = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.colorAccent, value, true); + return value.data; + } + + /** + * Create the complete UI for the search filter dialog and make sure the initial + * visibility of the UI elements is done. + */ + public void createSearchUI() { + initContentFiltersUi(contentFilterWorker); + initSortFiltersUi(sortFilterWorker); + // make sure that only sort filters relevant to the selected content filter are shown + showSortFilterContainerUI(); + } + + /** + * If UI is implemented within an fragment/activity this method has to be called from + * its corresponding lifecyle method manually. + */ + public abstract void onResume(); + + /** + * If UI is implemented within an fragment/activity this method has to be called from + * its corresponding lifecyle method manually. + */ + public abstract void onPause(); + + /** + * Helper interface used as 'function pointer'. + */ + protected interface UiWrapperMapDelegate { + void put(int identifier, IUiItemWrapper menuItemUiWrapper); + } + + /** + * Helper interface used as 'function pointer'. + */ + protected interface UiSelectorDelegate { + void selectFilter(int identifier); + } +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterLogic.java b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterLogic.java new file mode 100644 index 000000000..e0a476601 --- /dev/null +++ b/app/src/main/java/org/schabi/newpipe/fragments/list/search/filter/SearchFilterLogic.java @@ -0,0 +1,752 @@ +// Created by evermind-zz 2022, licensed GNU GPL version 3 or later + +package org.schabi.newpipe.fragments.list.search.filter; + +import org.schabi.newpipe.extractor.linkhandler.SearchQueryHandlerFactory; +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.fragments.list.search.SearchFragment; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import androidx.annotation.Nullable; + +import static org.schabi.newpipe.extractor.search.filter.FilterContainer.ITEM_IDENTIFIER_UNKNOWN; + +/** + * This class handles all the user interaction with the content and sort filters + * of NewPipeExtractor. + *
+ * The class works standalone to just get the default selected filters eg. during init phase
+ * eg. in {@link SearchFragment#initializeFilterData()}
+ */
+public class SearchFilterLogic {
+
+ /**
+ * This list is used to communicate with NewPipeExtractor.
+ * It contains only the content filter ids that the user has selected from the UI.
+ */
+ private final List
+ * - create list with default id that will be preselected
+ * - create exclusivity lists for exclusive groups
+ * {@link ExclusiveGroups#filterIdToGroupIdMap} and
+ * {@link ExclusiveGroups#exclusiveGroupsIdSet}
+ * - check if {@link #selectedContentFilters} are valid ids
+ *
+ * @param filterGroups content or sort filter {@link FilterGroup} array
+ * @param exclusive
+ * @param selectedFilters
+ * @param fidToSupersetSortFilterMap null possible, only for content filters relevant
+ */
+ private void initFilters(
+ final FilterGroup[] filterGroups,
+ final ExclusiveGroups exclusive,
+ final ArrayList
+ * The list is created through the {@link #userSelectedContentFilters} identifiers list.
+ * This identifiers refer to {@link FilterItem}s.
+ *
+ * {@link #userSelectedContentFilters} will be cleared first!
+ */
+ private void createContentFilterItemListFromIdentifierList() {
+ userSelectedContentFilters.clear();
+ final FilterContainer filterContainer = searchQHFactory.getAvailableContentFilter();
+
+ for (final int contentFilterId : selectedContentFilters) {
+ final FilterItem contentFilterItem = filterContainer.getFilterItem(contentFilterId);
+ if (contentFilterItem != null) {
+ userSelectedContentFilters.add(contentFilterItem);
+ }
+ }
+ }
+
+ /**
+ * Prepare sort filter list with the actual {@link FilterItem}s to send to the library.
+ *
+ * The list is created through the {@link #userSelectedSortFilters} identifiers list.
+ * This identifiers refer to {@link FilterItem}s.
+ *
+ * {@link #userSelectedSortFilters} will be cleared first!
+ */
+ private void createSortFilterItemListFromIdentifiersList() {
+ userSelectedSortFilters.clear();
+ for (final int sortFilterId : selectedSortFilters) {
+ for (final int contentFilterId : selectedContentFilters) {
+ final FilterContainer filterContainer =
+ searchQHFactory.getContentFilterSortFilterVariant(contentFilterId);
+ if (filterContainer != null) {
+ final FilterItem sortFilterItem = filterContainer.getFilterItem(sortFilterId);
+ if (sortFilterItem != null) {
+ userSelectedSortFilters.add(sortFilterItem);
+ }
+ }
+ }
+ }
+ }
+
+ public void showSortFilterContainerUI() {
+ showSortFilterIdsContainerUI(selectedContentFilters);
+ }
+
+ /**
+ * Show only that sort filter UIs that are available for selected content ids.
+ *
+ * @param contentFilterIds
+ */
+ private void showSortFilterIdsContainerUI(final List
+ * See {@link SearchFilterOptionMenuAlikeDialogGenerator}
+ */
+ protected void showAllAvailableSortFilters() {
+ contentFilterFidToSupersetSortFilterMap.values().stream()
+ .filter(Objects::nonNull)
+ .distinct()
+ .forEach(container ->
+ setUiItemsVisibility(container, true, sortFilterIdToUiItemMap));
+ }
+
+ private void setUiItemsVisibility(final FilterContainer filters,
+ final boolean isVisible,
+ final Map
+ * If a callback is registered it wil be called with copy's of the local sort and
+ * content lists. To avoid concurrently modification of the lists. As they are progressed
+ * through async javarx calls. Note: The members aka {@link FilterItem}'s are not copied.
+ */
+ public void prepareForSearch() {
+ createContentFilterItemListFromIdentifierList();
+ createSortFilterItemListFromIdentifiersList();
+
+ if (callback != null) {
+ callback.selectedFilters(new ArrayList<>(userSelectedContentFilters),
+ new ArrayList<>(userSelectedSortFilters));
+ }
+ }
+
+ /**
+ * This method is meant to be called to add {@link android.view.View}s that represents
+ * a content filter.
+ *
+ * It has to be called within a subclass of {@link SearchFilterLogic} which implements
+ * {@link ICreateUiForFiltersWorker} itself or as an any inner class.
+ *
+ * @param id the id of a content filter
+ * @param uiItemWrapper the wrapped UI {@link android.view.View} for that content filter
+ */
+ protected void addContentFilterUiWrapperToItemMap(final int id,
+ final IUiItemWrapper uiItemWrapper) {
+ contentFilterIdToUiItemMap.put(id, uiItemWrapper);
+ }
+
+ /**
+ * This method is meant to be called to add {@link android.view.View}s that represents
+ * a sort filter.
+ *
+ * It has to be called within a subclass of {@link SearchFilterLogic} which implements
+ * {@link ICreateUiForFiltersWorker} itself or as an any inner class.
+ *
+ * @param id the id of a sort filter
+ * @param uiItemWrapper the wrapped UI {@link android.view.View} for that sort filter
+ */
+ protected void addSortFilterUiWrapperToItemMap(final int id,
+ final IUiItemWrapper uiItemWrapper) {
+ sortFilterIdToUiItemMap.put(id, uiItemWrapper);
+ }
+
+ /**
+ * Wrap a {@link FilterItem} or {@link FilterGroup} to their
+ * actual UI element(s) ({@link android.view.View}).
+ */
+ public interface IUiItemWrapper {
+ /**
+ * set a view element visible.
+ *
+ * @param visible true if visible, false if not visible
+ */
+ void setVisible(boolean visible);
+
+ /**
+ * @return get the id of the corresponding {@link FilterItem}
+ */
+ int getItemId();
+
+ /**
+ * @return get the id of the corresponding {@link FilterGroup}
+ */
+ int getGroupId();
+
+ /**
+ * Is the UI element selected.
+ *
+ * @return true if selected
+ */
+ boolean isChecked();
+
+ /**
+ * select the UI element.
+ *
+ * @param checked select UI element
+ */
+ void setChecked(boolean checked);
+ }
+
+ /**
+ * Creating user elements for all filters inside a {@link FilterContainer}.
+ *
+ * Note: use {@link #addContentFilterUiWrapperToItemMap(int, IUiItemWrapper)} and
+ * {@link #addSortFilterUiWrapperToItemMap(int, IUiItemWrapper)} to actually make
+ * {@link SearchFilterLogic} aware of them.
+ */
+ public interface ICreateUiForFiltersWorker {
+ /**
+ * Will be called before any {@link FilterContainer} looping.
+ */
+ void prepare();
+
+ /**
+ * Create Ui elements specifically related to the {@link FilterGroup} itself.
+ * But it could also be used for creating items.
+ *
+ * -> This method is called *before* the {@link #createFilterItem(FilterItem, FilterGroup)}
+ *
+ * @param filterGroup one group each time from {@link FilterContainer#getFilterGroups()}
+ */
+ void createFilterGroupBeforeItems(FilterGroup filterGroup);
+
+ /**
+ * Create Ui elements specifically related to a {@link FilterItem} itself.
+ *
+ * @param filterItem the actual item you should create a UI element here
+ * @param filterGroup (optional) one group each time from
+ * {@link FilterContainer#getFilterGroups()}
+ */
+ void createFilterItem(FilterItem filterItem, FilterGroup filterGroup);
+
+ /**
+ * Create Ui elements specifically related to the {@link FilterGroup} itself.
+ * But it could also be used for creating items.
+ *
+ * -> This method is called *after* the {@link #createFilterItem(FilterItem, FilterGroup)}
+ *
+ * @param filterGroup one group each time from {@link FilterContainer#getFilterGroups()}
+ */
+ void createFilterGroupAfterItems(FilterGroup filterGroup);
+
+ /**
+ * do anything you might want to clean up or whatever.
+ */
+ void finish();
+
+ /**
+ * Notify if filters are visible. Eg to show or hide 'sort filter' section title
+ *
+ * @param areFiltersVisible true if filter visible
+ */
+ void filtersVisible(boolean areFiltersVisible);
+ }
+
+ /**
+ * This callback will be called if a search with additional filters should occur.
+ */
+ public interface Callback {
+ void selectedFilters(List
+ * We need to track this ourselves as we otherwise rely on androids functionality or lack of
+ * tracking the before selected item that now is unselected.
+ */
+ private static class ExclusiveGroups {
+
+ final Map
+ * We need to know which filter belongs to which group, that we can
+ * determine if a selected {@link FilterItem} is part of an exclusive
+ * group or not.
+ *
+ * @param filterId filter identifier
+ * @param filterGroupId group identifier
+ */
+ public void putFilterIdToItsGroupId(final int filterId, final int filterGroupId) {
+ filterIdToGroupIdMap.put(filterId, filterGroupId);
+ }
+
+ /**
+ * Add exclusive groups to the map.
+ *
+ * @param groupId the id of the exclusive group
+ */
+ public void addGroupToExclusiveGroupsMap(final int groupId) {
+ exclusiveGroupsIdSet.add(groupId);
+ }
+
+ private void addIdIfBelongsToExclusiveGroup(final int filterId) {
+ final int filterGroupId = filterIdToGroupIdMap.get(filterId);
+ if (exclusiveGroupsIdSet.contains(filterGroupId)) {
+ actualSelectedFilterIdInExclusiveGroupMap.put(filterGroupId, filterId);
+ }
+ }
+
+ /**
+ * check if the filter group id for a given filter id is already in a exclusive group.
+ *
+ * If so remove the group filter id.
+ *
+ * @param filterId the id of a filter that might belong to an exclusive filter group
+ * @return id of removed filter id from {@link #actualSelectedFilterIdInExclusiveGroupMap}
+ * otherwise {@link FilterContainer#ITEM_IDENTIFIER_UNKNOWN}
+ */
+
+ private int ifInExclusiveGroupRemovePreviouslySelectedId(final int filterId) {
+ int previousFilterId = ITEM_IDENTIFIER_UNKNOWN;
+ final int filterGroupId = filterIdToGroupIdMap.get(filterId);
+
+ if (exclusiveGroupsIdSet.contains(filterGroupId)
+ && actualSelectedFilterIdInExclusiveGroupMap.containsKey(filterGroupId)) {
+ previousFilterId = actualSelectedFilterIdInExclusiveGroupMap.get(filterGroupId);
+ actualSelectedFilterIdInExclusiveGroupMap.remove(filterGroupId);
+ }
+ return previousFilterId;
+ }
+ }
+}