Merge 4eb71a88cc
into 6f7b905983
This commit is contained in:
commit
9f46109bca
|
@ -198,7 +198,7 @@ dependencies {
|
|||
// name and the commit hash with the commit hash of the (pushed) commit you want to test
|
||||
// This works thanks to JitPack: https://jitpack.io/
|
||||
implementation 'com.github.TeamNewPipe:nanojson:1d9e1aea9049fc9f85e68b43ba39fe7be1c1f751'
|
||||
implementation 'com.github.TeamNewPipe:NewPipeExtractor:v0.23.1'
|
||||
implementation 'com.github.bravenewpipe:NewPipeExtractor:67366ea7b1'
|
||||
implementation 'com.github.TeamNewPipe:NoNonsense-FilePicker:5.0.0'
|
||||
|
||||
/** Checkstyle **/
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.schabi.newpipe.error.UserAction;
|
|||
import org.schabi.newpipe.extractor.channel.ChannelInfo;
|
||||
import org.schabi.newpipe.extractor.exceptions.ContentNotSupportedException;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.search.filter.FilterItem;
|
||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||
import org.schabi.newpipe.fragments.detail.TabAdapter;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
|
@ -461,7 +462,7 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||
.getDefaultSharedPreferences(context);
|
||||
|
||||
for (final ListLinkHandler linkHandler : currentInfo.getTabs()) {
|
||||
final String tab = linkHandler.getContentFilters().get(0);
|
||||
final FilterItem tab = linkHandler.getContentFilters().get(0);
|
||||
if (ChannelTabHelper.showChannelTab(context, preferences, tab)) {
|
||||
final ChannelTabFragment channelTabFragment =
|
||||
ChannelTabFragment.getInstance(serviceId, linkHandler, name);
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.schabi.newpipe.fragments.list.search;
|
|||
import static androidx.recyclerview.widget.ItemTouchHelper.Callback.makeMovementFlags;
|
||||
import static org.schabi.newpipe.ktx.ViewUtils.animate;
|
||||
import static org.schabi.newpipe.util.ExtractorHelper.showMetaInfoInTextView;
|
||||
import static java.util.Arrays.asList;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
@ -33,16 +32,18 @@ import androidx.annotation.Nullable;
|
|||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
import androidx.collection.SparseArrayCompat;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.recyclerview.widget.ItemTouchHelper;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.schabi.newpipe.App;
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.databinding.FragmentSearchBinding;
|
||||
import org.schabi.newpipe.error.ErrorInfo;
|
||||
import org.schabi.newpipe.error.ErrorUtil;
|
||||
import org.schabi.newpipe.error.ReCaptchaActivity;
|
||||
import org.schabi.newpipe.error.UserAction;
|
||||
import org.schabi.newpipe.extractor.InfoItem;
|
||||
|
@ -53,10 +54,13 @@ import org.schabi.newpipe.extractor.Page;
|
|||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.search.SearchExtractor;
|
||||
import org.schabi.newpipe.extractor.search.SearchInfo;
|
||||
import org.schabi.newpipe.extractor.services.peertube.linkHandler.PeertubeSearchQueryHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory;
|
||||
import org.schabi.newpipe.extractor.search.filter.FilterItem;
|
||||
import org.schabi.newpipe.fragments.BackPressable;
|
||||
import org.schabi.newpipe.fragments.list.BaseListFragment;
|
||||
import org.schabi.newpipe.fragments.list.search.filter.SearchFilterChipDialogFragment;
|
||||
import org.schabi.newpipe.fragments.list.search.filter.SearchFilterDialogFragment;
|
||||
import org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic;
|
||||
import org.schabi.newpipe.fragments.list.search.filter.SearchFilterOptionMenuAlikeDialogFragment;
|
||||
import org.schabi.newpipe.ktx.AnimationType;
|
||||
import org.schabi.newpipe.ktx.ExceptionUtils;
|
||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
|
||||
|
@ -66,7 +70,6 @@ import org.schabi.newpipe.util.DeviceUtils;
|
|||
import org.schabi.newpipe.util.ExtractorHelper;
|
||||
import org.schabi.newpipe.util.KeyboardUtil;
|
||||
import org.schabi.newpipe.util.NavigationHelper;
|
||||
import org.schabi.newpipe.util.ServiceHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
@ -104,9 +107,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
private static final int SUGGESTIONS_DEBOUNCE = 120; //ms
|
||||
private final PublishSubject<String> suggestionPublisher = PublishSubject.create();
|
||||
|
||||
@State
|
||||
int filterItemCheckedId = -1;
|
||||
|
||||
@State
|
||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
||||
|
||||
|
@ -114,15 +114,9 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
@State
|
||||
String searchString;
|
||||
|
||||
/**
|
||||
* No content filter should add like contentFilter = all
|
||||
* be aware of this when implementing an extractor.
|
||||
*/
|
||||
@State
|
||||
String[] contentFilter = new String[0];
|
||||
List<FilterItem> selectedContentFilter = new ArrayList<>();
|
||||
|
||||
@State
|
||||
String sortFilter;
|
||||
List<FilterItem> selectedSortFilter = new ArrayList<>();
|
||||
|
||||
// these represents the last search
|
||||
@State
|
||||
|
@ -140,8 +134,6 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
@State
|
||||
boolean wasSearchFocused = false;
|
||||
|
||||
private final SparseArrayCompat<String> menuItemToFilterName = new SparseArrayCompat<>();
|
||||
private StreamingService service;
|
||||
private Page nextPage;
|
||||
private boolean showLocalSuggestions = true;
|
||||
private boolean showRemoteSuggestions = true;
|
||||
|
@ -159,7 +151,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
|
||||
private FragmentSearchBinding searchBinding;
|
||||
|
||||
private View searchToolbarContainer;
|
||||
protected View searchToolbarContainer;
|
||||
private EditText searchEditText;
|
||||
private View searchClear;
|
||||
|
||||
|
@ -173,9 +165,32 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
*/
|
||||
private TextWatcher textWatcher;
|
||||
|
||||
@State
|
||||
ArrayList<Integer> userSelectedContentFilterList;
|
||||
|
||||
@State
|
||||
ArrayList<Integer> userSelectedSortFilterList = null;
|
||||
|
||||
protected SearchViewModel searchViewModel;
|
||||
protected SearchFilterLogic.Factory.Variant logicVariant =
|
||||
SearchFilterLogic.Factory.Variant.SEARCH_FILTER_LOGIC_DEFAULT;
|
||||
|
||||
|
||||
public static SearchFragment getInstance(final int serviceId, final String searchString) {
|
||||
final SearchFragment searchFragment = new SearchFragment();
|
||||
searchFragment.setQuery(serviceId, searchString, new String[0], "");
|
||||
final SearchFragment searchFragment;
|
||||
final App app = App.getApp();
|
||||
|
||||
|
||||
final String searchUi = PreferenceManager.getDefaultSharedPreferences(app)
|
||||
.getString(app.getString(R.string.search_filter_ui_key),
|
||||
app.getString(R.string.search_filter_ui_value));
|
||||
if (app.getString(R.string.search_filter_ui_option_menu_legacy_key).equals(searchUi)) {
|
||||
searchFragment = new SearchFragmentLegacy();
|
||||
} else {
|
||||
searchFragment = new SearchFragment();
|
||||
}
|
||||
|
||||
searchFragment.setQuery(serviceId, searchString);
|
||||
|
||||
if (!TextUtils.isEmpty(searchString)) {
|
||||
searchFragment.setSearchOnResume();
|
||||
|
@ -208,11 +223,53 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, @Nullable final ViewGroup container,
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
|
||||
if (userSelectedContentFilterList == null) {
|
||||
userSelectedContentFilterList = new ArrayList<>();
|
||||
}
|
||||
|
||||
if (userSelectedSortFilterList == null) {
|
||||
userSelectedSortFilterList = new ArrayList<>();
|
||||
}
|
||||
|
||||
initViewModel();
|
||||
|
||||
// observe the content/sort filter items lists
|
||||
searchViewModel.getSelectedContentFilterItemListLiveData().observe(
|
||||
getViewLifecycleOwner(), filterItems -> selectedContentFilter = filterItems);
|
||||
searchViewModel.getSelectedSortFilterItemListLiveData().observe(
|
||||
getViewLifecycleOwner(), filterItems -> selectedSortFilter = filterItems);
|
||||
|
||||
// the content/sort filters ids lists are only
|
||||
// observed here to store them via Icepick
|
||||
searchViewModel.getUserSelectedContentFilterListLiveData().observe(
|
||||
getViewLifecycleOwner(), filterIds -> userSelectedContentFilterList = filterIds);
|
||||
searchViewModel.getUserSelectedSortFilterListLiveData().observe(
|
||||
getViewLifecycleOwner(), filterIds -> userSelectedSortFilterList = filterIds);
|
||||
|
||||
searchViewModel.getDoSearchLiveData().observe(
|
||||
getViewLifecycleOwner(), doSearch -> {
|
||||
if (doSearch) {
|
||||
selectedFilters(selectedContentFilter, selectedSortFilter);
|
||||
searchViewModel.weConsumedDoSearchLiveData();
|
||||
}
|
||||
});
|
||||
|
||||
return inflater.inflate(R.layout.fragment_search, container, false);
|
||||
}
|
||||
|
||||
protected void initViewModel() {
|
||||
searchViewModel = new ViewModelProvider(this, SearchViewModel.Companion
|
||||
.getFactory(serviceId,
|
||||
logicVariant,
|
||||
userSelectedContentFilterList,
|
||||
userSelectedSortFilterList))
|
||||
.get(SearchViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
|
||||
searchBinding = FragmentSearchBinding.bind(rootView);
|
||||
|
@ -221,22 +278,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
initSearchListeners();
|
||||
}
|
||||
|
||||
private void updateService() {
|
||||
try {
|
||||
service = NewPipe.getService(serviceId);
|
||||
} catch (final Exception e) {
|
||||
ErrorUtil.showUiErrorSnackbar(this, "Getting service for id " + serviceId, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "onStart() called");
|
||||
}
|
||||
super.onStart();
|
||||
|
||||
updateService();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -268,11 +315,11 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
|
||||
if (!TextUtils.isEmpty(searchString)) {
|
||||
if (wasLoading.getAndSet(false)) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
search(searchString);
|
||||
return;
|
||||
} else if (infoListAdapter.getItemsList().isEmpty()) {
|
||||
if (savedState == null) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
search(searchString);
|
||||
return;
|
||||
} else if (!isLoading.get() && !wasSearchFocused && lastPanelError == null) {
|
||||
infoListAdapter.clearStreamItemList();
|
||||
|
@ -325,7 +372,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
if (requestCode == ReCaptchaActivity.RECAPTCHA_REQUEST) {
|
||||
if (resultCode == Activity.RESULT_OK
|
||||
&& !TextUtils.isEmpty(searchString)) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
search(searchString);
|
||||
} else {
|
||||
Log.e(TAG, "ReCaptcha failed");
|
||||
}
|
||||
|
@ -391,6 +438,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
searchString = searchEditText != null
|
||||
? searchEditText.getText().toString()
|
||||
: searchString;
|
||||
|
||||
super.onSaveInstanceState(bundle);
|
||||
}
|
||||
|
||||
|
@ -404,7 +452,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
|| (searchEditText != null && !TextUtils.isEmpty(searchEditText.getText()))) {
|
||||
search(!TextUtils.isEmpty(searchString)
|
||||
? searchString
|
||||
: searchEditText.getText().toString(), this.contentFilter, "");
|
||||
: searchEditText.getText().toString());
|
||||
} else {
|
||||
if (searchEditText != null) {
|
||||
searchEditText.setText("");
|
||||
|
@ -429,60 +477,22 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
supportActionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
int itemId = 0;
|
||||
boolean isFirstItem = true;
|
||||
final Context c = getContext();
|
||||
createMenu(menu, inflater);
|
||||
}
|
||||
|
||||
if (service == null) {
|
||||
Log.w(TAG, "onCreateOptionsMenu() called with null service");
|
||||
updateService();
|
||||
}
|
||||
|
||||
for (final String filter : service.getSearchQHFactory().getAvailableContentFilter()) {
|
||||
if (filter.equals(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS)) {
|
||||
final MenuItem musicItem = menu.add(2,
|
||||
itemId++,
|
||||
0,
|
||||
"YouTube Music");
|
||||
musicItem.setEnabled(false);
|
||||
} else if (filter.equals(PeertubeSearchQueryHandlerFactory.SEPIA_VIDEOS)) {
|
||||
final MenuItem sepiaItem = menu.add(2,
|
||||
itemId++,
|
||||
0,
|
||||
"Sepia Search");
|
||||
sepiaItem.setEnabled(false);
|
||||
}
|
||||
menuItemToFilterName.put(itemId, filter);
|
||||
final MenuItem item = menu.add(1,
|
||||
itemId++,
|
||||
0,
|
||||
ServiceHelper.getTranslatedFilterString(filter, c));
|
||||
if (isFirstItem) {
|
||||
item.setChecked(true);
|
||||
isFirstItem = false;
|
||||
}
|
||||
}
|
||||
menu.setGroupCheckable(1, true, true);
|
||||
|
||||
restoreFilterChecked(menu, filterItemCheckedId);
|
||||
protected void createMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_search_fragment, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
final var filter = Collections.singletonList(menuItemToFilterName.get(item.getItemId()));
|
||||
changeContentFilter(item, filter);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void restoreFilterChecked(final Menu menu, final int itemId) {
|
||||
if (itemId != -1) {
|
||||
final MenuItem item = menu.findItem(itemId);
|
||||
if (item == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
item.setChecked(true);
|
||||
if (item.getItemId() == R.id.action_filter) {
|
||||
hideKeyboardSearch();
|
||||
showSelectFiltersDialog();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
@ -562,7 +572,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
suggestionListAdapter.setListener(new SuggestionListAdapter.OnSuggestionItemSelected() {
|
||||
@Override
|
||||
public void onSuggestionItemSelected(final SuggestionItem item) {
|
||||
search(item.query, new String[0], "");
|
||||
search(item.query);
|
||||
searchEditText.setText(item.query);
|
||||
}
|
||||
|
||||
|
@ -619,7 +629,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
} else if (event != null
|
||||
&& (event.getKeyCode() == KeyEvent.KEYCODE_ENTER
|
||||
|| event.getAction() == EditorInfo.IME_ACTION_SEARCH)) {
|
||||
search(searchEditText.getText().toString(), new String[0], "");
|
||||
search(searchEditText.getText().toString());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
@ -671,7 +681,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
KeyboardUtil.showKeyboard(activity, searchEditText);
|
||||
}
|
||||
|
||||
private void hideKeyboardSearch() {
|
||||
protected void hideKeyboardSearch() {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "hideKeyboardSearch() called");
|
||||
}
|
||||
|
@ -805,9 +815,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
// no-op
|
||||
}
|
||||
|
||||
private void search(final String theSearchString,
|
||||
final String[] theContentFilter,
|
||||
final String theSortFilter) {
|
||||
private void search(final String theSearchString) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "search() called with: query = [" + theSearchString + "]");
|
||||
}
|
||||
|
@ -862,13 +870,12 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
}
|
||||
searchDisposable = ExtractorHelper.searchFor(serviceId,
|
||||
searchString,
|
||||
Arrays.asList(contentFilter),
|
||||
sortFilter)
|
||||
selectedContentFilter,
|
||||
selectedSortFilter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnEvent((searchResult, throwable) -> isLoading.set(false))
|
||||
.subscribe(this::handleResult, this::onItemError);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -884,8 +891,8 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
searchDisposable = ExtractorHelper.getMoreSearchItems(
|
||||
serviceId,
|
||||
searchString,
|
||||
asList(contentFilter),
|
||||
sortFilter,
|
||||
selectedContentFilter,
|
||||
selectedSortFilter,
|
||||
nextPage)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -917,25 +924,21 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
// Utils
|
||||
//////////////////////////////////////////////////////////////////////////*/
|
||||
|
||||
private void changeContentFilter(final MenuItem item, final List<String> theContentFilter) {
|
||||
filterItemCheckedId = item.getItemId();
|
||||
item.setChecked(true);
|
||||
public void selectedFilters(@NonNull final List<FilterItem> theSelectedContentFilter,
|
||||
@NonNull final List<FilterItem> theSelectedSortFilter) {
|
||||
|
||||
contentFilter = theContentFilter.toArray(new String[0]);
|
||||
selectedContentFilter = theSelectedContentFilter;
|
||||
selectedSortFilter = theSelectedSortFilter;
|
||||
|
||||
if (!TextUtils.isEmpty(searchString)) {
|
||||
search(searchString, contentFilter, sortFilter);
|
||||
search(searchString);
|
||||
}
|
||||
}
|
||||
|
||||
private void setQuery(final int theServiceId,
|
||||
final String theSearchString,
|
||||
final String[] theContentFilter,
|
||||
final String theSortFilter) {
|
||||
final String theSearchString) {
|
||||
serviceId = theServiceId;
|
||||
searchString = theSearchString;
|
||||
contentFilter = theContentFilter;
|
||||
sortFilter = theSortFilter;
|
||||
}
|
||||
|
||||
/*//////////////////////////////////////////////////////////////////////////
|
||||
|
@ -1020,7 +1023,7 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
|
||||
searchBinding.correctSuggestion.setOnClickListener(v -> {
|
||||
searchBinding.correctSuggestion.setVisibility(View.GONE);
|
||||
search(searchSuggestion, contentFilter, sortFilter);
|
||||
search(searchSuggestion);
|
||||
searchEditText.setText(searchSuggestion);
|
||||
});
|
||||
|
||||
|
@ -1085,4 +1088,22 @@ public class SearchFragment extends BaseListFragment<SearchInfo, ListExtractor.I
|
|||
UserAction.DELETE_FROM_HISTORY, "Deleting item failed")));
|
||||
disposables.add(onDelete);
|
||||
}
|
||||
|
||||
private void showSelectFiltersDialog() {
|
||||
final FragmentManager fragmentManager = getChildFragmentManager();
|
||||
final DialogFragment searchFilterUiDialog;
|
||||
|
||||
final String searchUi = PreferenceManager.getDefaultSharedPreferences(App.getApp())
|
||||
.getString(getString(R.string.search_filter_ui_key),
|
||||
getString(R.string.search_filter_ui_value));
|
||||
if (getString(R.string.search_filter_ui_option_menu_style_key).equals(searchUi)) {
|
||||
searchFilterUiDialog = new SearchFilterOptionMenuAlikeDialogFragment();
|
||||
} else if (getString(R.string.search_filter_ui_chip_dialog_key).equals(searchUi)) {
|
||||
searchFilterUiDialog = new SearchFilterChipDialogFragment();
|
||||
} else { // default dialog
|
||||
searchFilterUiDialog = new SearchFilterDialogFragment();
|
||||
}
|
||||
|
||||
searchFilterUiDialog.show(fragmentManager, "fragment_search");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.fragments.list.search;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic;
|
||||
import org.schabi.newpipe.fragments.list.search.filter.SearchFilterUIOptionMenu;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import icepick.State;
|
||||
|
||||
/**
|
||||
* Fragment that hosts the action menu based filter 'dialog'.
|
||||
* <p>
|
||||
* Called ..Legacy because this was the way NewPipe had implemented the search filter dialog.
|
||||
* <p>
|
||||
* The new UI's are handled by {@link SearchFragment} and implemented by
|
||||
* using {@link androidx.fragment.app.DialogFragment}.
|
||||
*/
|
||||
public class SearchFragmentLegacy extends SearchFragment {
|
||||
|
||||
@State
|
||||
protected int countOnPrepareOptionsMenuCalls = 0;
|
||||
private SearchFilterUIOptionMenu searchFilterUi;
|
||||
|
||||
@Override
|
||||
protected void initViewModel() {
|
||||
logicVariant = SearchFilterLogic.Factory.Variant.SEARCH_FILTER_LOGIC_LEGACY;
|
||||
super.initViewModel();
|
||||
|
||||
searchFilterUi = new SearchFilterUIOptionMenu(
|
||||
searchViewModel.getSearchFilterLogic(), requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createMenu(@NonNull final Menu menu,
|
||||
@NonNull final MenuInflater inflater) {
|
||||
searchFilterUi.createSearchUI(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
return searchFilterUi.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initViews(final View rootView,
|
||||
final Bundle savedInstanceState) {
|
||||
super.initViews(rootView, savedInstanceState);
|
||||
final Toolbar toolbar = (Toolbar) searchToolbarContainer.getParent();
|
||||
toolbar.setOverflowIcon(ContextCompat.getDrawable(requireContext(),
|
||||
R.drawable.ic_sort));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(@NonNull final Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
// workaround: we want to hide the keyboard in case we open the options
|
||||
// menu. As somehow this method gets triggered twice but only the 2nd
|
||||
// time is relevant as the options menu is selected by the user.
|
||||
if (++countOnPrepareOptionsMenuCalls > 1) {
|
||||
hideKeyboardSearch();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
package org.schabi.newpipe.fragments.list.search
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
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
|
||||
|
||||
/**
|
||||
* This class hosts the search filters logic. It facilitates
|
||||
* the communication with the SearchFragment* and the *DialogFragment
|
||||
* based search filter UI's
|
||||
*/
|
||||
class SearchViewModel(
|
||||
val serviceId: Int,
|
||||
logicVariant: Variant,
|
||||
userSelectedContentFilterList: List<Int>,
|
||||
userSelectedSortFilterList: List<Int>
|
||||
) : ViewModel() {
|
||||
|
||||
private val selectedContentFilterMutableLiveData: MutableLiveData<MutableList<FilterItem>> =
|
||||
MutableLiveData()
|
||||
private var selectedSortFilterLiveData: MutableLiveData<MutableList<FilterItem>> =
|
||||
MutableLiveData()
|
||||
private var userSelectedSortFilterListMutableLiveData: MutableLiveData<ArrayList<Int>> =
|
||||
MutableLiveData()
|
||||
private var userSelectedContentFilterListMutableLiveData: MutableLiveData<ArrayList<Int>> =
|
||||
MutableLiveData()
|
||||
private var doSearchMutableLiveData: MutableLiveData<Boolean> = MutableLiveData()
|
||||
|
||||
val selectedContentFilterItemListLiveData: LiveData<MutableList<FilterItem>>
|
||||
get() = selectedContentFilterMutableLiveData
|
||||
val selectedSortFilterItemListLiveData: LiveData<MutableList<FilterItem>>
|
||||
get() = selectedSortFilterLiveData
|
||||
val userSelectedContentFilterListLiveData: LiveData<ArrayList<Int>>
|
||||
get() = userSelectedContentFilterListMutableLiveData
|
||||
val userSelectedSortFilterListLiveData: LiveData<ArrayList<Int>>
|
||||
get() = userSelectedSortFilterListMutableLiveData
|
||||
val doSearchLiveData: LiveData<Boolean>
|
||||
get() = doSearchMutableLiveData
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
searchFilterLogic.setCallback { userSelectedContentFilter: List<FilterItem?>,
|
||||
userSelectedSortFilter: List<FilterItem?> ->
|
||||
selectedContentFilterMutableLiveData.value =
|
||||
userSelectedContentFilter as MutableList<FilterItem>
|
||||
selectedSortFilterLiveData.value =
|
||||
userSelectedSortFilter as MutableList<FilterItem>
|
||||
userSelectedContentFilterListMutableLiveData.value =
|
||||
searchFilterLogic.selectedContentFilters
|
||||
userSelectedSortFilterListMutableLiveData.value =
|
||||
searchFilterLogic.selectedSortFilters
|
||||
|
||||
doSearchMutableLiveData.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun weConsumedDoSearchLiveData() {
|
||||
doSearchMutableLiveData.value = false
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun getFactory(
|
||||
serviceId: Int,
|
||||
logicVariant: Variant,
|
||||
userSelectedContentFilterList: ArrayList<Int>,
|
||||
userSelectedSortFilterList: ArrayList<Int>
|
||||
) = viewModelFactory {
|
||||
initializer {
|
||||
SearchViewModel(
|
||||
serviceId,
|
||||
logicVariant,
|
||||
userSelectedContentFilterList,
|
||||
userSelectedSortFilterList
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
// 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.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.search.filter.FilterGroup;
|
||||
import org.schabi.newpipe.extractor.search.filter.FilterItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.ICreateUiForFiltersWorker;
|
||||
|
||||
/**
|
||||
* Common base for the {@link SearchFilterDialogGenerator} and
|
||||
* {@link SearchFilterOptionMenuAlikeDialogGenerator}'s
|
||||
* {@link ICreateUiForFiltersWorker} implementation.
|
||||
*/
|
||||
public abstract class BaseCreateSearchFilterUI
|
||||
implements ICreateUiForFiltersWorker {
|
||||
|
||||
@NonNull
|
||||
protected final BaseSearchFilterUiDialogGenerator dialogGenBase;
|
||||
@NonNull
|
||||
protected final Context context;
|
||||
protected final List<View> titleViewElements = new ArrayList<>();
|
||||
protected final SearchFilterLogic logic;
|
||||
protected int titleResId;
|
||||
|
||||
protected BaseCreateSearchFilterUI(
|
||||
@NonNull final BaseSearchFilterUiDialogGenerator dialogGenBase,
|
||||
@NonNull final SearchFilterLogic logic,
|
||||
@NonNull final Context context,
|
||||
final int titleResId) {
|
||||
this.dialogGenBase = dialogGenBase;
|
||||
this.logic = logic;
|
||||
this.context = context;
|
||||
this.titleResId = titleResId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterItem(@NonNull final FilterItem filterItem,
|
||||
@NonNull final FilterGroup filterGroup) {
|
||||
// no implementation here all creation stuff is done in createFilterGroupBeforeItems
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterGroupAfterItems(@NonNull final FilterGroup filterGroup) {
|
||||
// no implementation here all creation stuff is done in createFilterGroupBeforeItems
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
// no implementation here all creation stuff is done in createFilterGroupBeforeItems
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to control the visibility of the title 'sort filter' if the
|
||||
* chosen content filter has no sort filters.
|
||||
*
|
||||
* @param areFiltersVisible true if filter visible
|
||||
*/
|
||||
@Override
|
||||
public void filtersVisible(final boolean areFiltersVisible) {
|
||||
final int visibility = areFiltersVisible ? View.VISIBLE : View.GONE;
|
||||
for (final View view : titleViewElements) {
|
||||
if (view != null) {
|
||||
view.setVisibility(visibility);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class CreateContentFilterUI extends CreateSortFilterUI {
|
||||
|
||||
public CreateContentFilterUI(
|
||||
@NonNull final BaseSearchFilterUiDialogGenerator dialogGenBase,
|
||||
@NonNull final Context context,
|
||||
@NonNull final SearchFilterLogic logic) {
|
||||
super(dialogGenBase, context, logic);
|
||||
this.titleResId = R.string.filter_search_content_filters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterGroupBeforeItems(
|
||||
@NonNull final FilterGroup filterGroup) {
|
||||
dialogGenBase.createFilterGroup(filterGroup,
|
||||
logic::addContentFilterUiWrapperToItemMap,
|
||||
logic::selectContentFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void filtersVisible(final boolean areFiltersVisible) {
|
||||
// no implementation here. As content filters have to be always visible
|
||||
}
|
||||
}
|
||||
|
||||
public static class CreateSortFilterUI extends BaseCreateSearchFilterUI {
|
||||
|
||||
public CreateSortFilterUI(
|
||||
@NonNull final BaseSearchFilterUiDialogGenerator dialogGenBase,
|
||||
@NonNull final Context context,
|
||||
@NonNull final SearchFilterLogic logic) {
|
||||
super(dialogGenBase, logic, context, R.string.filter_search_sort_filters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare() {
|
||||
dialogGenBase.createTitle(context.getString(titleResId), titleViewElements);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterGroupBeforeItems(@NonNull final FilterGroup filterGroup) {
|
||||
dialogGenBase.createFilterGroup(filterGroup,
|
||||
logic::addSortFilterUiWrapperToItemMap,
|
||||
logic::selectSortFilter);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
// 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.search.filter.FilterItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public abstract class BaseItemWrapper implements SearchFilterLogic.IUiItemWrapper {
|
||||
@NonNull
|
||||
protected final FilterItem item;
|
||||
|
||||
protected BaseItemWrapper(@NonNull final FilterItem item) {
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemId() {
|
||||
return item.getIdentifier();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.fragments.list.search.filter;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.fragments.list.search.SearchViewModel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
/**
|
||||
* Base dialog class for {@link DialogFragment} based search filter dialogs.
|
||||
*/
|
||||
public abstract class BaseSearchFilterDialogFragment extends DialogFragment {
|
||||
|
||||
protected BaseSearchFilterUiGenerator dialogGenerator;
|
||||
protected SearchViewModel searchViewModel;
|
||||
|
||||
private void createSearchFilterUi() {
|
||||
dialogGenerator = createSearchFilterDialogGenerator();
|
||||
dialogGenerator.createSearchUI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(@NonNull final FragmentManager manager, @Nullable final String tag) {
|
||||
// Avoid multiple instances of the dialog that could be triggered by multiple taps
|
||||
if (manager.findFragmentByTag(tag) == null) {
|
||||
super.show(manager, tag);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract BaseSearchFilterUiGenerator createSearchFilterDialogGenerator();
|
||||
|
||||
/**
|
||||
* As we have different bindings we need to get this sorted in a method.
|
||||
*
|
||||
* @return the {@link Toolbar} null if there is no toolbar available.
|
||||
*/
|
||||
@Nullable
|
||||
protected abstract Toolbar getToolbar();
|
||||
|
||||
protected abstract View getRootView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container);
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Make sure that the first parameter is pointing to instance of SearchFragment otherwise
|
||||
// another SearchViewModel object will be created instead of the existing one used.
|
||||
// -> the SearchViewModel is first instantiated in SearchFragment. Here we just use it.
|
||||
searchViewModel =
|
||||
new ViewModelProvider(requireParentFragment()).get(SearchViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
final View rootView = getRootView(inflater, container);
|
||||
createSearchFilterUi();
|
||||
return rootView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
final Toolbar toolbar = getToolbar();
|
||||
if (toolbar != null) {
|
||||
initToolbar(toolbar);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the toolbar.
|
||||
* <p>
|
||||
* This method is only called if {@link #getToolbar()} is implemented to return a toolbar.
|
||||
*
|
||||
* @param toolbar the actual toolbar for this dialog fragment
|
||||
*/
|
||||
protected void initToolbar(@NonNull final Toolbar toolbar) {
|
||||
toolbar.setTitle(R.string.filter);
|
||||
toolbar.setNavigationIcon(R.drawable.ic_arrow_back);
|
||||
toolbar.inflateMenu(R.menu.menu_search_filter_dialog_fragment);
|
||||
toolbar.setNavigationOnClickListener(v -> dismiss());
|
||||
toolbar.setNavigationContentDescription(R.string.cancel);
|
||||
|
||||
final View okButton = toolbar.findViewById(R.id.search);
|
||||
okButton.setEnabled(true);
|
||||
|
||||
final View resetButton = toolbar.findViewById(R.id.reset);
|
||||
resetButton.setEnabled(true);
|
||||
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
if (item.getItemId() == R.id.search) {
|
||||
searchViewModel.getSearchFilterLogic().prepareForSearch();
|
||||
dismiss();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.reset) {
|
||||
searchViewModel.getSearchFilterLogic().reset();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// 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.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.extractor.search.filter.FilterGroup;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import static android.util.TypedValue.COMPLEX_UNIT_DIP;
|
||||
import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.ICreateUiForFiltersWorker;
|
||||
|
||||
public abstract class BaseSearchFilterUiDialogGenerator extends BaseSearchFilterUiGenerator {
|
||||
private static final float FONT_SIZE_TITLE_ITEMS_IN_DIP = 20f;
|
||||
|
||||
protected BaseSearchFilterUiDialogGenerator(
|
||||
@NonNull final SearchFilterLogic logic,
|
||||
@NonNull final Context context) {
|
||||
super(logic, context);
|
||||
}
|
||||
|
||||
protected abstract void createTitle(@NonNull String name,
|
||||
@NonNull List<View> titleViewElements);
|
||||
|
||||
protected abstract void createFilterGroup(@NonNull FilterGroup filterGroup,
|
||||
@NonNull UiWrapperMapDelegate wrapperDelegate,
|
||||
@NonNull UiSelectorDelegate selectorDelegate);
|
||||
|
||||
@Override
|
||||
protected ICreateUiForFiltersWorker createContentFilterWorker() {
|
||||
return new BaseCreateSearchFilterUI.CreateContentFilterUI(this, context, logic);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ICreateUiForFiltersWorker createSortFilterWorker() {
|
||||
return new BaseCreateSearchFilterUI.CreateSortFilterUI(this, context, logic);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a View that acts as a separator between two other {@link View}-Elements.
|
||||
*
|
||||
* @param layoutParams this layout will be modified to have the height of 1 -> to have a
|
||||
* the actual separator line.
|
||||
* @return the created {@link SeparatorLineView}
|
||||
*/
|
||||
@NonNull
|
||||
protected SeparatorLineView createSeparatorLine(
|
||||
@NonNull final ViewGroup.LayoutParams layoutParams) {
|
||||
final SeparatorLineView separatorLine = new SeparatorLineView(context);
|
||||
separatorLine.setBackgroundColor(getSeparatorLineColorFromTheme());
|
||||
layoutParams.height = 1; // always set the separator to the height of 1
|
||||
separatorLine.setLayoutParams(layoutParams);
|
||||
return separatorLine;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected TextView createTitleText(@NonNull final String name,
|
||||
@NonNull final ViewGroup.LayoutParams layoutParams) {
|
||||
final TextView title = new TextView(context);
|
||||
title.setText(name);
|
||||
title.setTextSize(COMPLEX_UNIT_DIP, FONT_SIZE_TITLE_ITEMS_IN_DIP);
|
||||
title.setLayoutParams(layoutParams);
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* A special view to separate two other {@link View}s.
|
||||
* <p>
|
||||
* class only needed to distinct this special view from other View based views.
|
||||
* (eg. instanceof)
|
||||
*/
|
||||
protected static final class SeparatorLineView extends View {
|
||||
|
||||
private SeparatorLineView(@NonNull final Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// 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.TypedValue;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.ICreateUiForFiltersWorker;
|
||||
import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.IUiItemWrapper;
|
||||
|
||||
/**
|
||||
* The base class to implement the search filter UI for content
|
||||
* and sort filter dialogs eg. {@link SearchFilterDialogGenerator}
|
||||
* or {@link SearchFilterOptionMenuAlikeDialogGenerator}.
|
||||
*/
|
||||
public abstract class BaseSearchFilterUiGenerator {
|
||||
protected final ICreateUiForFiltersWorker contentFilterWorker;
|
||||
protected final ICreateUiForFiltersWorker sortFilterWorker;
|
||||
protected final Context context;
|
||||
protected final SearchFilterLogic logic;
|
||||
|
||||
protected BaseSearchFilterUiGenerator(
|
||||
@NonNull final SearchFilterLogic logic,
|
||||
@NonNull final Context context) {
|
||||
this.context = context;
|
||||
this.logic = logic;
|
||||
this.contentFilterWorker = createContentFilterWorker();
|
||||
this.sortFilterWorker = createSortFilterWorker();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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() {
|
||||
logic.initContentFiltersUi(contentFilterWorker);
|
||||
logic.initSortFiltersUi(sortFilterWorker);
|
||||
doMeasurementsIfNeeded();
|
||||
// make sure that only sort filters relevant to the selected content filter are shown
|
||||
logic.showSortFilterContainerUI();
|
||||
}
|
||||
|
||||
protected void doMeasurementsIfNeeded() {
|
||||
// nothing to measure here, if you want to measure something override this method
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.fragments.list.search.filter;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.extractor.search.filter.FilterItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public abstract class BaseUiItemWrapper extends BaseItemWrapper {
|
||||
@NonNull
|
||||
protected final View view;
|
||||
|
||||
protected BaseUiItemWrapper(@NonNull final FilterItem item,
|
||||
@NonNull final View view) {
|
||||
super(item);
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(final boolean visible) {
|
||||
if (visible) {
|
||||
view.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
view.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.fragments.list.search.filter;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Every search filter option in this dialog is a {@link com.google.android.material.chip.Chip}.
|
||||
*/
|
||||
public class SearchFilterChipDialogFragment extends SearchFilterDialogFragment {
|
||||
|
||||
@Override
|
||||
protected BaseSearchFilterUiGenerator createSearchFilterDialogGenerator() {
|
||||
return new SearchFilterChipDialogGenerator(
|
||||
searchViewModel.getSearchFilterLogic(), binding.verticalScroll, requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
final Configuration configuration = getResources().getConfiguration();
|
||||
final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
|
||||
final ViewGroup.LayoutParams layoutParams = binding.getRoot().getLayoutParams();
|
||||
|
||||
if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
layoutParams.width = (int) (displayMetrics.widthPixels * 0.80f);
|
||||
} else if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
}
|
||||
|
||||
binding.getRoot().setLayoutParams(layoutParams);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
// 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.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
|
||||
import org.schabi.newpipe.extractor.search.filter.FilterGroup;
|
||||
import org.schabi.newpipe.util.DeviceUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class SearchFilterChipDialogGenerator extends SearchFilterDialogGenerator {
|
||||
|
||||
public SearchFilterChipDialogGenerator(
|
||||
@NonNull final SearchFilterLogic logic,
|
||||
@NonNull final ViewGroup root,
|
||||
@NonNull final Context context) {
|
||||
super(logic, root, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createFilterGroup(@NonNull final FilterGroup filterGroup,
|
||||
@NonNull final UiWrapperMapDelegate wrapperDelegate,
|
||||
@NonNull final UiSelectorDelegate selectorDelegate) {
|
||||
final boolean doSpanDataOverMultipleCells = true;
|
||||
final UiItemWrapperViews viewsWrapper = new UiItemWrapperViews(
|
||||
filterGroup.getIdentifier());
|
||||
|
||||
if (filterGroup.getNameId() != null) {
|
||||
final GridLayout.LayoutParams layoutParams =
|
||||
clipFreeRightColumnLayoutParams(doSpanDataOverMultipleCells);
|
||||
final TextView filterLabel = createFilterLabel(filterGroup, layoutParams);
|
||||
globalLayout.addView(filterLabel);
|
||||
viewsWrapper.add(filterLabel);
|
||||
} else if (doWeNeedASeparatorView()) {
|
||||
final SeparatorLineView separatorLineView = createSeparatorLine();
|
||||
globalLayout.addView(separatorLineView);
|
||||
viewsWrapper.add(separatorLineView);
|
||||
}
|
||||
|
||||
final ChipGroup chipGroup = new ChipGroup(context);
|
||||
chipGroup.setLayoutParams(
|
||||
setDefaultMarginInDp(clipFreeRightColumnLayoutParams(doSpanDataOverMultipleCells),
|
||||
8, 2, 4, 2));
|
||||
chipGroup.setSingleLine(false);
|
||||
chipGroup.setSingleSelection(filterGroup.isOnlyOneCheckable());
|
||||
|
||||
createUiChipElementsForFilterGroupItems(
|
||||
filterGroup, wrapperDelegate, selectorDelegate, chipGroup);
|
||||
|
||||
|
||||
wrapperDelegate.put(filterGroup.getIdentifier(), viewsWrapper);
|
||||
globalLayout.addView(chipGroup);
|
||||
viewsWrapper.add(chipGroup);
|
||||
}
|
||||
|
||||
private boolean doWeNeedASeparatorView() {
|
||||
// if 0 than there is nothing to separate
|
||||
if (globalLayout.getChildCount() == 0) {
|
||||
return false;
|
||||
}
|
||||
final View lastView = globalLayout.getChildAt(globalLayout.getChildCount() - 1);
|
||||
return !(lastView instanceof SeparatorLineView);
|
||||
}
|
||||
|
||||
private ViewGroup.MarginLayoutParams setDefaultMarginInDp(
|
||||
@NonNull final ViewGroup.MarginLayoutParams layoutParams,
|
||||
final int left, final int top, final int right, final int bottom) {
|
||||
layoutParams.setMargins(
|
||||
DeviceUtils.dpToPx(left, context),
|
||||
DeviceUtils.dpToPx(top, context),
|
||||
DeviceUtils.dpToPx(right, context),
|
||||
DeviceUtils.dpToPx(bottom, context)
|
||||
);
|
||||
return layoutParams;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// 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 androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
/**
|
||||
* A search filter dialog that also looks like a dialog aka. 'dialog style'.
|
||||
*/
|
||||
public class SearchFilterDialogFragment extends BaseSearchFilterDialogFragment {
|
||||
|
||||
protected SearchFilterDialogFragmentBinding binding;
|
||||
|
||||
@Override
|
||||
protected BaseSearchFilterUiGenerator createSearchFilterDialogGenerator() {
|
||||
return new SearchFilterDialogGenerator(
|
||||
searchViewModel.getSearchFilterLogic(), binding.verticalScroll, requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Toolbar getToolbar() {
|
||||
return binding.toolbarLayout.toolbar;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getRootView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container) {
|
||||
binding = SearchFilterDialogFragmentBinding
|
||||
.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,337 @@
|
|||
// 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.LayoutInflater;
|
||||
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.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.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class SearchFilterDialogGenerator extends BaseSearchFilterUiDialogGenerator {
|
||||
private static final int CHIP_GROUP_ELEMENTS_THRESHOLD = 2;
|
||||
private static final int CHIP_MIN_TOUCH_TARGET_SIZE_DP = 40;
|
||||
protected final GridLayout globalLayout;
|
||||
|
||||
public SearchFilterDialogGenerator(
|
||||
@NonNull final SearchFilterLogic logic,
|
||||
@NonNull final ViewGroup root,
|
||||
@NonNull final Context context) {
|
||||
super(logic, context);
|
||||
this.globalLayout = createGridLayout();
|
||||
root.addView(globalLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createTitle(@NonNull final String name,
|
||||
@NonNull final List<View> 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(@NonNull final FilterGroup filterGroup,
|
||||
@NonNull final UiWrapperMapDelegate wrapperDelegate,
|
||||
@NonNull final UiSelectorDelegate selectorDelegate) {
|
||||
final GridLayout.LayoutParams layoutParams = getLayoutParamsViews();
|
||||
boolean doSpanDataOverMultipleCells = false;
|
||||
final UiItemWrapperViews viewsWrapper = new UiItemWrapperViews(
|
||||
filterGroup.getIdentifier());
|
||||
|
||||
final TextView filterLabel;
|
||||
if (filterGroup.getNameId() != null) {
|
||||
filterLabel = createFilterLabel(filterGroup, layoutParams);
|
||||
viewsWrapper.add(filterLabel);
|
||||
} else {
|
||||
filterLabel = null;
|
||||
doSpanDataOverMultipleCells = true;
|
||||
}
|
||||
|
||||
if (filterGroup.isOnlyOneCheckable()) {
|
||||
if (filterLabel != null) {
|
||||
globalLayout.addView(filterLabel);
|
||||
}
|
||||
|
||||
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);
|
||||
doSpanDataOverMultipleCells = chooseParentViewForFilterLabelAndAdd(
|
||||
filterGroup, doSpanDataOverMultipleCells, filterLabel, chipGroup);
|
||||
|
||||
viewsWrapper.add(chipGroup);
|
||||
globalLayout.addView(chipGroup);
|
||||
chipGroup.setLayoutParams(
|
||||
clipFreeRightColumnLayoutParams(doSpanDataOverMultipleCells));
|
||||
chipGroup.setSingleLine(false);
|
||||
|
||||
createUiChipElementsForFilterGroupItems(
|
||||
filterGroup, wrapperDelegate, selectorDelegate, chipGroup);
|
||||
}
|
||||
|
||||
wrapperDelegate.put(filterGroup.getIdentifier(), viewsWrapper);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected TextView createFilterLabel(@NonNull final FilterGroup filterGroup,
|
||||
@NonNull final GridLayout.LayoutParams layoutParams) {
|
||||
final TextView filterLabel;
|
||||
filterLabel = new TextView(context);
|
||||
|
||||
filterLabel.setId(filterGroup.getIdentifier());
|
||||
filterLabel.setText(
|
||||
ServiceHelper.getTranslatedFilterString(filterGroup.getNameId(), context));
|
||||
filterLabel.setGravity(Gravity.CENTER_VERTICAL);
|
||||
setDefaultMargin(layoutParams);
|
||||
setZeroPadding(filterLabel);
|
||||
|
||||
filterLabel.setLayoutParams(layoutParams);
|
||||
return filterLabel;
|
||||
}
|
||||
|
||||
private boolean chooseParentViewForFilterLabelAndAdd(
|
||||
@NonNull final FilterGroup filterGroup,
|
||||
final boolean doSpanDataOverMultipleCells,
|
||||
@Nullable final TextView filterLabel,
|
||||
@NonNull final ChipGroup possibleParentView) {
|
||||
|
||||
boolean spanOverMultipleCells = doSpanDataOverMultipleCells;
|
||||
if (filterLabel != null) {
|
||||
// If we have more than CHIP_GROUP_ELEMENTS_THRESHOLD elements to be
|
||||
// displayed as Chips add its filterLabel as first element to ChipGroup.
|
||||
// Now the ChipGroup can be spanned over all the cells to use
|
||||
// the space better.
|
||||
if (filterGroup.getFilterItems().size() > CHIP_GROUP_ELEMENTS_THRESHOLD) {
|
||||
possibleParentView.addView(filterLabel);
|
||||
spanOverMultipleCells = true;
|
||||
} else {
|
||||
globalLayout.addView(filterLabel);
|
||||
}
|
||||
}
|
||||
return spanOverMultipleCells;
|
||||
}
|
||||
|
||||
private void createUiElementsForSingleSelectableItemsFilterGroup(
|
||||
@NonNull final FilterGroup filterGroup,
|
||||
@NonNull final UiWrapperMapDelegate wrapperDelegate,
|
||||
@NonNull final UiSelectorDelegate selectorDelegate,
|
||||
@NonNull 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);
|
||||
}
|
||||
|
||||
protected void createUiChipElementsForFilterGroupItems(
|
||||
@NonNull final FilterGroup filterGroup,
|
||||
@NonNull final UiWrapperMapDelegate wrapperDelegate,
|
||||
@NonNull final UiSelectorDelegate selectorDelegate,
|
||||
@NonNull final ChipGroup chipGroup) {
|
||||
for (final FilterItem item : filterGroup.getFilterItems()) {
|
||||
|
||||
if (item instanceof InjectFilterItem.DividerItem) {
|
||||
final InjectFilterItem.DividerItem dividerItem =
|
||||
(InjectFilterItem.DividerItem) item;
|
||||
|
||||
// For the width MATCH_PARENT is necessary as this allows the
|
||||
// dividerLabel to fill one row of ChipGroup exclusively
|
||||
final ChipGroup.LayoutParams layoutParams = new ChipGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
final TextView dividerLabel = createDividerLabel(dividerItem, layoutParams);
|
||||
chipGroup.addView(dividerLabel);
|
||||
} else {
|
||||
final Chip chip = createChipView(chipGroup, item);
|
||||
|
||||
final View.OnClickListener listener;
|
||||
listener = view -> selectorDelegate.selectFilter(view.getId());
|
||||
chip.setOnClickListener(listener);
|
||||
|
||||
chipGroup.addView(chip);
|
||||
wrapperDelegate.put(item.getIdentifier(),
|
||||
new UiItemWrapperChip(item, chip, chipGroup));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Chip createChipView(@NonNull final ChipGroup chipGroup,
|
||||
@NonNull final FilterItem item) {
|
||||
final Chip chip = (Chip) LayoutInflater.from(context).inflate(
|
||||
R.layout.chip_search_filter, chipGroup, false);
|
||||
chip.ensureAccessibleTouchTarget(
|
||||
DeviceUtils.dpToPx(CHIP_MIN_TOUCH_TARGET_SIZE_DP, context));
|
||||
chip.setText(ServiceHelper.getTranslatedFilterString(item.getNameId(), context));
|
||||
chip.setId(item.getIdentifier());
|
||||
chip.setCheckable(true);
|
||||
return chip;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TextView createDividerLabel(
|
||||
@NonNull final InjectFilterItem.DividerItem dividerItem,
|
||||
@NonNull final ViewGroup.MarginLayoutParams layoutParams) {
|
||||
final TextView dividerLabel;
|
||||
dividerLabel = new TextView(context);
|
||||
dividerLabel.setEnabled(true);
|
||||
|
||||
dividerLabel.setGravity(Gravity.CENTER_VERTICAL);
|
||||
setDefaultMargin(layoutParams);
|
||||
dividerLabel.setLayoutParams(layoutParams);
|
||||
final String menuDividerTitle =
|
||||
context.getString(dividerItem.getStringResId());
|
||||
dividerLabel.setText(menuDividerTitle);
|
||||
return dividerLabel;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected SeparatorLineView createSeparatorLine() {
|
||||
return createSeparatorLine(clipFreeRightColumnLayoutParams(true));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TextView createTitleText(final String name) {
|
||||
final TextView title = createTitleText(name,
|
||||
clipFreeRightColumnLayoutParams(true));
|
||||
title.setGravity(Gravity.CENTER);
|
||||
return title;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
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;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected 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;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private GridLayout.LayoutParams getLayoutParamsViews() {
|
||||
final GridLayout.LayoutParams layoutParams = new GridLayout.LayoutParams();
|
||||
layoutParams.setGravity(Gravity.CENTER_VERTICAL);
|
||||
setDefaultMargin(layoutParams);
|
||||
return layoutParams;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected ViewGroup.MarginLayoutParams setDefaultMargin(
|
||||
@NonNull final ViewGroup.MarginLayoutParams layoutParams) {
|
||||
layoutParams.setMargins(
|
||||
DeviceUtils.dpToPx(4, context),
|
||||
DeviceUtils.dpToPx(2, context),
|
||||
DeviceUtils.dpToPx(4, context),
|
||||
DeviceUtils.dpToPx(2, context)
|
||||
);
|
||||
return layoutParams;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected View setZeroPadding(@NonNull final View view) {
|
||||
view.setPadding(0, 0, 0, 0);
|
||||
return view;
|
||||
}
|
||||
|
||||
public static class UiItemWrapperChip extends BaseUiItemWrapper {
|
||||
|
||||
@NonNull
|
||||
private final ChipGroup chipGroup;
|
||||
|
||||
public UiItemWrapperChip(@NonNull final FilterItem item,
|
||||
@NonNull final View view,
|
||||
@NonNull final ChipGroup chipGroup) {
|
||||
super(item, view);
|
||||
this.chipGroup = chipGroup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
return ((Chip) view).isChecked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(final boolean checked) {
|
||||
((Chip) view).setChecked(checked);
|
||||
|
||||
if (checked) {
|
||||
chipGroup.check(view.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,224 @@
|
|||
// 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.util.SparseIntArray;
|
||||
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.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;
|
||||
private final FilterGroup group;
|
||||
private final BaseSearchFilterUiGenerator.UiWrapperMapDelegate wrapperDelegate;
|
||||
private final Spinner spinner;
|
||||
private final SparseIntArray id2PosMap = new SparseIntArray();
|
||||
private final SparseArrayCompat<UiItemWrapperSpinner>
|
||||
viewWrapperMap = new SparseArrayCompat<>();
|
||||
|
||||
public SearchFilterDialogSpinnerAdapter(
|
||||
@NonNull final Context context,
|
||||
@NonNull final FilterGroup group,
|
||||
@NonNull final BaseSearchFilterUiGenerator.UiWrapperMapDelegate wrapperDelegate,
|
||||
@NonNull final Spinner filterDataSpinner) {
|
||||
this.context = context;
|
||||
this.group = group;
|
||||
this.wrapperDelegate = wrapperDelegate;
|
||||
this.spinner = filterDataSpinner;
|
||||
|
||||
createViewWrappers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return group.getFilterItems().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(final int position) {
|
||||
return group.getFilterItems().get(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().get(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.getNameId(), context));
|
||||
view.setVisibility(wrappedView.getVisibility());
|
||||
view.setEnabled(wrappedView.isEnabled());
|
||||
|
||||
if (item instanceof DividerItem) {
|
||||
final DividerItem dividerItem = (DividerItem) item;
|
||||
wrappedView.setEnabled(false);
|
||||
view.setEnabled(wrappedView.isEnabled());
|
||||
final String menuDividerTitle = ">>>"
|
||||
+ context.getString(dividerItem.getStringResId()) + "<<<";
|
||||
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 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++;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
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.requireNonNull(wrappedView);
|
||||
return wrappedView.isEnabled();
|
||||
}
|
||||
|
||||
private static class UiItemWrapperSpinner
|
||||
extends BaseItemWrapper {
|
||||
@NonNull
|
||||
private final Spinner spinner;
|
||||
|
||||
/**
|
||||
* We have to store the visibility of the view and if it is enabled.
|
||||
* <p>
|
||||
* 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(@NonNull final FilterItem item,
|
||||
final int initialVisibility,
|
||||
final boolean isInitialEnabled,
|
||||
@NonNull 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,830 @@
|
|||
// 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 java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.collection.SparseArrayCompat;
|
||||
|
||||
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.
|
||||
* <p>
|
||||
* It also facilitates the generation of the Ui's according to the implemented
|
||||
* {@link ICreateUiForFiltersWorker}'s.
|
||||
*/
|
||||
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<FilterItem> userSelectedContentFilters = new ArrayList<>();
|
||||
/**
|
||||
* This list is used to communicate with NewPipeExtractor.
|
||||
* It contains only the sort filter ids that the user has selected from the UI.
|
||||
*/
|
||||
private final List<FilterItem> userSelectedSortFilters = new ArrayList<>();
|
||||
private final SearchQueryHandlerFactory searchQHFactory;
|
||||
private final ExclusiveGroups contentFilterExclusive = new ExclusiveGroups();
|
||||
private final ExclusiveGroups sortFilterExclusive = new ExclusiveGroups();
|
||||
private final SparseArrayCompat<IUiItemWrapper> contentFilterIdToUiItemMap =
|
||||
new SparseArrayCompat<>();
|
||||
private final SparseArrayCompat<IUiItemWrapper> sortFilterIdToUiItemMap =
|
||||
new SparseArrayCompat<>();
|
||||
private final SparseArrayCompat<FilterContainer> contentFilterFidToSupersetSortFilterMap =
|
||||
new SparseArrayCompat<>();
|
||||
private Callback callback;
|
||||
/**
|
||||
* This list is used to store via Icepick and eventual store as preset
|
||||
* It contains all the content filter ids that the user has selected. It
|
||||
* contains the same ids than {@link #userSelectedContentFilters}
|
||||
*/
|
||||
private List<Integer> selectedContentFilters = new ArrayList<>();
|
||||
/**
|
||||
* This list is used to store via Icepick and eventual store as preset
|
||||
* It contains all the sort filter ids that the user has selected and also
|
||||
* default id of none visible but selected sort filters.
|
||||
* It is a superset to {@link #userSelectedContentFilters}.
|
||||
*/
|
||||
private List<Integer> selectedSortFilters = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Store a reference of the sort filters Ui creator. This is needed
|
||||
* as a mechanism to tell if (the sort filter title) should be displayed or not.
|
||||
* <p>
|
||||
* The work is done via {@link ICreateUiForFiltersWorker#filtersVisible(boolean)}
|
||||
*/
|
||||
private ICreateUiForFiltersWorker uiSortFilterWorker;
|
||||
|
||||
|
||||
private SearchFilterLogic(@NonNull final SearchQueryHandlerFactory searchQHFactory,
|
||||
@Nullable final Callback callback) {
|
||||
this.searchQHFactory = searchQHFactory;
|
||||
this.callback = callback;
|
||||
initContentFilters();
|
||||
initSortFilters();
|
||||
}
|
||||
|
||||
public void setCallback(@Nullable final Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
initContentFilters();
|
||||
initSortFilters();
|
||||
deselectUiItems(contentFilterIdToUiItemMap);
|
||||
deselectUiItems(sortFilterIdToUiItemMap);
|
||||
reselectUiItems(selectedContentFilters, contentFilterIdToUiItemMap);
|
||||
reselectUiItems(selectedSortFilters, sortFilterIdToUiItemMap);
|
||||
showSortFilterContainerUI();
|
||||
}
|
||||
|
||||
private void reInitExclusiveFilterIds(@NonNull final List<Integer> selectedFilters,
|
||||
@NonNull final ExclusiveGroups exclusive) {
|
||||
checkIfIdsAreValid(selectedFilters, exclusive);
|
||||
|
||||
for (final int id : selectedFilters) {
|
||||
exclusive.ifInExclusiveGroupRemovePreviouslySelectedId(id);
|
||||
exclusive.addIdIfBelongsToExclusiveGroup(id);
|
||||
}
|
||||
}
|
||||
|
||||
public void restorePreviouslySelectedFilters(
|
||||
@Nullable final List<Integer> selectedContentFilterList,
|
||||
@Nullable final List<Integer> selectedSortFilterList) {
|
||||
if (selectedContentFilterList != null && selectedSortFilterList != null
|
||||
&& !selectedContentFilterList.isEmpty()) {
|
||||
reInitExclusiveFilterIds(selectedContentFilterList, contentFilterExclusive);
|
||||
reInitExclusiveFilterIds(selectedSortFilterList, sortFilterExclusive);
|
||||
|
||||
this.selectedContentFilters = selectedContentFilterList;
|
||||
this.selectedSortFilters = selectedSortFilterList;
|
||||
}
|
||||
|
||||
createContentFilterItemListFromIdentifierList();
|
||||
createSortFilterItemListFromIdentifiersList();
|
||||
}
|
||||
|
||||
private void reselectUiItems(
|
||||
@NonNull final List<Integer> selectedFilters,
|
||||
@NonNull final SparseArrayCompat<IUiItemWrapper> filterIdToUiItemMap) {
|
||||
for (final int id : selectedFilters) {
|
||||
final IUiItemWrapper iUiItemWrapper = filterIdToUiItemMap.get(id);
|
||||
if (iUiItemWrapper != null) {
|
||||
iUiItemWrapper.setChecked(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deselectUiItems(
|
||||
@NonNull final SparseArrayCompat<IUiItemWrapper> filterIdToUiItemMap) {
|
||||
for (int index = 0; index < filterIdToUiItemMap.size(); index++) {
|
||||
final IUiItemWrapper iUiItemWrapper = filterIdToUiItemMap.valueAt(index);
|
||||
if (iUiItemWrapper != null) {
|
||||
iUiItemWrapper.setChecked(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get copy of internal list
|
||||
@NonNull
|
||||
public ArrayList<Integer> getSelectedContentFilters() {
|
||||
return new ArrayList<>(this.selectedContentFilters);
|
||||
}
|
||||
|
||||
// get copy of internal list
|
||||
@NonNull
|
||||
public ArrayList<Integer> getSelectedSortFilters() {
|
||||
return new ArrayList<>(this.selectedSortFilters);
|
||||
}
|
||||
|
||||
// get copy of internal list, elements are not copied
|
||||
@NonNull
|
||||
public List<FilterItem> getSelectedContentFilterItems() {
|
||||
return new ArrayList<>(this.userSelectedContentFilters);
|
||||
}
|
||||
|
||||
// get copy of internal list, elements are not copied
|
||||
@NonNull
|
||||
public List<FilterItem> getSelectedSortFiltersItems() {
|
||||
return new ArrayList<>(this.userSelectedSortFilters);
|
||||
}
|
||||
|
||||
public void initContentFiltersUi(
|
||||
@NonNull final ICreateUiForFiltersWorker createUiForFiltersWorker) {
|
||||
final FilterContainer filters = searchQHFactory.getAvailableContentFilter();
|
||||
|
||||
if (filters != null && filters.getFilterGroups() != null) {
|
||||
initFiltersUi(filters.getFilterGroups(),
|
||||
contentFilterIdToUiItemMap,
|
||||
createUiForFiltersWorker);
|
||||
}
|
||||
|
||||
reselectUiItems(selectedContentFilters, contentFilterIdToUiItemMap);
|
||||
}
|
||||
|
||||
public void initSortFiltersUi(
|
||||
@NonNull final ICreateUiForFiltersWorker createUiForFiltersWorker) {
|
||||
final FilterContainer filters = searchQHFactory.getAvailableContentFilter();
|
||||
final List<FilterGroup> sortGroups = getAllSortFilterGroups(filters);
|
||||
uiSortFilterWorker = createUiForFiltersWorker;
|
||||
|
||||
initFiltersUi(sortGroups,
|
||||
sortFilterIdToUiItemMap,
|
||||
createUiForFiltersWorker);
|
||||
|
||||
reselectUiItems(selectedSortFilters, sortFilterIdToUiItemMap);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Ui elements.
|
||||
*
|
||||
* @param filterGroups the filter groups that whom a UI should be created
|
||||
* @param filterIdToUiItemMap points to a {@link FilterItem} or {@link FilterGroup}
|
||||
* corresponding actual UI element(s). This map will be first
|
||||
* called clear() on here.
|
||||
* @param createUiForFiltersWorker the implementation how to create the UI.
|
||||
*/
|
||||
private void initFiltersUi(
|
||||
@NonNull final List<FilterGroup> filterGroups,
|
||||
@NonNull final SparseArrayCompat<IUiItemWrapper> filterIdToUiItemMap,
|
||||
@NonNull final ICreateUiForFiltersWorker createUiForFiltersWorker) {
|
||||
|
||||
filterIdToUiItemMap.clear();
|
||||
Objects.requireNonNull(createUiForFiltersWorker);
|
||||
createUiForFiltersWorker.prepare();
|
||||
for (final FilterGroup filterGroup : filterGroups) {
|
||||
createUiForFiltersWorker.createFilterGroupBeforeItems(filterGroup);
|
||||
for (final FilterItem filterItem : filterGroup.getFilterItems()) {
|
||||
createUiForFiltersWorker.createFilterItem(filterItem, filterGroup);
|
||||
}
|
||||
createUiForFiltersWorker.createFilterGroupAfterItems(filterGroup);
|
||||
}
|
||||
createUiForFiltersWorker.finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Init the content filter logical states.
|
||||
* <p>
|
||||
* - 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 corresponding exclusive object (either for content
|
||||
* or sort) filter array
|
||||
* @param selectedFilters corresponding selected filter ids
|
||||
* @param fidToSupersetSortFilterMap null possible, only for content filters relevant
|
||||
*/
|
||||
private void initFilters(
|
||||
@NonNull final List<FilterGroup> filterGroups,
|
||||
@NonNull final ExclusiveGroups exclusive,
|
||||
@NonNull final List<Integer> selectedFilters,
|
||||
@Nullable final SparseArrayCompat<FilterContainer> fidToSupersetSortFilterMap) {
|
||||
selectedFilters.clear();
|
||||
exclusive.clear();
|
||||
|
||||
for (final FilterGroup filterGroup : filterGroups) {
|
||||
if (filterGroup.isOnlyOneCheckable()) {
|
||||
exclusive.addGroupToExclusiveGroupsMap(filterGroup.getIdentifier());
|
||||
}
|
||||
|
||||
// is the default selected filter for this group
|
||||
final int defaultId = filterGroup.getDefaultSelectedFilterId();
|
||||
|
||||
for (final FilterItem item : filterGroup.getFilterItems()) {
|
||||
if (fidToSupersetSortFilterMap != null) {
|
||||
fidToSupersetSortFilterMap.put(item.getIdentifier(),
|
||||
filterGroup.getAllSortFilters());
|
||||
}
|
||||
exclusive.putFilterIdToItsGroupId(item.getIdentifier(),
|
||||
filterGroup.getIdentifier());
|
||||
}
|
||||
|
||||
if (defaultId != ITEM_IDENTIFIER_UNKNOWN) {
|
||||
exclusive.handleIdInExclusiveGroup(defaultId, selectedFilters);
|
||||
}
|
||||
}
|
||||
|
||||
checkIfIdsAreValid(selectedFilters, exclusive);
|
||||
}
|
||||
|
||||
private void checkIfIdsAreValid(@NonNull final List<Integer> selectedFilters,
|
||||
@NonNull final ExclusiveGroups exclusive) {
|
||||
for (final int id : selectedFilters) {
|
||||
if (!exclusive.filterIdToGroupIdMapContainsId(id)) {
|
||||
throw new RuntimeException("The id " + id + " is invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initContentFilters() {
|
||||
final FilterContainer filters = searchQHFactory.getAvailableContentFilter();
|
||||
contentFilterFidToSupersetSortFilterMap.clear();
|
||||
|
||||
if (filters != null && filters.getFilterGroups() != null) {
|
||||
initFilters(filters.getFilterGroups(),
|
||||
contentFilterExclusive, selectedContentFilters,
|
||||
contentFilterFidToSupersetSortFilterMap);
|
||||
}
|
||||
}
|
||||
|
||||
private void initSortFilters() {
|
||||
final FilterContainer filters = searchQHFactory.getAvailableContentFilter();
|
||||
final List<FilterGroup> sortGroups = getAllSortFilterGroups(filters);
|
||||
initFilters(sortGroups, sortFilterExclusive, selectedSortFilters, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare content filter list with the actual {@link FilterItem}s to send to the library.
|
||||
* <p>
|
||||
* The list is created through the {@link #userSelectedContentFilters} identifiers list.
|
||||
* This identifiers refer to {@link FilterItem}s.
|
||||
* <p>
|
||||
* {@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.
|
||||
* <p>
|
||||
* The list is created through the {@link #userSelectedSortFilters} identifiers list.
|
||||
* This identifiers refer to {@link FilterItem}s.
|
||||
* <p>
|
||||
* {@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 content filter ids list
|
||||
*/
|
||||
private void showSortFilterIdsContainerUI(@NonNull final List<Integer> contentFilterIds) {
|
||||
for (final int contentFilterId : contentFilterIds) {
|
||||
showSortFilterIdContainerUI(contentFilterId);
|
||||
}
|
||||
}
|
||||
|
||||
private void notifySortFiltersVisibility() {
|
||||
boolean sortFilterVisible = false;
|
||||
if (uiSortFilterWorker != null) {
|
||||
for (final int contentFilterId : selectedContentFilters) {
|
||||
sortFilterVisible = searchQHFactory
|
||||
.getContentFilterSortFilterVariant(contentFilterId) != null;
|
||||
if (sortFilterVisible) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
uiSortFilterWorker.filtersVisible(sortFilterVisible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show only the sort filters that are available for a given content filter id.
|
||||
*
|
||||
* @param contentFilterId a content filter id and <b>not</b> a sort filter id.
|
||||
*/
|
||||
private void showSortFilterIdContainerUI(final int contentFilterId) {
|
||||
final FilterContainer subsetFilterContainer =
|
||||
searchQHFactory.getContentFilterSortFilterVariant(contentFilterId);
|
||||
|
||||
final FilterContainer supersetFilterContainer =
|
||||
contentFilterFidToSupersetSortFilterMap.get(contentFilterId);
|
||||
if (subsetFilterContainer != null) {
|
||||
if (supersetFilterContainer == null) {
|
||||
throw new RuntimeException(
|
||||
"supersetFilterContainer should never be null here");
|
||||
}
|
||||
|
||||
setUiItemsVisibility(supersetFilterContainer, false, sortFilterIdToUiItemMap);
|
||||
setUiItemsVisibility(subsetFilterContainer, true, sortFilterIdToUiItemMap);
|
||||
} else {
|
||||
if (supersetFilterContainer != null) {
|
||||
setUiItemsVisibility(supersetFilterContainer, false,
|
||||
sortFilterIdToUiItemMap);
|
||||
}
|
||||
}
|
||||
notifySortFiltersVisibility();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is only used to show the all sort filters for measurement of the width.
|
||||
* <p>
|
||||
* See {@link SearchFilterOptionMenuAlikeDialogGenerator}
|
||||
*/
|
||||
protected void showAllAvailableSortFilters() {
|
||||
for (int index = 0; index < contentFilterFidToSupersetSortFilterMap.size(); index++) {
|
||||
final FilterContainer container =
|
||||
contentFilterFidToSupersetSortFilterMap.valueAt(index);
|
||||
if (container != null) {
|
||||
setUiItemsVisibility(container, true, sortFilterIdToUiItemMap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setUiItemsVisibility(
|
||||
@Nullable final FilterContainer filters,
|
||||
final boolean isVisible,
|
||||
@NonNull final SparseArrayCompat<IUiItemWrapper> filterIdToUiItemMap) {
|
||||
if (filters != null && filters.getFilterGroups() != null) {
|
||||
for (final FilterGroup filterGroup : filters.getFilterGroups()) {
|
||||
setUiItemVisible(isVisible, filterIdToUiItemMap, filterGroup.getIdentifier());
|
||||
for (final FilterItem item : filterGroup.getFilterItems()) {
|
||||
setUiItemVisible(isVisible, filterIdToUiItemMap, item.getIdentifier());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setUiItemVisible(
|
||||
final boolean isVisible,
|
||||
@NonNull final SparseArrayCompat<IUiItemWrapper> filterIdToUiItemMap,
|
||||
final int id) {
|
||||
final IUiItemWrapper uiWrapper = filterIdToUiItemMap.get(id);
|
||||
if (uiWrapper != null) {
|
||||
uiWrapper.setVisible(isVisible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sort filter groups for the content filters.
|
||||
* It has to have all content filter groups that are available for a service.
|
||||
*
|
||||
* @param filters the content filters
|
||||
* @return the sort filter groups. Empty list if either param filters or no
|
||||
* filter groups available
|
||||
*/
|
||||
@NonNull
|
||||
private List<FilterGroup> getAllSortFilterGroups(@Nullable final FilterContainer filters) {
|
||||
if (filters != null && filters.getFilterGroups() != null) {
|
||||
final List<FilterGroup> sortGroups = new ArrayList<>();
|
||||
for (final FilterGroup filterGroup : filters.getFilterGroups()) {
|
||||
final FilterContainer sf = filterGroup.getAllSortFilters();
|
||||
if (sf != null && sf.getFilterGroups() != null) {
|
||||
sortGroups.addAll(sf.getFilterGroups());
|
||||
}
|
||||
}
|
||||
return sortGroups;
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
protected void handleIdInNonExclusiveGroup(final int filterId,
|
||||
@Nullable final IUiItemWrapper uiItemWrapper,
|
||||
@NonNull final List<Integer> selectedFilter) {
|
||||
if (uiItemWrapper != null) { // could be null if there is no UI
|
||||
if (uiItemWrapper.isChecked()) {
|
||||
if (!selectedFilter.contains(filterId)) {
|
||||
selectedFilter.add(filterId);
|
||||
}
|
||||
} else { // remove from list
|
||||
if (selectedFilter.contains(filterId)) {
|
||||
selectedFilter.remove((Integer) filterId);
|
||||
}
|
||||
}
|
||||
} else { // we have no UI
|
||||
if (!selectedFilter.contains(filterId)) {
|
||||
selectedFilter.add(filterId);
|
||||
} else {
|
||||
selectedFilter.remove((Integer) filterId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void selectContentFilter(final int filterId) {
|
||||
selectFilter(filterId, contentFilterIdToUiItemMap, selectedContentFilters,
|
||||
contentFilterExclusive);
|
||||
showSortFilterIdContainerUI(filterId);
|
||||
}
|
||||
|
||||
public synchronized void selectSortFilter(final int filterId) {
|
||||
selectFilter(filterId, sortFilterIdToUiItemMap, selectedSortFilters, sortFilterExclusive);
|
||||
}
|
||||
|
||||
private void selectFilter(
|
||||
final int id,
|
||||
@NonNull final SparseArrayCompat<IUiItemWrapper> filterIdToUiItemMap,
|
||||
@NonNull final List<Integer> selectedFilter,
|
||||
@NonNull final ExclusiveGroups exclusive) {
|
||||
final IUiItemWrapper uiItemWrapper =
|
||||
filterIdToUiItemMap.get(id);
|
||||
|
||||
// here we remove/add the by the UI (de)selected id.
|
||||
if (exclusive.handleIdInExclusiveGroup(id, selectedFilter)) {
|
||||
if (uiItemWrapper != null && !uiItemWrapper.isChecked()) {
|
||||
uiItemWrapper.setChecked(true);
|
||||
}
|
||||
} else {
|
||||
handleIdInNonExclusiveGroup(id, uiItemWrapper, selectedFilter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the content and sort filters {@link FilterItem}'s lists for a now filtered
|
||||
* search.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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
|
||||
*/
|
||||
public void addContentFilterUiWrapperToItemMap(
|
||||
final int id,
|
||||
@NonNull 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.
|
||||
* <p>
|
||||
* 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
|
||||
*/
|
||||
public void addSortFilterUiWrapperToItemMap(
|
||||
final int id,
|
||||
@NonNull 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();
|
||||
|
||||
/**
|
||||
* 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}.
|
||||
*
|
||||
* <b>Note:</b> 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.
|
||||
* <p>
|
||||
* -> This method is called *before* the {@link #createFilterItem(FilterItem, FilterGroup)}
|
||||
*
|
||||
* @param filterGroup one group each time from {@link FilterContainer#getFilterGroups()}
|
||||
*/
|
||||
void createFilterGroupBeforeItems(@NonNull 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(@NonNull FilterItem filterItem, @NonNull FilterGroup filterGroup);
|
||||
|
||||
/**
|
||||
* Create Ui elements specifically related to the {@link FilterGroup} itself.
|
||||
* But it could also be used for creating items.
|
||||
* <p>
|
||||
* -> This method is called *after* the {@link #createFilterItem(FilterItem, FilterGroup)}
|
||||
*
|
||||
* @param filterGroup one group each time from {@link FilterContainer#getFilterGroups()}
|
||||
*/
|
||||
void createFilterGroupAfterItems(@NonNull 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(@NonNull List<FilterItem> userSelectedContentFilter,
|
||||
@NonNull List<FilterItem> userSelectedSortFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track and handle filters of groups in which only one {@link FilterItem} can be selected.
|
||||
* <p>
|
||||
* 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 SparseArrayCompat<Integer> actualSelectedFilterIdInExclusiveGroupMap =
|
||||
new SparseArrayCompat<>();
|
||||
/**
|
||||
* To quickly determine if a content filter group supports
|
||||
* only one item selected (exclusiveness), we need a set that resembles that.
|
||||
*/
|
||||
private final Set<Integer> exclusiveGroupsIdSet = new HashSet<>();
|
||||
/**
|
||||
* To quickly determine if a content filter id belongs to an exclusive group.
|
||||
* This maps works in conjunction with {@link #exclusiveGroupsIdSet}
|
||||
*/
|
||||
private final SparseArrayCompat<Integer> filterIdToGroupIdMap =
|
||||
new SparseArrayCompat<>();
|
||||
|
||||
/**
|
||||
* Clear {@link #exclusiveGroupsIdSet} and {@link #filterIdToGroupIdMap}.
|
||||
*/
|
||||
public void clear() {
|
||||
exclusiveGroupsIdSet.clear();
|
||||
filterIdToGroupIdMap.clear();
|
||||
actualSelectedFilterIdInExclusiveGroupMap.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if filter id is valid.
|
||||
*
|
||||
* @param filterId the filter id to check
|
||||
* @return true if valid
|
||||
*/
|
||||
public boolean filterIdToGroupIdMapContainsId(final int filterId) {
|
||||
return filterIdToGroupIdMap.indexOfKey(filterId) >= 0;
|
||||
}
|
||||
|
||||
public boolean isFilterIdPartOfAnExclusiveGroup(final int filterId) {
|
||||
if (filterIdToGroupIdMapContainsId(filterId)) {
|
||||
final int filterGroupId =
|
||||
Objects.requireNonNull(filterIdToGroupIdMap.get(filterId));
|
||||
return exclusiveGroupsIdSet.contains(filterGroupId);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param filterId the id of a {@link FilterItem}
|
||||
* @param selectedFilter the list of filter Ids that could contain the given id
|
||||
* @return true if exclusive group
|
||||
*/
|
||||
private boolean handleIdInExclusiveGroup(final int filterId,
|
||||
@NonNull final List<Integer> selectedFilter) {
|
||||
// case exclusive group selection
|
||||
if (isFilterIdPartOfAnExclusiveGroup(filterId)) {
|
||||
final int previousSelectedId =
|
||||
ifInExclusiveGroupRemovePreviouslySelectedId(filterId);
|
||||
if (selectedFilter.contains(previousSelectedId)) {
|
||||
selectedFilter.remove((Integer) previousSelectedId);
|
||||
selectedFilter.add(filterId);
|
||||
} else if (previousSelectedId == ITEM_IDENTIFIER_UNKNOWN) {
|
||||
selectedFilter.add(filterId);
|
||||
}
|
||||
addIdIfBelongsToExclusiveGroup(filterId);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert filter ids with corresponding group ids.
|
||||
* <p>
|
||||
* 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 =
|
||||
Objects.requireNonNull(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.
|
||||
* <p>
|
||||
* 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 =
|
||||
Objects.requireNonNull(filterIdToGroupIdMap.get(filterId));
|
||||
|
||||
final int index = actualSelectedFilterIdInExclusiveGroupMap.indexOfKey(filterGroupId);
|
||||
if (exclusiveGroupsIdSet.contains(filterGroupId) && index >= 0) {
|
||||
previousFilterId = actualSelectedFilterIdInExclusiveGroupMap.valueAt(index);
|
||||
actualSelectedFilterIdInExclusiveGroupMap.removeAt(index);
|
||||
}
|
||||
return previousFilterId;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory {
|
||||
private Factory() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create variant of {@link SearchFilterLogic}.
|
||||
*
|
||||
* @param logicVariant the variant {@link Variant}.
|
||||
* @param searchQHFactory of the service
|
||||
* @param callback if you want to get the data the user has requested by calling
|
||||
* {@link SearchFilterLogic#prepareForSearch()}
|
||||
* @return instance of {@link SearchFilterLogic}.
|
||||
*/
|
||||
@NonNull
|
||||
public static SearchFilterLogic create(
|
||||
@NonNull final Variant logicVariant,
|
||||
@NonNull final SearchQueryHandlerFactory searchQHFactory,
|
||||
@Nullable final Callback callback) {
|
||||
switch (logicVariant) {
|
||||
|
||||
case SEARCH_FILTER_LOGIC_LEGACY: // the case we are using SearchFragmentLegacy
|
||||
return new SearchFilterLogic(searchQHFactory, callback) {
|
||||
@Override
|
||||
protected void handleIdInNonExclusiveGroup(
|
||||
final int filterId,
|
||||
@Nullable final IUiItemWrapper uiItemWrapper,
|
||||
@NonNull final List<Integer> selectedFilter) {
|
||||
|
||||
if (null != uiItemWrapper) {
|
||||
// for the action menu based UI we have to toggle first
|
||||
// to be compatible with the SearchFilterLogic
|
||||
uiItemWrapper.setChecked(!uiItemWrapper.isChecked());
|
||||
}
|
||||
super.handleIdInNonExclusiveGroup(
|
||||
filterId, uiItemWrapper, selectedFilter);
|
||||
}
|
||||
};
|
||||
|
||||
default:
|
||||
case SEARCH_FILTER_LOGIC_DEFAULT:
|
||||
return new SearchFilterLogic(searchQHFactory, callback);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Variant {
|
||||
SEARCH_FILTER_LOGIC_DEFAULT,
|
||||
SEARCH_FILTER_LOGIC_LEGACY
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.fragments.list.search.filter;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.schabi.newpipe.databinding.SearchFilterOptionMenuAlikeDialogFragmentBinding;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
||||
/**
|
||||
* A search filter dialog that looks like a action menu aka. 'action menu style'.
|
||||
*/
|
||||
public class SearchFilterOptionMenuAlikeDialogFragment extends BaseSearchFilterDialogFragment {
|
||||
|
||||
private SearchFilterOptionMenuAlikeDialogFragmentBinding binding;
|
||||
|
||||
@Override
|
||||
protected BaseSearchFilterUiGenerator createSearchFilterDialogGenerator() {
|
||||
return new SearchFilterOptionMenuAlikeDialogGenerator(
|
||||
searchViewModel.getSearchFilterLogic(), binding.verticalScroll, requireContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Toolbar getToolbar() {
|
||||
return binding.toolbarLayout.toolbar;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getRootView(@NonNull final LayoutInflater inflater,
|
||||
final ViewGroup container) {
|
||||
binding = SearchFilterOptionMenuAlikeDialogFragmentBinding
|
||||
.inflate(inflater, container, false);
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View view, final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
// place the dialog in the 'action menu position'
|
||||
setDialogGravity(Gravity.END | Gravity.TOP);
|
||||
}
|
||||
|
||||
private void setDialogGravity(final int gravity) {
|
||||
final Dialog dialog = getDialog();
|
||||
if (dialog != null) {
|
||||
final Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
final WindowManager.LayoutParams layoutParams = window.getAttributes();
|
||||
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
layoutParams.horizontalMargin = 0;
|
||||
layoutParams.gravity = gravity;
|
||||
layoutParams.dimAmount = 0;
|
||||
layoutParams.flags &= ~WindowManager.LayoutParams.FLAG_DIM_BEHIND;
|
||||
window.setAttributes(layoutParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initToolbar(final @NonNull Toolbar toolbar) {
|
||||
super.initToolbar(toolbar);
|
||||
// no room for a title
|
||||
toolbar.setTitle("");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,365 @@
|
|||
// 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.CheckBox;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RadioGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
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.List;
|
||||
|
||||
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;
|
||||
private static final float FONT_SIZE_SELECTABLE_VIEW_ITEMS_IN_DIP = 18f;
|
||||
private static final int VIEW_ITEMS_MIN_WIDTH_IN_DIP = 168;
|
||||
private final LinearLayout globalLayout;
|
||||
|
||||
public SearchFilterOptionMenuAlikeDialogGenerator(
|
||||
@NonNull final SearchFilterLogic logic,
|
||||
@NonNull final ViewGroup root,
|
||||
@NonNull final Context context) {
|
||||
super(logic, context);
|
||||
this.globalLayout = createLinearLayout();
|
||||
root.addView(globalLayout);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doMeasurementsIfNeeded() {
|
||||
measureWidthOfChildrenAndResizeToWidest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize all width of {@link #globalLayout} children without tag {@link #NO_RESIZE_VIEW_TAG}.
|
||||
* <p>
|
||||
* Initially this method was only used to resize the width of separator line
|
||||
* views created by {@link #createSeparatorLine()}. But now also the views
|
||||
* the user will interact with are set to the widest child.
|
||||
* <p>
|
||||
* Reasons:
|
||||
* 1. Separator lines should be as wide as the widest UI element but this
|
||||
* can only be determined on runtime
|
||||
* 2. Other view elements more specific checkable/selectable should also
|
||||
* expand their width over the complete dialog width to be easier to select
|
||||
*/
|
||||
private void measureWidthOfChildrenAndResizeToWidest() {
|
||||
logic.showAllAvailableSortFilters();
|
||||
|
||||
// initialize width with a passable default width
|
||||
int widestViewInPx = DeviceUtils.dpToPx(VIEW_ITEMS_MIN_WIDTH_IN_DIP, context);
|
||||
final int noOfChildren = globalLayout.getChildCount();
|
||||
|
||||
for (int x = 0; x < noOfChildren; x++) {
|
||||
final View childView = globalLayout.getChildAt(x);
|
||||
childView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
||||
final int width = childView.getMeasuredWidth();
|
||||
if (width > widestViewInPx) {
|
||||
widestViewInPx = width;
|
||||
}
|
||||
}
|
||||
|
||||
for (int x = 0; x < noOfChildren; x++) {
|
||||
final View childView = globalLayout.getChildAt(x);
|
||||
|
||||
if (childView.getTag() != NO_RESIZE_VIEW_TAG) {
|
||||
final ViewGroup.LayoutParams layoutParams = childView.getLayoutParams();
|
||||
layoutParams.width = widestViewInPx;
|
||||
childView.setLayoutParams(layoutParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createTitle(@NonNull final String name,
|
||||
@NonNull final List<View> titleViewElements) {
|
||||
final TextView titleView = createTitleText(name);
|
||||
titleView.setTag(NO_RESIZE_VIEW_TAG);
|
||||
final View separatorLine = createSeparatorLine();
|
||||
final View separatorLine2 = createSeparatorLine();
|
||||
final View separatorLine3 = createSeparatorLine();
|
||||
|
||||
globalLayout.addView(separatorLine);
|
||||
globalLayout.addView(separatorLine2);
|
||||
globalLayout.addView(titleView);
|
||||
globalLayout.addView(separatorLine3);
|
||||
|
||||
titleViewElements.add(titleView);
|
||||
titleViewElements.add(separatorLine);
|
||||
titleViewElements.add(separatorLine2);
|
||||
titleViewElements.add(separatorLine3);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void createFilterGroup(@NonNull final FilterGroup filterGroup,
|
||||
@NonNull final UiWrapperMapDelegate wrapperDelegate,
|
||||
@NonNull final UiSelectorDelegate selectorDelegate) {
|
||||
final UiItemWrapperViews viewsWrapper = new UiItemWrapperViews(
|
||||
filterGroup.getIdentifier());
|
||||
|
||||
final View separatorLine = createSeparatorLine();
|
||||
globalLayout.addView(separatorLine);
|
||||
viewsWrapper.add(separatorLine);
|
||||
|
||||
if (filterGroup.getNameId() != null) {
|
||||
final TextView filterLabel =
|
||||
createFilterGroupLabel(filterGroup, getLayoutParamsLabelLeft());
|
||||
globalLayout.addView(filterLabel);
|
||||
viewsWrapper.add(filterLabel);
|
||||
}
|
||||
|
||||
if (filterGroup.isOnlyOneCheckable()) {
|
||||
|
||||
final RadioGroup radioGroup = new RadioGroup(context);
|
||||
radioGroup.setLayoutParams(getLayoutParamsViews());
|
||||
|
||||
createUiElementsForSingleSelectableItemsFilterGroup(
|
||||
filterGroup, wrapperDelegate, selectorDelegate, radioGroup);
|
||||
|
||||
globalLayout.addView(radioGroup);
|
||||
viewsWrapper.add(radioGroup);
|
||||
|
||||
} else { // multiple items in FilterGroup selectable
|
||||
createUiElementsForMultipleSelectableItemsFilterGroup(
|
||||
filterGroup, wrapperDelegate, selectorDelegate);
|
||||
}
|
||||
|
||||
wrapperDelegate.put(filterGroup.getIdentifier(), viewsWrapper);
|
||||
}
|
||||
|
||||
private void createUiElementsForSingleSelectableItemsFilterGroup(
|
||||
@NonNull final FilterGroup filterGroup,
|
||||
@NonNull final UiWrapperMapDelegate wrapperDelegate,
|
||||
@NonNull final UiSelectorDelegate selectorDelegate,
|
||||
@NonNull final RadioGroup radioGroup) {
|
||||
for (final FilterItem item : filterGroup.getFilterItems()) {
|
||||
|
||||
final View view;
|
||||
if (item instanceof DividerItem) {
|
||||
view = createDividerTextView(item, getLayoutParamsViews());
|
||||
} else {
|
||||
view = createViewItemRadio(item, getLayoutParamsViews());
|
||||
|
||||
wrapperDelegate.put(item.getIdentifier(),
|
||||
new UiItemWrapperCheckBoxAndRadioButton(
|
||||
item, view, radioGroup));
|
||||
|
||||
final View.OnClickListener listener = v -> {
|
||||
if (v != null) {
|
||||
selectorDelegate.selectFilter(v.getId());
|
||||
}
|
||||
};
|
||||
view.setOnClickListener(listener);
|
||||
}
|
||||
radioGroup.addView(view);
|
||||
}
|
||||
}
|
||||
|
||||
private void createUiElementsForMultipleSelectableItemsFilterGroup(
|
||||
@NonNull final FilterGroup filterGroup,
|
||||
@NonNull final UiWrapperMapDelegate wrapperDelegate,
|
||||
@NonNull final UiSelectorDelegate selectorDelegate) {
|
||||
for (final FilterItem item : filterGroup.getFilterItems()) {
|
||||
final View view;
|
||||
if (item instanceof DividerItem) {
|
||||
view = createDividerTextView(item, getLayoutParamsViews());
|
||||
} else {
|
||||
final CheckBox checkBox = createCheckBox(item, getLayoutParamsViews());
|
||||
|
||||
wrapperDelegate.put(item.getIdentifier(),
|
||||
new UiItemWrapperCheckBoxAndRadioButton(
|
||||
item, checkBox, null));
|
||||
|
||||
final View.OnClickListener listener = v -> {
|
||||
if (v != null) {
|
||||
selectorDelegate.selectFilter(v.getId());
|
||||
}
|
||||
};
|
||||
checkBox.setOnClickListener(listener);
|
||||
|
||||
view = checkBox;
|
||||
}
|
||||
globalLayout.addView(view);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private LinearLayout createLinearLayout() {
|
||||
final LinearLayout linearLayout = new LinearLayout(context);
|
||||
|
||||
linearLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(1, 1);
|
||||
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
layoutParams.setMargins(
|
||||
DeviceUtils.dpToPx(2, context),
|
||||
DeviceUtils.dpToPx(2, context),
|
||||
DeviceUtils.dpToPx(2, context),
|
||||
DeviceUtils.dpToPx(2, context));
|
||||
linearLayout.setLayoutParams(layoutParams);
|
||||
|
||||
return linearLayout;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private LinearLayout.LayoutParams getLayoutForSeparatorLine() {
|
||||
final LinearLayout.LayoutParams layoutParams = getLayoutParamsLabelLeft();
|
||||
layoutParams.width = 0;
|
||||
layoutParams.gravity = Gravity.CENTER_HORIZONTAL;
|
||||
return layoutParams;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private View createSeparatorLine() {
|
||||
return createSeparatorLine(getLayoutForSeparatorLine());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TextView createTitleText(final String name) {
|
||||
final LinearLayout.LayoutParams layoutParams = getLayoutParamsLabelLeft();
|
||||
layoutParams.gravity = Gravity.CENTER_HORIZONTAL;
|
||||
final TextView title = createTitleText(name, layoutParams);
|
||||
setPadding(title, 5);
|
||||
return title;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private View setPadding(@NonNull final View view, final int sizeInDip) {
|
||||
final int sizeInPx = DeviceUtils.dpToPx(sizeInDip, context);
|
||||
view.setPadding(
|
||||
sizeInPx,
|
||||
sizeInPx,
|
||||
sizeInPx,
|
||||
sizeInPx);
|
||||
return view;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TextView createFilterGroupLabel(@NonNull final FilterGroup filterGroup,
|
||||
@NonNull final ViewGroup.LayoutParams layoutParams) {
|
||||
final TextView filterLabel = new TextView(context);
|
||||
filterLabel.setId(filterGroup.getIdentifier());
|
||||
filterLabel.setText(ServiceHelper
|
||||
.getTranslatedFilterString(filterGroup.getNameId(), context));
|
||||
filterLabel.setGravity(Gravity.TOP);
|
||||
// resizing not needed as view is not selectable
|
||||
filterLabel.setTag(NO_RESIZE_VIEW_TAG);
|
||||
filterLabel.setLayoutParams(layoutParams);
|
||||
return filterLabel;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private CheckBox createCheckBox(@NonNull final FilterItem item,
|
||||
@NonNull final ViewGroup.LayoutParams layoutParams) {
|
||||
final CheckBox checkBox = new CheckBox(context);
|
||||
checkBox.setLayoutParams(layoutParams);
|
||||
checkBox.setText(ServiceHelper.getTranslatedFilterString(
|
||||
item.getNameId(), context));
|
||||
checkBox.setId(item.getIdentifier());
|
||||
checkBox.setTextSize(COMPLEX_UNIT_DIP, FONT_SIZE_SELECTABLE_VIEW_ITEMS_IN_DIP);
|
||||
return checkBox;
|
||||
}
|
||||
|
||||
@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 =
|
||||
context.getString(dividerItem.getStringResId());
|
||||
view.setText(menuDividerTitle);
|
||||
view.setGravity(Gravity.TOP);
|
||||
view.setLayoutParams(layoutParams);
|
||||
return view;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private RadioButton createViewItemRadio(@NonNull final FilterItem item,
|
||||
@NonNull final ViewGroup.LayoutParams layoutParams) {
|
||||
final RadioButton view = new RadioButton(context);
|
||||
view.setId(item.getIdentifier());
|
||||
view.setText(ServiceHelper.getTranslatedFilterString(item.getNameId(), context));
|
||||
view.setLayoutParams(layoutParams);
|
||||
view.setTextSize(COMPLEX_UNIT_DIP, FONT_SIZE_SELECTABLE_VIEW_ITEMS_IN_DIP);
|
||||
return view;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private LinearLayout.LayoutParams getLayoutParamsViews() {
|
||||
final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
layoutParams.setMargins(
|
||||
DeviceUtils.dpToPx(4, context),
|
||||
DeviceUtils.dpToPx(8, context),
|
||||
DeviceUtils.dpToPx(4, context),
|
||||
DeviceUtils.dpToPx(8, context));
|
||||
return layoutParams;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private LinearLayout.LayoutParams getLayoutParamsLabelLeft() {
|
||||
final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
layoutParams.setMargins(
|
||||
DeviceUtils.dpToPx(2, context),
|
||||
DeviceUtils.dpToPx(2, context),
|
||||
DeviceUtils.dpToPx(2, context),
|
||||
DeviceUtils.dpToPx(2, context));
|
||||
return layoutParams;
|
||||
}
|
||||
|
||||
private static final class UiItemWrapperCheckBoxAndRadioButton
|
||||
extends BaseUiItemWrapper {
|
||||
|
||||
@Nullable
|
||||
private final View group;
|
||||
|
||||
private UiItemWrapperCheckBoxAndRadioButton(@NonNull final FilterItem item,
|
||||
@NonNull final View view,
|
||||
@Nullable final View group) {
|
||||
super(item, view);
|
||||
this.group = group;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
if (view instanceof RadioButton) {
|
||||
return ((RadioButton) view).isChecked();
|
||||
} else if (view instanceof CheckBox) {
|
||||
return ((CheckBox) view).isChecked();
|
||||
} else {
|
||||
return view.isSelected();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(final boolean checked) {
|
||||
if (checked && group instanceof RadioGroup) {
|
||||
((RadioGroup) group).check(view.getId());
|
||||
} else if (view instanceof CheckBox) {
|
||||
((CheckBox) view).setChecked(checked);
|
||||
} else {
|
||||
view.setSelected(checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,303 @@
|
|||
// 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.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import org.schabi.newpipe.R;
|
||||
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.util.ServiceHelper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The implementation of the action menu based 'dialog'.
|
||||
*/
|
||||
public class SearchFilterUIOptionMenu extends BaseSearchFilterUiGenerator {
|
||||
|
||||
// Menu groups identifier
|
||||
private static final int MENU_GROUP_SEARCH_RESET_BUTTONS = 0;
|
||||
// give them negative ids to not conflict with the ids of the filters
|
||||
private static final int MENU_ID_SEARCH_BUTTON = -100;
|
||||
private static final int MENU_ID_RESET_BUTTON = -101;
|
||||
private Menu menu = null;
|
||||
// initialize with first group id -> next group after the search/reset buttons group
|
||||
private int newLastUsedGroupId = MENU_GROUP_SEARCH_RESET_BUTTONS + 1;
|
||||
private int firstSortFilterGroupId;
|
||||
|
||||
public SearchFilterUIOptionMenu(
|
||||
@NonNull final SearchFilterLogic logic,
|
||||
@NonNull final Context context) {
|
||||
super(logic, context);
|
||||
}
|
||||
|
||||
int getLastUsedGroupIdThanIncrement() {
|
||||
return newLastUsedGroupId++;
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private void alwaysShowMenuItemIcon(final Menu theMenu) {
|
||||
// always show icons
|
||||
if (theMenu instanceof MenuBuilder) {
|
||||
final MenuBuilder builder = ((MenuBuilder) theMenu);
|
||||
builder.setOptionalIconsVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void createSearchUI(@NonNull final Menu theMenu) {
|
||||
this.menu = theMenu;
|
||||
alwaysShowMenuItemIcon(theMenu);
|
||||
|
||||
createSearchUI();
|
||||
|
||||
MenuCompat.setGroupDividerEnabled(theMenu, true);
|
||||
}
|
||||
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
if (item.getGroupId() == MENU_GROUP_SEARCH_RESET_BUTTONS
|
||||
&& item.getItemId() == MENU_ID_SEARCH_BUTTON) {
|
||||
logic.prepareForSearch();
|
||||
} else { // all other menu groups -> reset, content filters and sort filters
|
||||
|
||||
// main part for holding onto the menu -> not closing it
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW);
|
||||
item.setActionView(new View(context));
|
||||
item.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemActionExpand(final MenuItem item) {
|
||||
if (item.getGroupId() == MENU_GROUP_SEARCH_RESET_BUTTONS
|
||||
&& item.getItemId() == MENU_ID_RESET_BUTTON) {
|
||||
logic.reset();
|
||||
} else if (item.getGroupId() < firstSortFilterGroupId) { // content filters
|
||||
final int filterId = item.getItemId();
|
||||
logic.selectContentFilter(filterId);
|
||||
} else { // the sort filters
|
||||
Log.d(TAG, "onMenuItemActionExpand: sort filters are here");
|
||||
logic.selectSortFilter(item.getItemId());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMenuItemActionCollapse(final MenuItem item) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ICreateUiForFiltersWorker createSortFilterWorker() {
|
||||
return new CreateSortFilterUI();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ICreateUiForFiltersWorker createContentFilterWorker() {
|
||||
return new CreateContentFilterUI();
|
||||
}
|
||||
|
||||
private static class UiItemWrapper implements IUiItemWrapper {
|
||||
|
||||
private final MenuItem item;
|
||||
|
||||
UiItemWrapper(final MenuItem item) {
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(final boolean visible) {
|
||||
item.setVisible(visible);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemId() {
|
||||
return item.getItemId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
return item.isChecked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(final boolean checked) {
|
||||
item.setChecked(checked);
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateContentFilterUI implements ICreateUiForFiltersWorker {
|
||||
|
||||
/**
|
||||
* MenuItem's that should not be checkable.
|
||||
*/
|
||||
final List<MenuItem> nonCheckableMenuItems = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* {@link Menu#setGroupCheckable(int, boolean, boolean)} makes all {@link MenuItem}
|
||||
* checkable.
|
||||
* <p>
|
||||
* We do not want a group header or a group divider to be checkable. Therefore this method
|
||||
* calls above mentioned method and afterwards makes all items uncheckable that are placed
|
||||
* inside {@link #nonCheckableMenuItems}.
|
||||
*
|
||||
* @param isOnlyOneCheckable is in group only one selection allowed.
|
||||
* @param groupId which group should be affected
|
||||
*/
|
||||
private void makeAllowedMenuItemInGroupCheckable(final boolean isOnlyOneCheckable,
|
||||
final int groupId) {
|
||||
// this method makes all MenuItem's checkable
|
||||
menu.setGroupCheckable(groupId, true, isOnlyOneCheckable);
|
||||
// uncheckable unwanted
|
||||
for (final MenuItem uncheckableItem : nonCheckableMenuItems) {
|
||||
if (uncheckableItem != null) {
|
||||
uncheckableItem.setCheckable(false);
|
||||
}
|
||||
}
|
||||
nonCheckableMenuItems.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare() {
|
||||
// create the search button
|
||||
menu.add(MENU_GROUP_SEARCH_RESET_BUTTONS,
|
||||
MENU_ID_SEARCH_BUTTON,
|
||||
0,
|
||||
context.getString(R.string.search))
|
||||
.setEnabled(true)
|
||||
.setCheckable(false)
|
||||
.setIcon(R.drawable.ic_search);
|
||||
|
||||
menu.add(MENU_GROUP_SEARCH_RESET_BUTTONS,
|
||||
MENU_ID_RESET_BUTTON,
|
||||
0,
|
||||
context.getString(R.string.playback_reset))
|
||||
.setEnabled(true)
|
||||
.setCheckable(false)
|
||||
.setIcon(R.drawable.ic_settings_backup_restore);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterGroupBeforeItems(
|
||||
@NonNull final FilterGroup filterGroup) {
|
||||
if (filterGroup.getNameId() != null) {
|
||||
createNotEnabledAndUncheckableGroupTitleMenuItem(
|
||||
FilterContainer.ITEM_IDENTIFIER_UNKNOWN, filterGroup.getNameId());
|
||||
}
|
||||
}
|
||||
|
||||
protected MenuItem createNotEnabledAndUncheckableGroupTitleMenuItem(
|
||||
final int identifier,
|
||||
final LibraryStringIds nameId) {
|
||||
final MenuItem item = menu.add(
|
||||
newLastUsedGroupId,
|
||||
identifier,
|
||||
0,
|
||||
ServiceHelper.getTranslatedFilterString(nameId, context));
|
||||
item.setEnabled(false);
|
||||
|
||||
nonCheckableMenuItems.add(item);
|
||||
|
||||
return item;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterItem(@NonNull final FilterItem filterItem,
|
||||
@NonNull final FilterGroup filterGroup) {
|
||||
final MenuItem item = createMenuItem(filterItem);
|
||||
|
||||
if (filterItem instanceof DividerItem) {
|
||||
final DividerItem dividerItem = (DividerItem) filterItem;
|
||||
final String menuDividerTitle = ">>>"
|
||||
+ context.getString(dividerItem.getStringResId())
|
||||
+ "<<<";
|
||||
item.setTitle(menuDividerTitle);
|
||||
item.setEnabled(false);
|
||||
nonCheckableMenuItems.add(item);
|
||||
}
|
||||
|
||||
logic.addContentFilterUiWrapperToItemMap(filterItem.getIdentifier(),
|
||||
new UiItemWrapper(item));
|
||||
}
|
||||
|
||||
protected MenuItem createMenuItem(final FilterItem filterItem) {
|
||||
return menu.add(newLastUsedGroupId,
|
||||
filterItem.getIdentifier(),
|
||||
0,
|
||||
ServiceHelper.getTranslatedFilterString(filterItem.getNameId(), context));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterGroupAfterItems(@NonNull final FilterGroup filterGroup) {
|
||||
makeAllowedMenuItemInGroupCheckable(filterGroup.isOnlyOneCheckable(),
|
||||
getLastUsedGroupIdThanIncrement());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
firstSortFilterGroupId = newLastUsedGroupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void filtersVisible(final boolean areFiltersVisible) {
|
||||
// no implementation here as there is no 'sort filter' title as MenuItem
|
||||
}
|
||||
}
|
||||
|
||||
private class CreateSortFilterUI extends CreateContentFilterUI {
|
||||
|
||||
private void addSortFilterUiToItemMap(final int id,
|
||||
final MenuItem item) {
|
||||
logic.addSortFilterUiWrapperToItemMap(id, new UiItemWrapper(item));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare() {
|
||||
firstSortFilterGroupId = newLastUsedGroupId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterGroupBeforeItems(
|
||||
@NonNull final FilterGroup filterGroup) {
|
||||
if (filterGroup.getNameId() != null) {
|
||||
final MenuItem item = createNotEnabledAndUncheckableGroupTitleMenuItem(
|
||||
filterGroup.getIdentifier(), filterGroup.getNameId());
|
||||
addSortFilterUiToItemMap(filterGroup.getIdentifier(), item);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterItem(@NonNull final FilterItem filterItem,
|
||||
@NonNull final FilterGroup filterGroup) {
|
||||
final MenuItem item = createMenuItem(filterItem);
|
||||
addSortFilterUiToItemMap(filterItem.getIdentifier(), item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
// no implementation here as we do not need to clean up anything or whatever
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.fragments.list.search.filter;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Wrapper for views that are either just labels or eg. a RadioGroup container
|
||||
* etc. that represent a {@link org.schabi.newpipe.extractor.search.filter.FilterGroup}.
|
||||
*/
|
||||
final class UiItemWrapperViews implements SearchFilterLogic.IUiItemWrapper {
|
||||
|
||||
private final int itemId;
|
||||
private final List<View> views = new ArrayList<>();
|
||||
|
||||
UiItemWrapperViews(final int itemId) {
|
||||
this.itemId = itemId;
|
||||
}
|
||||
|
||||
public void add(@NonNull final View view) {
|
||||
this.views.add(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(final boolean visible) {
|
||||
for (final View view : views) {
|
||||
if (visible) {
|
||||
view.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
view.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemId() {
|
||||
return this.itemId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
boolean isChecked = false;
|
||||
for (final View view : views) {
|
||||
if (view.isSelected()) {
|
||||
isChecked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isChecked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(final boolean checked) {
|
||||
// not relevant as here views are wrapped that are either just labels or eg. a
|
||||
// RadioGroup container etc. that represent a FilterGroup.
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import androidx.annotation.StringRes;
|
|||
import org.schabi.newpipe.R;
|
||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs;
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler;
|
||||
import org.schabi.newpipe.extractor.search.filter.FilterItem;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
@ -20,16 +21,11 @@ public final class ChannelTabHelper {
|
|||
* @param tab the channel tab to check
|
||||
* @return whether the tab should contain (playable) streams or not
|
||||
*/
|
||||
public static boolean isStreamsTab(final String tab) {
|
||||
switch (tab) {
|
||||
case ChannelTabs.VIDEOS:
|
||||
case ChannelTabs.TRACKS:
|
||||
case ChannelTabs.SHORTS:
|
||||
case ChannelTabs.LIVESTREAMS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
public static boolean isStreamsTab(final FilterItem tab) {
|
||||
return tab.equals(ChannelTabs.VIDEOS)
|
||||
|| tab.equals(ChannelTabs.TRACKS)
|
||||
|| tab.equals(ChannelTabs.SHORTS)
|
||||
|| tab.equals(ChannelTabs.LIVESTREAMS);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -37,7 +33,7 @@ public final class ChannelTabHelper {
|
|||
* @return whether the tab should contain (playable) streams or not
|
||||
*/
|
||||
public static boolean isStreamsTab(final ListLinkHandler tab) {
|
||||
final List<String> contentFilters = tab.getContentFilters();
|
||||
final List<FilterItem> contentFilters = tab.getContentFilters();
|
||||
if (contentFilters.isEmpty()) {
|
||||
return false; // this should never happen, but check just to be sure
|
||||
} else {
|
||||
|
@ -46,63 +42,57 @@ public final class ChannelTabHelper {
|
|||
}
|
||||
|
||||
@StringRes
|
||||
private static int getShowTabKey(final String tab) {
|
||||
switch (tab) {
|
||||
case ChannelTabs.VIDEOS:
|
||||
return R.string.show_channel_tabs_videos;
|
||||
case ChannelTabs.TRACKS:
|
||||
return R.string.show_channel_tabs_tracks;
|
||||
case ChannelTabs.SHORTS:
|
||||
return R.string.show_channel_tabs_shorts;
|
||||
case ChannelTabs.LIVESTREAMS:
|
||||
return R.string.show_channel_tabs_livestreams;
|
||||
case ChannelTabs.CHANNELS:
|
||||
return R.string.show_channel_tabs_channels;
|
||||
case ChannelTabs.PLAYLISTS:
|
||||
return R.string.show_channel_tabs_playlists;
|
||||
case ChannelTabs.ALBUMS:
|
||||
return R.string.show_channel_tabs_albums;
|
||||
default:
|
||||
return -1;
|
||||
private static int getShowTabKey(final FilterItem tab) {
|
||||
if (tab.equals(ChannelTabs.VIDEOS)) {
|
||||
return R.string.show_channel_tabs_videos;
|
||||
} else if (tab.equals(ChannelTabs.TRACKS)) {
|
||||
return R.string.show_channel_tabs_tracks;
|
||||
} else if (tab.equals(ChannelTabs.SHORTS)) {
|
||||
return R.string.show_channel_tabs_shorts;
|
||||
} else if (tab.equals(ChannelTabs.LIVESTREAMS)) {
|
||||
return R.string.show_channel_tabs_livestreams;
|
||||
} else if (tab.equals(ChannelTabs.CHANNELS)) {
|
||||
return R.string.show_channel_tabs_channels;
|
||||
} else if (tab.equals(ChannelTabs.PLAYLISTS)) {
|
||||
return R.string.show_channel_tabs_playlists;
|
||||
} else if (tab.equals(ChannelTabs.ALBUMS)) {
|
||||
return R.string.show_channel_tabs_albums;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@StringRes
|
||||
private static int getFetchFeedTabKey(final String tab) {
|
||||
switch (tab) {
|
||||
case ChannelTabs.VIDEOS:
|
||||
return R.string.fetch_channel_tabs_videos;
|
||||
case ChannelTabs.TRACKS:
|
||||
return R.string.fetch_channel_tabs_tracks;
|
||||
case ChannelTabs.SHORTS:
|
||||
return R.string.fetch_channel_tabs_shorts;
|
||||
case ChannelTabs.LIVESTREAMS:
|
||||
return R.string.fetch_channel_tabs_livestreams;
|
||||
default:
|
||||
return -1;
|
||||
private static int getFetchFeedTabKey(final FilterItem tab) {
|
||||
if (tab.equals(ChannelTabs.VIDEOS)) {
|
||||
return R.string.fetch_channel_tabs_videos;
|
||||
} else if (tab.equals(ChannelTabs.TRACKS)) {
|
||||
return R.string.fetch_channel_tabs_tracks;
|
||||
} else if (tab.equals(ChannelTabs.SHORTS)) {
|
||||
return R.string.fetch_channel_tabs_shorts;
|
||||
} else if (tab.equals(ChannelTabs.LIVESTREAMS)) {
|
||||
return R.string.fetch_channel_tabs_livestreams;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@StringRes
|
||||
public static int getTranslationKey(final String tab) {
|
||||
switch (tab) {
|
||||
case ChannelTabs.VIDEOS:
|
||||
return R.string.channel_tab_videos;
|
||||
case ChannelTabs.TRACKS:
|
||||
return R.string.channel_tab_tracks;
|
||||
case ChannelTabs.SHORTS:
|
||||
return R.string.channel_tab_shorts;
|
||||
case ChannelTabs.LIVESTREAMS:
|
||||
return R.string.channel_tab_livestreams;
|
||||
case ChannelTabs.CHANNELS:
|
||||
return R.string.channel_tab_channels;
|
||||
case ChannelTabs.PLAYLISTS:
|
||||
return R.string.channel_tab_playlists;
|
||||
case ChannelTabs.ALBUMS:
|
||||
return R.string.channel_tab_albums;
|
||||
default:
|
||||
return R.string.unknown_content;
|
||||
public static int getTranslationKey(final FilterItem tab) {
|
||||
if (tab.equals(ChannelTabs.VIDEOS)) {
|
||||
return R.string.channel_tab_videos;
|
||||
} else if (tab.equals(ChannelTabs.TRACKS)) {
|
||||
return R.string.channel_tab_tracks;
|
||||
} else if (tab.equals(ChannelTabs.SHORTS)) {
|
||||
return R.string.channel_tab_shorts;
|
||||
} else if (tab.equals(ChannelTabs.LIVESTREAMS)) {
|
||||
return R.string.channel_tab_livestreams;
|
||||
} else if (tab.equals(ChannelTabs.CHANNELS)) {
|
||||
return R.string.channel_tab_channels;
|
||||
} else if (tab.equals(ChannelTabs.PLAYLISTS)) {
|
||||
return R.string.channel_tab_playlists;
|
||||
} else if (tab.equals(ChannelTabs.ALBUMS)) {
|
||||
return R.string.channel_tab_albums;
|
||||
}
|
||||
return R.string.unknown_content;
|
||||
}
|
||||
|
||||
public static boolean showChannelTab(final Context context,
|
||||
|
@ -119,7 +109,7 @@ public final class ChannelTabHelper {
|
|||
|
||||
public static boolean showChannelTab(final Context context,
|
||||
final SharedPreferences sharedPreferences,
|
||||
final String tab) {
|
||||
final FilterItem tab) {
|
||||
final int key = ChannelTabHelper.getShowTabKey(tab);
|
||||
if (key == -1) {
|
||||
return false;
|
||||
|
@ -130,7 +120,7 @@ public final class ChannelTabHelper {
|
|||
public static boolean fetchFeedChannelTab(final Context context,
|
||||
final SharedPreferences sharedPreferences,
|
||||
final ListLinkHandler tab) {
|
||||
final List<String> contentFilters = tab.getContentFilters();
|
||||
final List<FilterItem> contentFilters = tab.getContentFilters();
|
||||
if (contentFilters.isEmpty()) {
|
||||
return false; // this should never happen, but check just to be sure
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import android.util.Log;
|
|||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.schabi.newpipe.extractor.search.filter.FilterItem;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
@ -74,8 +76,8 @@ public final class ExtractorHelper {
|
|||
}
|
||||
|
||||
public static Single<SearchInfo> searchFor(final int serviceId, final String searchString,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter) {
|
||||
final List<FilterItem> contentFilter,
|
||||
final List<FilterItem> sortFilter) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
SearchInfo.getInfo(NewPipe.getService(serviceId),
|
||||
|
@ -87,8 +89,8 @@ public final class ExtractorHelper {
|
|||
public static Single<InfoItemsPage<InfoItem>> getMoreSearchItems(
|
||||
final int serviceId,
|
||||
final String searchString,
|
||||
final List<String> contentFilter,
|
||||
final String sortFilter,
|
||||
final List<FilterItem> contentFilter,
|
||||
final List<FilterItem> sortFilter,
|
||||
final Page page) {
|
||||
checkServiceId(serviceId);
|
||||
return Single.fromCallable(() ->
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
package org.schabi.newpipe.util;
|
||||
|
||||
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import com.grack.nanojson.JsonObject;
|
||||
import com.grack.nanojson.JsonParser;
|
||||
import com.grack.nanojson.JsonParserException;
|
||||
|
@ -20,15 +12,182 @@ import org.schabi.newpipe.extractor.NewPipe;
|
|||
import org.schabi.newpipe.extractor.ServiceList;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
import org.schabi.newpipe.extractor.search.filter.LibraryStringIds;
|
||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
|
||||
|
||||
public final class ServiceHelper {
|
||||
private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube;
|
||||
/**
|
||||
* Map all available {@link LibraryStringIds} ids to resource ids available in strings.xml.
|
||||
*/
|
||||
private static final Map<LibraryStringIds, Integer> LIBRARY_STRING_ID_TO_RES_ID_MAP =
|
||||
new EnumMap<>(LibraryStringIds.class);
|
||||
|
||||
private ServiceHelper() { }
|
||||
static {
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_10_30_MIN,
|
||||
R.string.search_filters_10_30_min);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_2_10_MIN,
|
||||
R.string.search_filters_2_10_min);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_360,
|
||||
R.string.search_filters_360);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_3D,
|
||||
R.string.search_filters_3d);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_4_20_MIN,
|
||||
R.string.search_filters_4_20_min);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_4K,
|
||||
R.string.search_filters_4k);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_ADDED,
|
||||
R.string.search_filters_added);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_ALBUMS,
|
||||
R.string.albums);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_ANY_TIME,
|
||||
R.string.search_filters_any_time);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_ALL,
|
||||
R.string.all);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_ARTISTS_AND_LABELS,
|
||||
R.string.search_filters_artists_and_labels);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_ASCENDING,
|
||||
R.string.search_filters_ascending);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_CCOMMONS,
|
||||
R.string.search_filters_ccommons);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_CHANNELS,
|
||||
R.string.channels);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_CONFERENCES,
|
||||
R.string.conferences);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_CREATION_DATE,
|
||||
R.string.search_filters_creation_date);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_DATE,
|
||||
R.string.search_filters_date);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_DURATION,
|
||||
R.string.search_filters_duration);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_EVENTS,
|
||||
R.string.events);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_FEATURES,
|
||||
R.string.search_filters_features);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_GREATER_30_MIN,
|
||||
R.string.search_filters_greater_30_min);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_HD,
|
||||
R.string.search_filters_hd);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_HDR,
|
||||
R.string.search_filters_hdr);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_KIND,
|
||||
R.string.search_filters_kind);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_LAST_30_DAYS,
|
||||
R.string.search_filters_last_30_days);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_LAST_7_DAYS,
|
||||
R.string.search_filters_last_7_days);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_LAST_HOUR,
|
||||
R.string.search_filters_last_hour);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_LAST_YEAR,
|
||||
R.string.search_filters_last_year);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_LENGTH,
|
||||
R.string.search_filters_length);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_LESS_2_MIN,
|
||||
R.string.search_filters_less_2_min);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_LICENSE,
|
||||
R.string.search_filters_license);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_LIKES,
|
||||
R.string.detail_likes_img_view_description);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_LIVE,
|
||||
R.string.duration_live);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_LOCATION,
|
||||
R.string.search_filters_location);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_LONG_GREATER_10_MIN,
|
||||
R.string.search_filters_long_greater_10_min);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_MEDIUM_4_10_MIN,
|
||||
R.string.search_filters_medium_4_10_min);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_THIS_MONTH,
|
||||
R.string.search_filters_this_month);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_ARTISTS,
|
||||
R.string.artists);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_SONGS,
|
||||
R.string.songs);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_NAME,
|
||||
R.string.name);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_NO,
|
||||
R.string.search_filters_no);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_OVER_20_MIN,
|
||||
R.string.search_filters_over_20_min);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_PAST_DAY,
|
||||
R.string.search_filters_past_day);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_PAST_HOUR,
|
||||
R.string.search_filters_past_hour);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_PAST_MONTH,
|
||||
R.string.search_filters_past_month);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_PAST_WEEK,
|
||||
R.string.search_filters_past_week);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_PAST_YEAR,
|
||||
R.string.search_filters_past_year);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_PLAYLISTS,
|
||||
R.string.playlists);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_PUBLISH_DATE,
|
||||
R.string.search_filters_publish_date);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_PUBLISHED,
|
||||
R.string.search_filters_published);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_PURCHASED,
|
||||
R.string.search_filters_published);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_RATING,
|
||||
R.string.search_filters_rating);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_RELEVANCE,
|
||||
R.string.search_filters_relevance);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_SENSITIVE,
|
||||
R.string.search_filters_sensitive);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_SEPIASEARCH,
|
||||
R.string.search_filters_sepiasearch);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_SHORT_LESS_4_MIN,
|
||||
R.string.search_filters_short_less_4_min);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_SORT_BY,
|
||||
R.string.search_filters_sort_by);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_SORT_ORDER,
|
||||
R.string.search_filters_sort_order);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_SUBTITLES,
|
||||
R.string.search_filters_subtitles);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_TO_MODIFY_COMMERCIALLY,
|
||||
R.string.search_filters_to_modify_commercially);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_TODAY,
|
||||
R.string.search_filters_today);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_TRACKS,
|
||||
R.string.tracks);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_UNDER_4_MIN,
|
||||
R.string.search_filters_under_4_min);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_UPLOAD_DATE,
|
||||
R.string.search_filters_upload_date);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_USERS,
|
||||
R.string.users);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_VIDEOS,
|
||||
R.string.videos_string);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_VIEWS,
|
||||
R.string.search_filters_views);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_VOD_VIDEOS,
|
||||
R.string.search_filters_vod_videos);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_VR180,
|
||||
R.string.search_filters_vr180);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_THIS_WEEK,
|
||||
R.string.search_filters_this_week);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_THIS_YEAR,
|
||||
R.string.search_filters_this_year);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_YES,
|
||||
R.string.search_filters_yes);
|
||||
LIBRARY_STRING_ID_TO_RES_ID_MAP.put(LibraryStringIds.SEARCH_FILTERS_YOUTUBE_MUSIC,
|
||||
R.string.search_filters_youtube_music);
|
||||
}
|
||||
|
||||
private ServiceHelper() {
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
public static int getIcon(final int serviceId) {
|
||||
|
@ -48,38 +207,6 @@ public final class ServiceHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public static String getTranslatedFilterString(final String filter, final Context c) {
|
||||
switch (filter) {
|
||||
case "all":
|
||||
return c.getString(R.string.all);
|
||||
case "videos":
|
||||
case "sepia_videos":
|
||||
case "music_videos":
|
||||
return c.getString(R.string.videos_string);
|
||||
case "channels":
|
||||
return c.getString(R.string.channels);
|
||||
case "playlists":
|
||||
case "music_playlists":
|
||||
return c.getString(R.string.playlists);
|
||||
case "tracks":
|
||||
return c.getString(R.string.tracks);
|
||||
case "users":
|
||||
return c.getString(R.string.users);
|
||||
case "conferences":
|
||||
return c.getString(R.string.conferences);
|
||||
case "events":
|
||||
return c.getString(R.string.events);
|
||||
case "music_songs":
|
||||
return c.getString(R.string.songs);
|
||||
case "music_albums":
|
||||
return c.getString(R.string.albums);
|
||||
case "music_artists":
|
||||
return c.getString(R.string.artists);
|
||||
default:
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a resource string with instructions for importing subscriptions for each service.
|
||||
*
|
||||
|
@ -107,12 +234,10 @@ public final class ServiceHelper {
|
|||
*/
|
||||
@StringRes
|
||||
public static int getImportInstructionsHint(final int serviceId) {
|
||||
switch (serviceId) {
|
||||
case 1:
|
||||
return R.string.import_soundcloud_instructions_hint;
|
||||
default:
|
||||
return -1;
|
||||
if (serviceId == 1) {
|
||||
return R.string.import_soundcloud_instructions_hint;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static int getSelectedServiceId(final Context context) {
|
||||
|
@ -210,4 +335,14 @@ public final class ServiceHelper {
|
|||
initService(context, s.getServiceId());
|
||||
}
|
||||
}
|
||||
|
||||
public static String getTranslatedFilterString(@NonNull final LibraryStringIds stringId,
|
||||
@NonNull final Context context) {
|
||||
if (LIBRARY_STRING_ID_TO_RES_ID_MAP.containsKey(stringId)) {
|
||||
return context.getString(
|
||||
Objects.requireNonNull(LIBRARY_STRING_ID_TO_RES_ID_MAP.get(stringId)));
|
||||
} else {
|
||||
return stringId.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- adding more contrast than @color/mtrl_chip_background_color if a Chip is selected -->
|
||||
<item android:alpha="0.30" android:color="?attr/colorOnSurface" android:state_enabled="true" android:state_selected="true"/>
|
||||
<item android:alpha="0.18" android:color="?attr/colorOnSurface" android:state_enabled="true" android:state_checked="true"/>
|
||||
<!-- 12% of 87% opacity -->
|
||||
<item android:alpha="0.10" android:color="?attr/colorOnSurface" android:state_enabled="true"/>
|
||||
<item android:alpha="0.12" android:color="?attr/colorOnSurface"/>
|
||||
</selector>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This is used to inflate a chip with a Material theme, otherwise it would crash -->
|
||||
<!-- Theme.MaterialComponents.DayNight is used to guarantee auto day/night switching -->
|
||||
<com.google.android.material.chip.Chip xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
style="@style/ChipSearchFilter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/Theme.MaterialComponents.DayNight" />
|
|
@ -0,0 +1,23 @@
|
|||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" >
|
||||
|
||||
<include
|
||||
android:id="@+id/toolbar_layout"
|
||||
layout="@layout/toolbar_layout" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/toolbar_layout">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/vertical_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</RelativeLayout>
|
|
@ -0,0 +1,22 @@
|
|||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include
|
||||
android:id="@+id/toolbar_layout"
|
||||
layout="@layout/toolbar_layout" />
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/vertical_scroll"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,15 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:id="@+id/reset"
|
||||
android:title="@string/playback_reset"
|
||||
android:icon="@drawable/ic_settings_backup_restore"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/search"
|
||||
android:title="@string/search"
|
||||
android:icon="@drawable/ic_search"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_filter"
|
||||
android:title="@string/filter"
|
||||
android:icon="@drawable/ic_sort"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
|
@ -806,4 +806,64 @@
|
|||
<string name="share_playlist">Wiedergabeliste teilen</string>
|
||||
<string name="share_playlist_with_titles_message">Teile die Wiedergabeliste mit Details wie dem Namen der Wiedergabeliste und den Videotiteln oder als einfache Liste von Video-URLs</string>
|
||||
<string name="video_details_list_item">- %1$s: %2$s</string>
|
||||
</resources>
|
||||
<string name="filter_search_sort_filters">Sortierfilter</string>
|
||||
<string name="filter_search_content_filters">Inhaltsfilter</string>
|
||||
<string name="search_filters_10_30_min">10–30 min</string>
|
||||
<string name="search_filters_2_10_min">2–10 min</string>
|
||||
<string name="search_filters_360">360°</string>
|
||||
<string name="search_filters_3d">3D</string>
|
||||
<string name="search_filters_4_20_min">4–20 min</string>
|
||||
<string name="search_filters_4k">4K</string>
|
||||
<string name="search_filters_added">Hinzugefügt</string>
|
||||
<string name="search_filters_artists_and_labels">Künstler & Labels</string>
|
||||
<string name="search_filters_any_time">Jederzeit</string>
|
||||
<string name="search_filters_ascending">Aufsteigend</string>
|
||||
<string name="search_filters_ccommons">Creative Commons</string>
|
||||
<string name="search_filters_creation_date">Erstellungsdatum</string>
|
||||
<string name="search_filters_date">Datum</string>
|
||||
<string name="search_filters_duration">Dauer</string>
|
||||
<string name="search_filters_features">Eigenschaften</string>
|
||||
<string name="search_filters_greater_30_min">> 30 min</string>
|
||||
<string name="search_filters_hd">HD</string>
|
||||
<string name="search_filters_hdr">HDR</string>
|
||||
<string name="search_filters_kind">Art</string>
|
||||
<string name="search_filters_last_30_days">Letzte 30 Tage</string>
|
||||
<string name="search_filters_last_7_days">Letzte 7 Tage</string>
|
||||
<string name="search_filters_last_hour">Letzte Stunde</string>
|
||||
<string name="search_filters_last_year">Letztes Jahr</string>
|
||||
<string name="search_filters_length">Länge</string>
|
||||
<string name="search_filters_less_2_min">< 2 min</string>
|
||||
<string name="search_filters_license">Lizenz</string>
|
||||
<string name="search_filters_location">Standort</string>
|
||||
<string name="search_filters_long_greater_10_min">Lang (> 10 min)</string>
|
||||
<string name="search_filters_medium_4_10_min">Mittel (4–10 min)</string>
|
||||
<string name="search_filters_no">Nein</string>
|
||||
<string name="search_filters_over_20_min">Über 20 min</string>
|
||||
<string name="search_filters_past_day">Vorheriger Tag</string>
|
||||
<string name="search_filters_past_hour">Vorige Stunde</string>
|
||||
<string name="search_filters_past_month">Vergangener Monat</string>
|
||||
<string name="search_filters_past_week">Vergangene Woche</string>
|
||||
<string name="search_filters_past_year">Vergangenes Jahr</string>
|
||||
<string name="search_filters_publish_date">Erscheinungsdatum</string>
|
||||
<string name="search_filters_published">Veröffentlicht</string>
|
||||
<string name="search_filters_purchased">Gekauft</string>
|
||||
<string name="search_filters_rating">Bewertung</string>
|
||||
<string name="search_filters_relevance">Relevanz</string>
|
||||
<string name="search_filters_sensitive">Sensibler Inhalt</string>
|
||||
<string name="search_filters_sepiasearch">SepiaSuche</string>
|
||||
<string name="search_filters_short_less_4_min">Kurz (< 4 min)</string>
|
||||
<string name="search_filters_sort_by">Sortiert nach</string>
|
||||
<string name="search_filters_sort_order">Sortierung</string>
|
||||
<string name="search_filters_subtitles">Untertitel</string>
|
||||
<string name="search_filters_to_modify_commercially">Gewerblich nutzbar</string>
|
||||
<string name="search_filters_today">Heute</string>
|
||||
<string name="search_filters_under_4_min">Unter 4 min</string>
|
||||
<string name="search_filters_upload_date">Hochladedatum</string>
|
||||
<string name="search_filters_views">Aufrufe</string>
|
||||
<string name="search_filters_vod_videos">VOD-Videos</string>
|
||||
<string name="search_filters_vr180">VR180</string>
|
||||
<string name="search_filters_this_month">Dieser Monat</string>
|
||||
<string name="search_filters_this_week">Diese Woche</string>
|
||||
<string name="search_filters_this_year">Dieses Jahr</string>
|
||||
<string name="search_filters_yes">Ja</string>
|
||||
</resources>
|
||||
|
|
|
@ -1383,6 +1383,28 @@
|
|||
<item>@string/card</item>
|
||||
</string-array>
|
||||
|
||||
<string name="search_filter_ui_key">search_filter_ui</string>
|
||||
<string name="search_filter_ui_value">@string/search_filter_ui_dialog_key</string>
|
||||
|
||||
<string name="search_filter_ui_dialog_key">dialog</string>
|
||||
<string name="search_filter_ui_option_menu_style_key">style</string>
|
||||
<string name="search_filter_ui_option_menu_legacy_key">legacy</string>
|
||||
<string name="search_filter_ui_chip_dialog_key">chip</string>
|
||||
|
||||
<string-array name="search_filter_ui_values">
|
||||
<item>@string/search_filter_ui_dialog_key</item>
|
||||
<item>@string/search_filter_ui_option_menu_style_key</item>
|
||||
<item>@string/search_filter_ui_option_menu_legacy_key</item>
|
||||
<item>@string/search_filter_ui_chip_dialog_key</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="search_filter_ui_description">
|
||||
<item>@string/search_filter_ui_dialog</item>
|
||||
<item>@string/search_filter_ui_style</item>
|
||||
<item>@string/search_filter_ui_legacy</item>
|
||||
<item>@string/search_filter_ui_chip_dialog</item>
|
||||
</string-array>
|
||||
|
||||
<string name="tablet_mode_key">tablet_mode</string>
|
||||
|
||||
<string name="tablet_mode_auto_key">auto</string>
|
||||
|
|
|
@ -845,4 +845,78 @@
|
|||
</plurals>
|
||||
<string name="show_more">Show more</string>
|
||||
<string name="show_less">Show less</string>
|
||||
|
||||
<!-- begin - strings for search filter feature (NewPipeExtractor Services) -->
|
||||
<string name="search_filters_10_30_min">10–30 min</string>
|
||||
<string name="search_filters_2_10_min">2–10 min</string>
|
||||
<string name="search_filters_360">360°</string>
|
||||
<string name="search_filters_3d">3D</string>
|
||||
<string name="search_filters_4_20_min">4–20 min</string>
|
||||
<string name="search_filters_4k">4K</string>
|
||||
<string name="search_filters_added">Added</string>
|
||||
<string name="search_filters_artists_and_labels">artists & labels</string>
|
||||
<string name="search_filters_any_time">Any time</string>
|
||||
<string name="search_filters_ascending">Ascending</string>
|
||||
<string name="search_filters_ccommons">Creative Commons</string>
|
||||
<string name="search_filters_creation_date">Creation date</string>
|
||||
<string name="search_filters_date">Date</string>
|
||||
<string name="search_filters_duration">Duration</string>
|
||||
<string name="search_filters_features">Features</string>
|
||||
<string name="search_filters_greater_30_min">> 30 min</string>
|
||||
<string name="search_filters_hd">HD</string>
|
||||
<string name="search_filters_hdr">HDR</string>
|
||||
<string name="search_filters_kind">Kind</string>
|
||||
<string name="search_filters_last_30_days">Last 30 days</string>
|
||||
<string name="search_filters_last_7_days">Last 7 days</string>
|
||||
<string name="search_filters_last_hour">Last hour</string>
|
||||
<string name="search_filters_last_year">last year</string>
|
||||
<string name="search_filters_length">Length</string>
|
||||
<string name="search_filters_less_2_min">< 2 min</string>
|
||||
<string name="search_filters_license">License</string>
|
||||
<string name="search_filters_location">Location</string>
|
||||
<string name="search_filters_long_greater_10_min">Long (> 10 min)</string>
|
||||
<string name="search_filters_medium_4_10_min">Medium (4–10 min)</string>
|
||||
<string name="search_filters_no">No</string>
|
||||
<string name="search_filters_over_20_min">Over 20 min</string>
|
||||
<string name="search_filters_past_day">Past day</string>
|
||||
<string name="search_filters_past_hour">Past hour</string>
|
||||
<string name="search_filters_past_month">Past month</string>
|
||||
<string name="search_filters_past_week">Past week</string>
|
||||
<string name="search_filters_past_year">Past year</string>
|
||||
<string name="search_filters_publish_date">Publish date</string>
|
||||
<string name="search_filters_published">Published</string>
|
||||
<string name="search_filters_purchased">Purchased</string>
|
||||
<string name="search_filters_rating">Rating</string>
|
||||
<string name="search_filters_relevance">Relevance</string>
|
||||
<string name="search_filters_sensitive">Sensitive</string>
|
||||
<string name="search_filters_sepiasearch">SepiaSearch</string>
|
||||
<string name="search_filters_short_less_4_min">Short (< 4 min)</string>
|
||||
<string name="search_filters_sort_by">Sort by</string>
|
||||
<string name="search_filters_sort_order">Sort order</string>
|
||||
<string name="search_filters_subtitles">Subtitles</string>
|
||||
<string name="search_filters_to_modify_commercially">To modify commercially</string>
|
||||
<string name="search_filters_today">Today</string>
|
||||
<string name="search_filters_under_4_min">Under 4 min</string>
|
||||
<string name="search_filters_upload_date">Upload Date</string>
|
||||
<string name="search_filters_views">Views</string>
|
||||
<string name="search_filters_vod_videos">VOD videos</string>
|
||||
<string name="search_filters_vr180">VR180</string>
|
||||
<string name="search_filters_this_month">This month</string>
|
||||
<string name="search_filters_this_week">This week</string>
|
||||
<string name="search_filters_this_year">This year</string>
|
||||
<string name="search_filters_yes">Yes</string>
|
||||
<string name="search_filters_youtube_music">YouTube Music</string>
|
||||
<!-- end - strings for search filter feature (NewPipeExtractor Services) -->
|
||||
|
||||
<!-- begin - strings for search filter UI -->
|
||||
<string name="filter">Filter</string>
|
||||
<string name="filter_search_sort_filters">Sort filters</string>
|
||||
<string name="filter_search_content_filters">Content filters</string>
|
||||
<string name="search_filter_ui">Select Search Filter UI</string>
|
||||
<string name="search_filter_ui_dialog">Simple Dialog (default)</string>
|
||||
<string name="search_filter_ui_style">Action Menu styled Dialog</string>
|
||||
<string name="search_filter_ui_legacy">Action Menu (legacy)</string>
|
||||
<string name="search_filter_ui_chip_dialog">Chip Dialog</string>
|
||||
<!-- end - strings for search filter UI -->
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -155,4 +155,12 @@
|
|||
|
||||
<style name="RouterActivityThemeDark" parent="Base.RouterActivityThemeDark" />
|
||||
|
||||
<!-- custom Chip style for SearchFilterDialogFragment -->
|
||||
<style name="ChipSearchFilter" parent="Widget.MaterialComponents.Chip.Filter">
|
||||
<item name="checkedIconEnabled">false</item>
|
||||
<item name="checkedIcon">@null</item>
|
||||
<item name="chipBackgroundColor">@color/mtrl_search_filter_chip_background_color</item>
|
||||
<item name="chipStrokeWidth">0.1dp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -66,6 +66,16 @@
|
|||
app:singleLineTitle="false"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="@string/search_filter_ui_dialog_key"
|
||||
android:entries="@array/search_filter_ui_description"
|
||||
android:entryValues="@array/search_filter_ui_values"
|
||||
android:key="@string/search_filter_ui_key"
|
||||
android:summary="%s"
|
||||
android:title="@string/search_filter_ui"
|
||||
app:singleLineTitle="false"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="@string/main_tabs_position_key"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,543 @@
|
|||
// Created by evermind-zz 2022, licensed GNU GPL version 3 or later
|
||||
|
||||
package org.schabi.newpipe.filter;
|
||||
|
||||
|
||||
import org.junit.Test;
|
||||
import org.schabi.newpipe.extractor.NewPipe;
|
||||
import org.schabi.newpipe.extractor.StreamingService;
|
||||
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||
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.extractor.services.peertube.search.filter.PeertubeFilters;
|
||||
import org.schabi.newpipe.extractor.services.youtube.search.filter.YoutubeFilters;
|
||||
import org.schabi.newpipe.fragments.list.search.filter.BaseSearchFilterUiGenerator;
|
||||
import org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.Callback;
|
||||
import static org.schabi.newpipe.fragments.list.search.filter.SearchFilterLogic.ICreateUiForFiltersWorker;
|
||||
|
||||
/**
|
||||
* Test the {@link SearchFilterLogic} and
|
||||
* {@link org.schabi.newpipe.extractor.search.filter.SearchFiltersBase}.
|
||||
*/
|
||||
public class SearchFilterLogicAndUiGeneratorTest {
|
||||
|
||||
private static final int PEERTUBE_SERVICE_ID = 3;
|
||||
private static final int YOUTUBE_SERVICE_ID = 0;
|
||||
private final Map<Integer, ElementsWrapper> universalWrapper = new HashMap<>();
|
||||
private BaseSearchFilterUiGenerator generator;
|
||||
private StreamingService service;
|
||||
private SearchFilterGeneratorWorkersClass.FilterWorker sortWorker;
|
||||
private List<FilterItem> fromCallbackContentFilterItems;
|
||||
private List<FilterItem> fromCallbackSortFilterItems;
|
||||
private SearchFilterLogic logic;
|
||||
|
||||
|
||||
private void setupEach(final boolean withUiWorker,
|
||||
final SearchFilterLogic.Callback callback)
|
||||
throws ExtractionException {
|
||||
setupEach(withUiWorker, PEERTUBE_SERVICE_ID, callback);
|
||||
}
|
||||
|
||||
private void setupEach(final boolean withUiWorker,
|
||||
final int serviceId,
|
||||
final SearchFilterLogic.Callback callback)
|
||||
throws ExtractionException {
|
||||
service = NewPipe.getService(serviceId);
|
||||
|
||||
logic = SearchFilterLogic.Factory.create(
|
||||
SearchFilterLogic.Factory.Variant.SEARCH_FILTER_LOGIC_DEFAULT,
|
||||
service.getSearchQHFactory(),
|
||||
callback);
|
||||
|
||||
if (withUiWorker) {
|
||||
generator = new SearchFilterGeneratorWorkersClass(service.getSearchQHFactory(),
|
||||
callback, logic);
|
||||
} else {
|
||||
generator = new SearchFilterGeneratorNoWorkersClass(service.getSearchQHFactory(),
|
||||
callback, logic);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resetAndRestoreTest() throws ExtractionException {
|
||||
setupEach(false, null);
|
||||
// 1. no data input (eg no previously selected filters set)
|
||||
final ArrayList<Integer> contentFilters = logic.getSelectedContentFilters();
|
||||
final ArrayList<Integer> sortFilters = logic.getSelectedSortFilters();
|
||||
logic.reset();
|
||||
final ArrayList<Integer> contentFilters2 = logic.getSelectedContentFilters();
|
||||
final ArrayList<Integer> sortFilters2 = logic.getSelectedSortFilters();
|
||||
assertTrue(!contentFilters2.isEmpty() && !contentFilters.isEmpty());
|
||||
assertTrue(!sortFilters2.isEmpty() && !sortFilters.isEmpty());
|
||||
|
||||
// 2. test if initially set some data that should be present in output
|
||||
final ArrayList<Integer> contentFiltersWithNoneDefaultId = new ArrayList<>();
|
||||
contentFiltersWithNoneDefaultId.add(PeertubeFilters.ID_CF_MAIN_VIDEOS);
|
||||
final ArrayList<Integer> sortFiltersWithNoneDefaultId = new ArrayList<>();
|
||||
sortFiltersWithNoneDefaultId.add(PeertubeFilters.ID_SF_SORT_BY_CREATION_DATE);
|
||||
|
||||
logic.restorePreviouslySelectedFilters(contentFiltersWithNoneDefaultId,
|
||||
sortFiltersWithNoneDefaultId);
|
||||
|
||||
ArrayList<Integer> contentFilterResetResult = logic.getSelectedContentFilters();
|
||||
ArrayList<Integer> sortFilterResetResult = logic.getSelectedSortFilters();
|
||||
|
||||
assertTrue(contentFilterResetResult.contains(PeertubeFilters.ID_CF_MAIN_VIDEOS));
|
||||
assertTrue(sortFilterResetResult.contains(PeertubeFilters.ID_SF_SORT_BY_CREATION_DATE));
|
||||
assertFalse(contentFilterResetResult.contains(PeertubeFilters.ID_CF_MAIN_CHANNELS));
|
||||
assertFalse(sortFilterResetResult.contains(PeertubeFilters.ID_SF_SORT_BY_RELEVANCE));
|
||||
|
||||
logic.reset(); // now go back to default values
|
||||
|
||||
contentFilterResetResult = logic.getSelectedContentFilters();
|
||||
sortFilterResetResult = logic.getSelectedSortFilters();
|
||||
assertTrue(contentFilterResetResult.contains(PeertubeFilters.ID_CF_MAIN_VIDEOS));
|
||||
assertTrue(sortFilterResetResult.contains(PeertubeFilters.ID_SF_SORT_BY_RELEVANCE));
|
||||
|
||||
// 3. test if empty input data results in defaults
|
||||
setupEach(false, null);
|
||||
logic.restorePreviouslySelectedFilters(new ArrayList<>(),
|
||||
new ArrayList<>());
|
||||
final ArrayList<Integer> contentFilterResultNoInput =
|
||||
logic.getSelectedContentFilters();
|
||||
final ArrayList<Integer> sortFilterResultNoInput =
|
||||
logic.getSelectedSortFilters();
|
||||
assertTrue(contentFilterResultNoInput.contains(PeertubeFilters.ID_CF_MAIN_VIDEOS));
|
||||
assertTrue(sortFilterResultNoInput.contains(PeertubeFilters.ID_SF_SORT_BY_RELEVANCE));
|
||||
|
||||
// 4. compare 2 and 3 results
|
||||
assertArrayEquals(contentFilterResetResult.toArray(),
|
||||
contentFilterResultNoInput.toArray());
|
||||
assertArrayEquals(sortFilterResetResult.toArray(),
|
||||
sortFilterResultNoInput.toArray());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkIfInitResultsInDefaultSortAndContentFiltersTest() throws ExtractionException {
|
||||
setupEach(false, null);
|
||||
|
||||
// after setupEach() there should be default entries.
|
||||
final ArrayList<Integer> defaultContentFilters =
|
||||
logic.getSelectedContentFilters();
|
||||
final ArrayList<Integer> defaultSortFilters =
|
||||
logic.getSelectedSortFilters();
|
||||
|
||||
assertTrue(defaultContentFilters.contains(PeertubeFilters.ID_CF_MAIN_VIDEOS));
|
||||
assertTrue(defaultSortFilters.contains(PeertubeFilters.ID_SF_SORT_BY_RELEVANCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void contentFilterItemsIdsMatchIdsAndCallbackTest() throws ExtractionException {
|
||||
setupEach(false, new SearchFilterLogic.Callback() {
|
||||
@Override
|
||||
public void selectedFilters(@NonNull final List<FilterItem> userSelectedContentFilter,
|
||||
@NonNull final List<FilterItem> userSelectedSortFilter) {
|
||||
fromCallbackContentFilterItems = userSelectedContentFilter;
|
||||
fromCallbackSortFilterItems = userSelectedSortFilter;
|
||||
}
|
||||
});
|
||||
|
||||
// reset to null
|
||||
fromCallbackContentFilterItems = null;
|
||||
fromCallbackSortFilterItems = null;
|
||||
|
||||
// after setupEach() there should be default entries.
|
||||
ArrayList<Integer> defaultContentFiltersIds = logic.getSelectedContentFilters();
|
||||
ArrayList<Integer> defaultSortFiltersIds = logic.getSelectedSortFilters();
|
||||
List<FilterItem> defaultContentFilterItems = logic.getSelectedContentFilterItems();
|
||||
List<FilterItem> defaultSortFilterItems = logic.getSelectedSortFiltersItems();
|
||||
|
||||
assertNotEquals(defaultContentFiltersIds.size(), defaultContentFilterItems.size());
|
||||
assertNotEquals(defaultSortFiltersIds.size(), defaultSortFilterItems.size());
|
||||
|
||||
assertNull(fromCallbackContentFilterItems);
|
||||
assertNull(fromCallbackSortFilterItems);
|
||||
|
||||
logic.prepareForSearch(); // callback variables are now being initialized
|
||||
|
||||
assertNotNull(fromCallbackContentFilterItems);
|
||||
assertNotNull(fromCallbackSortFilterItems);
|
||||
|
||||
defaultContentFiltersIds = logic.getSelectedContentFilters();
|
||||
defaultSortFiltersIds = logic.getSelectedSortFilters();
|
||||
defaultContentFilterItems = logic.getSelectedContentFilterItems();
|
||||
defaultSortFilterItems = logic.getSelectedSortFiltersItems();
|
||||
|
||||
assertTrue(defaultContentFilterItems.size() > 0);
|
||||
assertTrue(defaultSortFilterItems.size() > 0);
|
||||
|
||||
assertEquals(defaultContentFiltersIds.size(), defaultContentFilterItems.size());
|
||||
assertEquals(defaultSortFiltersIds.size(), defaultSortFilterItems.size());
|
||||
|
||||
compareFilterIdsWithFilterItems(defaultContentFiltersIds, defaultContentFilterItems);
|
||||
compareFilterIdsWithFilterItems(defaultSortFiltersIds, defaultSortFilterItems);
|
||||
|
||||
compareFilterIdsWithFilterItems(defaultContentFiltersIds, fromCallbackContentFilterItems);
|
||||
compareFilterIdsWithFilterItems(defaultSortFiltersIds, fromCallbackSortFilterItems);
|
||||
}
|
||||
|
||||
private void compareFilterIdsWithFilterItems(final ArrayList<Integer> filterIds,
|
||||
final List<FilterItem> filterItems) {
|
||||
int idx = 0;
|
||||
for (final FilterItem item : filterItems) {
|
||||
final int filterItemId = item.getIdentifier();
|
||||
final int filterItemId2 = filterIds.get(idx++);
|
||||
assertEquals(filterItemId, filterItemId2);
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void checkIllegalContentFilterIdsTest() throws ExtractionException {
|
||||
setupEach(false, null);
|
||||
final ArrayList<Integer> contentFiltersWithIllegalIds = new ArrayList<>();
|
||||
contentFiltersWithIllegalIds.add(10000);
|
||||
final ArrayList<Integer> sortFiltersEmpty = new ArrayList<>();
|
||||
|
||||
logic.restorePreviouslySelectedFilters(contentFiltersWithIllegalIds,
|
||||
sortFiltersEmpty);
|
||||
}
|
||||
|
||||
@Test(expected = RuntimeException.class)
|
||||
public void checkIllegalSortFilterIdsTest() throws ExtractionException {
|
||||
setupEach(false, null);
|
||||
// content filter can not be empty
|
||||
final ArrayList<Integer> contentFiltersWithValidId = new ArrayList<>();
|
||||
contentFiltersWithValidId.add(PeertubeFilters.ID_CF_MAIN_VIDEOS);
|
||||
final ArrayList<Integer> sortFiltersWithIllegalIds = new ArrayList<>();
|
||||
sortFiltersWithIllegalIds.add(20000);
|
||||
|
||||
logic.restorePreviouslySelectedFilters(contentFiltersWithValidId,
|
||||
sortFiltersWithIllegalIds);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectOneContenFilterKeepDefaultSortFilterTest() throws ExtractionException {
|
||||
setupEach(false, null);
|
||||
|
||||
// set only one content filter, keep default sort filters
|
||||
logic.selectContentFilter(PeertubeFilters.ID_CF_MAIN_PLAYLISTS);
|
||||
ArrayList<Integer> contentFilterResetResult = logic.getSelectedContentFilters();
|
||||
ArrayList<Integer> sortFilterResetResult = logic.getSelectedSortFilters();
|
||||
assertTrue(contentFilterResetResult.contains(PeertubeFilters.ID_CF_MAIN_PLAYLISTS));
|
||||
assertTrue(sortFilterResetResult.contains(PeertubeFilters.ID_SF_SORT_BY_RELEVANCE));
|
||||
|
||||
logic.selectContentFilter(PeertubeFilters.ID_CF_MAIN_CHANNELS);
|
||||
contentFilterResetResult = logic.getSelectedContentFilters();
|
||||
sortFilterResetResult = logic.getSelectedSortFilters();
|
||||
assertFalse(contentFilterResetResult.contains(PeertubeFilters.ID_CF_MAIN_PLAYLISTS));
|
||||
assertTrue(contentFilterResetResult.contains(PeertubeFilters.ID_CF_MAIN_CHANNELS));
|
||||
assertTrue(sortFilterResetResult.contains(PeertubeFilters.ID_SF_SORT_BY_RELEVANCE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectOneContentFilterAndOneSortFilterTest() throws ExtractionException {
|
||||
setupEach(false, null);
|
||||
|
||||
logic.selectContentFilter(PeertubeFilters.ID_CF_MAIN_PLAYLISTS);
|
||||
logic.selectSortFilter(PeertubeFilters.ID_SF_SORT_BY_CREATION_DATE);
|
||||
ArrayList<Integer> contentFilterResetResult = logic.getSelectedContentFilters();
|
||||
ArrayList<Integer> sortFilterResetResult = logic.getSelectedSortFilters();
|
||||
assertTrue(contentFilterResetResult.contains(PeertubeFilters.ID_CF_MAIN_PLAYLISTS));
|
||||
assertFalse(sortFilterResetResult.contains(PeertubeFilters.ID_SF_SORT_BY_RELEVANCE));
|
||||
assertTrue(sortFilterResetResult.contains(PeertubeFilters.ID_SF_SORT_BY_CREATION_DATE));
|
||||
|
||||
logic.selectContentFilter(PeertubeFilters.ID_CF_MAIN_CHANNELS);
|
||||
logic.selectSortFilter(PeertubeFilters.ID_SF_SORT_BY_DURATION);
|
||||
contentFilterResetResult = logic.getSelectedContentFilters();
|
||||
sortFilterResetResult = logic.getSelectedSortFilters();
|
||||
assertFalse(contentFilterResetResult.contains(PeertubeFilters.ID_CF_MAIN_PLAYLISTS));
|
||||
assertTrue(contentFilterResetResult.contains(PeertubeFilters.ID_CF_MAIN_CHANNELS));
|
||||
assertTrue(sortFilterResetResult.contains(PeertubeFilters.ID_SF_SORT_BY_DURATION));
|
||||
assertFalse(sortFilterResetResult.contains(PeertubeFilters.ID_SF_SORT_BY_CREATION_DATE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectTwoContentFiltersTest() throws ExtractionException {
|
||||
setupEach(false, null);
|
||||
|
||||
logic.selectContentFilter(PeertubeFilters.ID_CF_MAIN_PLAYLISTS);
|
||||
ArrayList<Integer> contentFilterResetResult = logic.getSelectedContentFilters();
|
||||
assertTrue(contentFilterResetResult.contains(PeertubeFilters.ID_CF_MAIN_PLAYLISTS));
|
||||
assertFalse(contentFilterResetResult.contains(PeertubeFilters.ID_CF_SEPIA_SEPIASEARCH));
|
||||
|
||||
// 2nd content filters added from another group of course as PeertubeFilter.ID_CF_MAIN_GRP
|
||||
// is exclusive group -> only one item per group allowed
|
||||
logic.selectContentFilter(PeertubeFilters.ID_CF_SEPIA_SEPIASEARCH);
|
||||
contentFilterResetResult = logic.getSelectedContentFilters();
|
||||
assertTrue(contentFilterResetResult.contains(PeertubeFilters.ID_CF_MAIN_PLAYLISTS));
|
||||
assertTrue(contentFilterResetResult.contains(PeertubeFilters.ID_CF_SEPIA_SEPIASEARCH));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectMultipleSortFilterInNonExclusiveGroupTest() throws ExtractionException {
|
||||
selectMultipleSortFilterInNonExclusiveGroupHelper(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectMultipleSortFilterInNonExclusiveGroupWithUiTest() throws ExtractionException {
|
||||
selectMultipleSortFilterInNonExclusiveGroupHelper(true);
|
||||
}
|
||||
|
||||
private void selectMultipleSortFilterInNonExclusiveGroupHelper(final boolean withUiWorker)
|
||||
throws ExtractionException {
|
||||
setupEach(withUiWorker, YOUTUBE_SERVICE_ID, null);
|
||||
|
||||
if (withUiWorker) {
|
||||
universalWrapper.clear();
|
||||
generator.createSearchUI();
|
||||
simulateUiClicking(YoutubeFilters.ID_CF_MAIN_VIDEOS);
|
||||
}
|
||||
logic.selectContentFilter(YoutubeFilters.ID_CF_MAIN_VIDEOS);
|
||||
final ArrayList<Integer> contentFilterResetResult = logic.getSelectedContentFilters();
|
||||
assertTrue(contentFilterResetResult.contains(YoutubeFilters.ID_CF_MAIN_VIDEOS));
|
||||
|
||||
// select 1st element from a non-exclusive group
|
||||
if (withUiWorker) {
|
||||
simulateUiClicking(YoutubeFilters.ID_SF_FEATURES_3D);
|
||||
}
|
||||
logic.selectSortFilter(YoutubeFilters.ID_SF_FEATURES_3D);
|
||||
ArrayList<Integer> sortFilterResetResult = logic.getSelectedSortFilters();
|
||||
assertTrue(sortFilterResetResult.contains(YoutubeFilters.ID_SF_FEATURES_3D));
|
||||
assertFalse(sortFilterResetResult.contains(YoutubeFilters.ID_SF_FEATURES_4K));
|
||||
|
||||
// select 2nd element from a non-exclusive group
|
||||
if (withUiWorker) {
|
||||
simulateUiClicking(YoutubeFilters.ID_SF_FEATURES_4K);
|
||||
}
|
||||
logic.selectSortFilter(YoutubeFilters.ID_SF_FEATURES_4K);
|
||||
sortFilterResetResult = logic.getSelectedSortFilters();
|
||||
assertTrue(sortFilterResetResult.contains(YoutubeFilters.ID_SF_FEATURES_3D));
|
||||
assertTrue(sortFilterResetResult.contains(YoutubeFilters.ID_SF_FEATURES_4K));
|
||||
|
||||
// deselect previous selected element
|
||||
if (withUiWorker) {
|
||||
simulateUiClicking(YoutubeFilters.ID_SF_FEATURES_4K);
|
||||
}
|
||||
logic.selectSortFilter(YoutubeFilters.ID_SF_FEATURES_4K);
|
||||
sortFilterResetResult = logic.getSelectedSortFilters();
|
||||
assertTrue(sortFilterResetResult.contains(YoutubeFilters.ID_SF_FEATURES_3D));
|
||||
assertFalse(sortFilterResetResult.contains(YoutubeFilters.ID_SF_FEATURES_4K));
|
||||
}
|
||||
|
||||
private void simulateUiClicking(final int id) {
|
||||
final boolean isSelected = universalWrapper.get(id).isChecked();
|
||||
universalWrapper.get(id).setChecked(!isSelected);
|
||||
}
|
||||
|
||||
private void expectSortFiltersToBeVisible(final int id) {
|
||||
final FilterContainer sortFilterVariant = service.getSearchQHFactory()
|
||||
.getContentFilterSortFilterVariant(id);
|
||||
assertTrue(!sortFilterVariant.getFilterGroups().isEmpty());
|
||||
for (final FilterGroup group : sortFilterVariant.getFilterGroups()) {
|
||||
for (final FilterItem item : group.getFilterItems()) {
|
||||
final int itemId = item.getIdentifier();
|
||||
assertTrue(universalWrapper.containsKey(itemId));
|
||||
assertNotNull(universalWrapper.get(itemId));
|
||||
assertTrue(universalWrapper.get(itemId).visible);
|
||||
}
|
||||
}
|
||||
assertNotNull(sortWorker.areAnySortFiltersVisible);
|
||||
assertTrue(sortWorker.areAnySortFiltersVisible.isPresent());
|
||||
assertTrue(sortWorker.areAnySortFiltersVisible.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkIfCorrespondingSortFiltersAreDisplayedTest()
|
||||
throws ExtractionException {
|
||||
setupEach(true, PEERTUBE_SERVICE_ID, null);
|
||||
|
||||
universalWrapper.clear();
|
||||
generator.createSearchUI();
|
||||
|
||||
// 1st test:
|
||||
// default content filter is PeertubeFilters.ID_CF_MAIN_ALL so we expect all sort filters
|
||||
// visible. Get the filters from service and compare with universalWrapper map
|
||||
expectSortFiltersToBeVisible(PeertubeFilters.ID_CF_MAIN_VIDEOS);
|
||||
|
||||
// 2nd test:
|
||||
// content filter with no sort filters aka Ui element should be not visible.
|
||||
// get all sort filters from and compare with universalWrapper map
|
||||
// set content filter with no sort filters available
|
||||
final int contentFilterWithNoSortFilters = PeertubeFilters.ID_CF_MAIN_PLAYLISTS;
|
||||
logic.selectContentFilter(contentFilterWithNoSortFilters);
|
||||
final FilterContainer noSortFiltersAkaNull = service.getSearchQHFactory()
|
||||
.getContentFilterSortFilterVariant(contentFilterWithNoSortFilters);
|
||||
assertNull(noSortFiltersAkaNull);
|
||||
|
||||
// get content filter with all sort filters visible in two ways
|
||||
// first way
|
||||
final FilterContainer allSortFilters = service.getSearchQHFactory()
|
||||
.getContentFilterSortFilterVariant(PeertubeFilters.ID_CF_MAIN_VIDEOS);
|
||||
// second way
|
||||
final Optional<FilterGroup> allSortFilters2 = service.getSearchQHFactory()
|
||||
.getAvailableContentFilter()
|
||||
.getFilterGroups().stream()
|
||||
.filter(filterGroup
|
||||
-> (filterGroup.getIdentifier() == PeertubeFilters.ID_CF_MAIN_GRP))
|
||||
.findFirst();
|
||||
|
||||
assertNotNull(allSortFilters);
|
||||
assertTrue(allSortFilters2.isPresent());
|
||||
assertEquals(allSortFilters, allSortFilters2.get().getAllSortFilters());
|
||||
assertTrue(!allSortFilters.getFilterGroups().isEmpty());
|
||||
assertNotNull(sortWorker.areAnySortFiltersVisible);
|
||||
assertTrue(sortWorker.areAnySortFiltersVisible.isPresent());
|
||||
assertFalse(sortWorker.areAnySortFiltersVisible.get());
|
||||
|
||||
// expect all sort filters not visible
|
||||
for (final FilterGroup group : allSortFilters.getFilterGroups()) {
|
||||
for (final FilterItem item : group.getFilterItems()) {
|
||||
final int id = item.getIdentifier();
|
||||
assertTrue(universalWrapper.containsKey(id));
|
||||
assertNotNull(universalWrapper.get(id));
|
||||
assertFalse(universalWrapper.get(id).visible);
|
||||
}
|
||||
}
|
||||
|
||||
// 3rd test:
|
||||
// select content filter that should have all sort filters visible again
|
||||
final int contentFilterWithAllSortFiltersVisible = PeertubeFilters.ID_CF_MAIN_VIDEOS;
|
||||
logic.selectContentFilter(contentFilterWithAllSortFiltersVisible);
|
||||
expectSortFiltersToBeVisible(contentFilterWithAllSortFiltersVisible);
|
||||
}
|
||||
|
||||
// helpers
|
||||
private static class SearchFilterGeneratorNoWorkersClass extends BaseSearchFilterUiGenerator {
|
||||
|
||||
SearchFilterGeneratorNoWorkersClass(final SearchQueryHandlerFactory linkHandlerFactory,
|
||||
final Callback callback,
|
||||
final SearchFilterLogic logic) {
|
||||
super(logic, null); // context is null as this is no androidTest
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ICreateUiForFiltersWorker createSortFilterWorker() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ICreateUiForFiltersWorker createContentFilterWorker() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static class ElementsWrapper implements SearchFilterLogic.IUiItemWrapper {
|
||||
public final FilterItem item;
|
||||
public final int groupId;
|
||||
public boolean isSelected;
|
||||
public boolean visible;
|
||||
|
||||
ElementsWrapper(final FilterItem item,
|
||||
final int groupId) {
|
||||
this.item = item;
|
||||
this.groupId = groupId;
|
||||
this.visible = false;
|
||||
this.isSelected = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(final boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemId() {
|
||||
return item.getIdentifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChecked() {
|
||||
return isSelected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setChecked(final boolean checked) {
|
||||
this.isSelected = checked;
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchFilterGeneratorWorkersClass extends SearchFilterGeneratorNoWorkersClass {
|
||||
|
||||
SearchFilterGeneratorWorkersClass(final SearchQueryHandlerFactory linkHandlerFactory,
|
||||
final Callback callback,
|
||||
final SearchFilterLogic logic) {
|
||||
super(linkHandlerFactory, callback, logic);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ICreateUiForFiltersWorker createSortFilterWorker() {
|
||||
sortWorker = new FilterWorker(true);
|
||||
return sortWorker;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ICreateUiForFiltersWorker createContentFilterWorker() {
|
||||
return new FilterWorker(false);
|
||||
}
|
||||
|
||||
class FilterWorker implements ICreateUiForFiltersWorker {
|
||||
|
||||
private final boolean isSortWorker;
|
||||
public Optional<Boolean> areAnySortFiltersVisible = null;
|
||||
|
||||
FilterWorker(final boolean isSortWorker) {
|
||||
this.isSortWorker = isSortWorker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterGroupBeforeItems(@NonNull final FilterGroup filterGroup) {
|
||||
for (final FilterItem item : filterGroup.getFilterItems()) {
|
||||
final ElementsWrapper element =
|
||||
new ElementsWrapper(item, filterGroup.getIdentifier());
|
||||
universalWrapper.put(item.getIdentifier(), element);
|
||||
if (isSortWorker) {
|
||||
logic.addSortFilterUiWrapperToItemMap(item.getIdentifier(), element);
|
||||
} else {
|
||||
logic.addContentFilterUiWrapperToItemMap(item.getIdentifier(), element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterItem(@NonNull final FilterItem filterItem,
|
||||
@NonNull final FilterGroup filterGroup) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createFilterGroupAfterItems(@NonNull final FilterGroup filterGroup) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void filtersVisible(final boolean areFiltersVisible) {
|
||||
areAnySortFiltersVisible = Optional.of(areFiltersVisible);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue