searchfilters: Moving DividerItem from NewPipeExtractor into NewPipe

DividerItem was inserted in the content filter framework in the
NewPipeExtractor to have a section title for YoutubeMusic. But as
UI releated stuff seems a bit out of place in the Extractor I came
up with injecting the DividerItem aka section title in the frontend
without having to change too much in the frontend.
This commit is contained in:
evermind 2022-11-22 23:18:40 +01:00
parent 2da5876970
commit d7bfb0ab7a
8 changed files with 269 additions and 28 deletions

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.search.filter.FilterItem
import org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem
import org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic
import org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.Factory.Variant
@ -38,11 +39,16 @@ class SearchViewModel(
val doSearchLiveData: LiveData<Boolean>
get() = doSearchMutableLiveData
val searchFilterLogic = SearchFilterLogic.Factory.create(
logicVariant, NewPipe.getService(serviceId).searchQHFactory, null
)
var searchFilterLogic: SearchFilterLogic
init {
// inject before creating SearchFilterLogic
InjectFilterItem.DividerBetweenYoutubeAndYoutubeMusic.run()
searchFilterLogic = SearchFilterLogic.Factory.create(
logicVariant,
NewPipe.getService(serviceId).searchQHFactory, null
)
searchFilterLogic.restorePreviouslySelectedFilters(
userSelectedContentFilterList,
userSelectedSortFilterList

View file

@ -0,0 +1,147 @@
package org.schabi.newpipe.fragments.list.search.filter;
import org.schabi.newpipe.App;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
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.extractor.search.filter.LibraryStringIds;
import org.schabi.newpipe.extractor.services.youtube.search.filter.YoutubeFilters;
import java.util.List;
import androidx.annotation.NonNull;
/**
* Inject a {@link FilterItem} that actually should not be a real filter.
* <p>
* This base class is meant to inject eg {@link DividerItem} (that inherits {@link FilterItem})
* as Divider between {@link FilterItem}. It will be shown in the UI's.
* <p>
* Of course you have to handle {@link DividerItem} or whatever in the Ui's.
* For that for example have a look at {@link SearchFilterDialogSpinnerAdapter}.
*/
public abstract class InjectFilterItem {
protected InjectFilterItem(
@NonNull final String serviceName,
final int injectedAfterFilterWithId,
@NonNull final FilterItem toBeInjectedFilterItem) {
prepareAndInject(serviceName, injectedAfterFilterWithId, toBeInjectedFilterItem);
}
// Please refer a static boolean to determine if already injected
protected abstract boolean isAlreadyInjected();
// Please refer a static boolean to determine if already injected
protected abstract void setAsInjected();
private void prepareAndInject(
@NonNull final String serviceName,
final int injectedAfterFilterWithId,
@NonNull final FilterItem toBeInjectedFilterItem) {
if (isAlreadyInjected()) { // already run
return;
}
try { // using serviceName to test if we are trying to inject into the right service
final List<FilterGroup> groups = NewPipe.getService(serviceName)
.getSearchQHFactory().getAvailableContentFilter().getFilterGroups();
injectFilterItemIntoGroup(
groups,
injectedAfterFilterWithId,
toBeInjectedFilterItem);
setAsInjected();
} catch (final ExtractionException ignored) {
// no the service we want to prepareAndInject -> so ignore
}
}
private void injectFilterItemIntoGroup(
@NonNull final List<FilterGroup> groups,
final int injectedAfterFilterWithId,
@NonNull final FilterItem toBeInjectedFilterItem) {
int indexForFilterId = 0;
boolean isFilterItemFound = false;
FilterGroup groupWithTheSearchFilterItem = null;
for (final FilterGroup group : groups) {
for (final FilterItem item : group.getFilterItems()) {
if (item.getIdentifier() == injectedAfterFilterWithId) {
isFilterItemFound = true;
break;
}
indexForFilterId++;
}
if (isFilterItemFound) {
groupWithTheSearchFilterItem = group;
break;
}
}
if (isFilterItemFound) {
// we want to insert after the FilterItem we've searched
indexForFilterId++;
groupWithTheSearchFilterItem.getFilterItems()
.add(indexForFilterId, toBeInjectedFilterItem);
}
}
/**
* Inject DividerItem between YouTube content filters and YoutubeMusic content filters.
*/
public static class DividerBetweenYoutubeAndYoutubeMusic extends InjectFilterItem {
private static boolean isYoutubeMusicDividerInjected = false;
protected DividerBetweenYoutubeAndYoutubeMusic() {
super(App.getApp().getApplicationContext().getString(R.string.youtube),
YoutubeFilters.ID_CF_MAIN_PLAYLISTS,
new DividerItem(R.string.search_filters_youtube_music)
);
}
/**
* Have a static runner method to avoid creating unnecessary objects if already inserted.
*/
public static void run() {
if (!isYoutubeMusicDividerInjected) {
new DividerBetweenYoutubeAndYoutubeMusic();
}
}
@Override
protected boolean isAlreadyInjected() {
return isYoutubeMusicDividerInjected;
}
@Override
protected void setAsInjected() {
isYoutubeMusicDividerInjected = true;
}
}
/**
* Used to have a title divider between regular {@link FilterItem}s.
*/
public static class DividerItem extends FilterItem {
private final int resId;
public DividerItem(final int resId) {
// the LibraryStringIds.. is not needed at all I just need one to satisfy FilterItem.
super(FilterContainer.ITEM_IDENTIFIER_UNKNOWN, LibraryStringIds.SEARCH_FILTERS_ALL);
this.resId = resId;
}
public int getStringResId() {
return this.resId;
}
}
}

View file

@ -23,6 +23,8 @@ import java.util.Objects;
import androidx.annotation.NonNull;
import androidx.collection.SparseArrayCompat;
import static org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem.DividerItem;
public class SearchFilterDialogSpinnerAdapter extends BaseAdapter {
private final Context context;
@ -48,12 +50,12 @@ public class SearchFilterDialogSpinnerAdapter extends BaseAdapter {
@Override
public int getCount() {
return group.getFilterItems().length;
return group.getFilterItems().size();
}
@Override
public Object getItem(final int position) {
return group.getFilterItems()[position];
return group.getFilterItems().get(position);
}
@Override
@ -63,7 +65,7 @@ public class SearchFilterDialogSpinnerAdapter extends BaseAdapter {
@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
final FilterItem item = group.getFilterItems()[position];
final FilterItem item = group.getFilterItems().get(position);
final TextView view;
if (convertView != null) {
@ -89,11 +91,12 @@ public class SearchFilterDialogSpinnerAdapter extends BaseAdapter {
view.setVisibility(wrappedView.getVisibility());
view.setEnabled(wrappedView.isEnabled());
if (item instanceof FilterItem.DividerItem) {
if (item instanceof DividerItem) {
final DividerItem dividerItem = (DividerItem) item;
wrappedView.setEnabled(false);
view.setEnabled(wrappedView.isEnabled());
final String menuDividerTitle = ">>>"
+ ServiceHelper.getTranslatedFilterString(item.getNameId(), context) + "<<<";
+ context.getString(dividerItem.getStringResId()) + "<<<";
view.setText(menuDividerTitle);
}
}
@ -111,7 +114,7 @@ public class SearchFilterDialogSpinnerAdapter extends BaseAdapter {
isInitialEnabled,
spinner);
if (item instanceof FilterItem.DividerItem) {
if (item instanceof DividerItem) {
wrappedView.setEnabled(false);
}

View file

@ -8,7 +8,6 @@ import org.schabi.newpipe.extractor.search.filter.FilterGroup;
import org.schabi.newpipe.extractor.search.filter.FilterItem;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
@ -185,7 +184,7 @@ public class SearchFilterLogic {
final List<FilterGroup> sortGroups = getAllSortFilterGroups(filters);
uiSortFilterWorker = createUiForFiltersWorker;
initFiltersUi(sortGroups.toArray(new FilterGroup[0]),
initFiltersUi(sortGroups,
sortFilterIdToUiItemMap,
createUiForFiltersWorker);
@ -202,7 +201,7 @@ public class SearchFilterLogic {
* @param createUiForFiltersWorker the implementation how to create the UI.
*/
private void initFiltersUi(
@NonNull final FilterGroup[] filterGroups,
@NonNull final List<FilterGroup> filterGroups,
@NonNull final SparseArrayCompat<IUiItemWrapper> filterIdToUiItemMap,
@NonNull final ICreateUiForFiltersWorker createUiForFiltersWorker) {
@ -235,7 +234,7 @@ public class SearchFilterLogic {
* @param fidToSupersetSortFilterMap null possible, only for content filters relevant
*/
private void initFilters(
@NonNull final FilterGroup[] filterGroups,
@NonNull final List<FilterGroup> filterGroups,
@NonNull final ExclusiveGroups exclusive,
@NonNull final List<Integer> selectedFilters,
@Nullable final SparseArrayCompat<FilterContainer> fidToSupersetSortFilterMap) {
@ -290,9 +289,7 @@ public class SearchFilterLogic {
private void initSortFilters() {
final FilterContainer filters = searchQHFactory.getAvailableContentFilter();
final List<FilterGroup> sortGroups = getAllSortFilterGroups(filters);
initFilters(sortGroups.toArray(new FilterGroup[0]), sortFilterExclusive,
selectedSortFilters, null);
initFilters(sortGroups, sortFilterExclusive, selectedSortFilters, null);
}
/**
@ -450,7 +447,7 @@ public class SearchFilterLogic {
for (final FilterGroup filterGroup : filters.getFilterGroups()) {
final FilterContainer sf = filterGroup.getAllSortFilters();
if (sf != null && sf.getFilterGroups() != null) {
sortGroups.addAll(Arrays.asList(sf.getFilterGroups()));
sortGroups.addAll(sf.getFilterGroups());
}
}
return sortGroups;

View file

@ -23,6 +23,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
import static org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem.DividerItem;
public class SearchFilterOptionMenuAlikeDialogGenerator extends BaseSearchFilterUiDialogGenerator {
private static final Integer NO_RESIZE_VIEW_TAG = 1;
@ -149,7 +150,7 @@ public class SearchFilterOptionMenuAlikeDialogGenerator extends BaseSearchFilter
for (final FilterItem item : filterGroup.getFilterItems()) {
final View view;
if (item instanceof FilterItem.DividerItem) {
if (item instanceof DividerItem) {
view = createDividerTextView(item, getLayoutParamsViews());
} else {
view = createViewItemRadio(item, getLayoutParamsViews());
@ -175,7 +176,7 @@ public class SearchFilterOptionMenuAlikeDialogGenerator extends BaseSearchFilter
@NonNull final UiSelectorDelegate selectorDelegate) {
for (final FilterItem item : filterGroup.getFilterItems()) {
final View view;
if (item instanceof FilterItem.DividerItem) {
if (item instanceof DividerItem) {
view = createDividerTextView(item, getLayoutParamsViews());
} else {
final CheckBox checkBox = createCheckBox(item, getLayoutParamsViews());
@ -278,10 +279,11 @@ public class SearchFilterOptionMenuAlikeDialogGenerator extends BaseSearchFilter
@NonNull
private TextView createDividerTextView(@NonNull final FilterItem item,
@NonNull final ViewGroup.LayoutParams layoutParams) {
final DividerItem dividerItem = (DividerItem) item;
final TextView view = new TextView(context);
view.setEnabled(true);
final String menuDividerTitle =
ServiceHelper.getTranslatedFilterString(item.getNameId(), context);
context.getString(dividerItem.getStringResId());
view.setText(menuDividerTitle);
view.setGravity(Gravity.TOP);
view.setLayoutParams(layoutParams);

View file

@ -24,6 +24,7 @@ import androidx.appcompat.view.menu.MenuBuilder;
import androidx.core.view.MenuCompat;
import static android.content.ContentValues.TAG;
import static org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem.DividerItem;
import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.ICreateUiForFiltersWorker;
import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.IUiItemWrapper;
@ -227,9 +228,10 @@ public class SearchFilterUIOptionMenu extends BaseSearchFilterUiGenerator {
@NonNull final FilterGroup filterGroup) {
final MenuItem item = createMenuItem(filterItem);
if (filterItem instanceof FilterItem.DividerItem) {
if (filterItem instanceof DividerItem) {
final DividerItem dividerItem = (DividerItem) filterItem;
final String menuDividerTitle = ">>>"
+ ServiceHelper.getTranslatedFilterString(filterItem.getNameId(), context)
+ context.getString(dividerItem.getStringResId())
+ "<<<";
item.setTitle(menuDividerTitle);
item.setEnabled(false);

View file

@ -0,0 +1,85 @@
package org.schabi.newpipe.filter;
import org.junit.Test;
import org.schabi.newpipe.extractor.NewPipe;
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
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.extractor.services.youtube.search.filter.YoutubeFilters;
import org.schabi.newpipe.fragments.list.search.filter.InjectFilterItem;
import java.util.Collection;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import androidx.annotation.NonNull;
import static junit.framework.TestCase.assertFalse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class InjectFilterItemTest {
static final String SERVICE_NAME = "YouTube";
@Test
public void injectIntoFilterGroupTest() throws ExtractionException {
final FilterContainer filterContainer = NewPipe.getService(SERVICE_NAME)
.getSearchQHFactory().getAvailableContentFilter();
final AtomicInteger itemCount = new AtomicInteger();
assertFalse(getInjectedFilterItem(filterContainer, itemCount).isPresent());
InjectDividerTestClass.run(SERVICE_NAME);
final int expectedInjectedItemPosition = 5;
final AtomicInteger injectedItemPosition = new AtomicInteger();
assertTrue(getInjectedFilterItem(filterContainer, injectedItemPosition).isPresent());
assertTrue(itemCount.get() > injectedItemPosition.get());
assertEquals(expectedInjectedItemPosition, injectedItemPosition.get());
}
@NonNull
private Optional<FilterItem> getInjectedFilterItem(
@NonNull final FilterContainer filterContainer,
@NonNull final AtomicInteger itemCount) {
return filterContainer.getFilterGroups().stream()
.map(FilterGroup::getFilterItems)
.flatMap(Collection::stream)
.filter(item -> {
itemCount.getAndIncrement();
return item instanceof InjectFilterItem.DividerItem;
})
.findAny();
}
public static class InjectDividerTestClass extends InjectFilterItem {
private static boolean isDividerInjected = false;
protected InjectDividerTestClass(@NonNull final String serviceName) {
super(serviceName,
YoutubeFilters.ID_CF_MAIN_PLAYLISTS,
new DividerItem(0)
);
}
public static void run(final String serviceName) {
if (!isDividerInjected) {
new InjectDividerTestClass(serviceName);
}
}
@Override
protected boolean isAlreadyInjected() {
return isDividerInjected;
}
@Override
protected void setAsInjected() {
isDividerInjected = true;
}
}
}

View file

@ -17,7 +17,6 @@ import org.schabi.newpipe.fragments.list.search.filter.BaseSearchFilterUiGenerat
import org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ -347,7 +346,7 @@ public class SearchFilterLogicAndUiGeneratorTest {
private void expectSortFiltersToBeVisible(final int id) {
final FilterContainer sortFilterVariant = service.getSearchQHFactory()
.getContentFilterSortFilterVariant(id);
assertTrue(sortFilterVariant.getFilterGroups().length > 0);
assertTrue(!sortFilterVariant.getFilterGroups().isEmpty());
for (final FilterGroup group : sortFilterVariant.getFilterGroups()) {
for (final FilterItem item : group.getFilterItems()) {
final int itemId = item.getIdentifier();
@ -389,9 +388,9 @@ public class SearchFilterLogicAndUiGeneratorTest {
final FilterContainer allSortFilters = service.getSearchQHFactory()
.getContentFilterSortFilterVariant(PeertubeFilters.ID_CF_MAIN_ALL);
// second way
final Optional<FilterGroup> allSortFilters2 = Arrays.stream(service.getSearchQHFactory()
.getAvailableContentFilter()
.getFilterGroups())
final Optional<FilterGroup> allSortFilters2 = service.getSearchQHFactory()
.getAvailableContentFilter()
.getFilterGroups().stream()
.filter(filterGroup
-> (filterGroup.getIdentifier() == PeertubeFilters.ID_CF_MAIN_GRP))
.findFirst();
@ -399,7 +398,7 @@ public class SearchFilterLogicAndUiGeneratorTest {
assertNotNull(allSortFilters);
assertTrue(allSortFilters2.isPresent());
assertEquals(allSortFilters, allSortFilters2.get().getAllSortFilters());
assertTrue(allSortFilters.getFilterGroups().length > 0);
assertTrue(!allSortFilters.getFilterGroups().isEmpty());
assertNotNull(sortWorker.areAnySortFiltersVisible);
assertTrue(sortWorker.areAnySortFiltersVisible.isPresent());
assertFalse(sortWorker.areAnySortFiltersVisible.get());