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 3c038aaf7c
commit 7c650f6e9d
2 changed files with 232 additions and 0 deletions

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

@ -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;
}
}
}