Ability to search within downloaded files.

This commit is contained in:
malania02 2025-11-11 00:24:04 +01:00
parent 0cddd0e859
commit 1a73f0ada9
5 changed files with 251 additions and 9 deletions

View File

@ -14,6 +14,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import us.shandian.giga.get.DownloadMission;
import us.shandian.giga.get.FinishedMission;
@ -600,12 +601,25 @@ public class DownloadManager {
boolean hasFinished = false;
private boolean filteringEnabled = false;
private String currentFilter = "";
private MissionIterator() {
hidden = new ArrayList<>(2);
current = null;
snapshot = getSpecialItems();
}
public void filter(String query) {
currentFilter = query.trim().toLowerCase(Locale.getDefault());
filteringEnabled = !currentFilter.isEmpty();
}
public void clearFilter() {
currentFilter = "";
filteringEnabled = false;
}
private ArrayList<Object> getSpecialItems() {
synchronized (DownloadManager.this) {
ArrayList<Mission> pending = new ArrayList<>(mMissionsPending);
@ -620,6 +634,11 @@ public class DownloadManager {
return pending.remove(mission) || finished.remove(mission);
});
if (filteringEnabled && currentFilter != null && !currentFilter.isEmpty()) {
pending.removeIf(m -> !matchesFilter(m, currentFilter));
finished.removeIf(m -> !matchesFilter(m, currentFilter));
}
int fakeTotal = pending.size();
if (fakeTotal > 0) fakeTotal++;
@ -642,6 +661,11 @@ public class DownloadManager {
}
}
private boolean matchesFilter(Mission mission, String query) {
String name = mission.storage.getName().toLowerCase(Locale.getDefault());
return name.contains(query);
}
public MissionItem getItem(int position) {
Object object = snapshot.get(position);
@ -729,6 +753,10 @@ public class DownloadManager {
Object x = snapshot.get(oldItemPosition);
Object y = current.get(newItemPosition);
// Necessary to avoid flickering of headers when filtering
if (x == PENDING && y == PENDING) return true;
if (x == FINISHED && y == FINISHED) return true;
if (x instanceof Mission && y instanceof Mission) {
return ((Mission) x).storage.equals(((Mission) y).storage);
}

View File

@ -116,6 +116,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
private final View mView;
private final ArrayList<Mission> mHidden;
private Snackbar mSnackbar;
private boolean showButtons = true;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
@ -185,7 +186,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
str = R.string.missions_header_pending;
} else {
str = R.string.missions_header_finished;
if (mClear != null) mClear.setVisible(true);
if (mClear != null) mClear.setVisible(showButtons);
}
((ViewHolderHeader) view).header.setText(str);
@ -727,13 +728,25 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
}
}
public void filter(String query) {
if (query == null) return;
String currentFilter = query.trim();
if (currentFilter.isEmpty()) {
mIterator.clearFilter();
} else {
mIterator.filter(currentFilter);
}
applyChanges();
}
public void applyChanges() {
mIterator.start();
DiffUtil.calculateDiff(mIterator, true).dispatchUpdatesTo(this);
mIterator.end();
checkEmptyMessageVisibility();
if (mClear != null) mClear.setVisible(mIterator.hasFinishedMissions());
if (mClear != null) mClear.setVisible(showButtons && mIterator.hasFinishedMissions());
}
public void forceUpdate() {
@ -753,7 +766,7 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
public void setClearButton(MenuItem clearButton) {
if (mClear == null)
clearButton.setVisible(mIterator.hasFinishedMissions());
clearButton.setVisible(showButtons && mIterator.hasFinishedMissions());
mClear = clearButton;
}
@ -767,6 +780,18 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
if (init) checkMasterButtonsVisibility();
}
public void showMenuButtons() {
showButtons = true;
if (mClear != null) mClear.setVisible(mIterator.hasFinishedMissions());
checkMasterButtonsVisibility();
}
public void hideMenuButtons() {
showButtons = false;
if (mClear != null) mClear.setVisible(false);
checkMasterButtonsVisibility();
}
private void checkEmptyMessageVisibility() {
int flag = mIterator.getOldListSize() > 0 ? View.GONE : View.VISIBLE;
if (mEmptyMessage.getVisibility() != flag) mEmptyMessage.setVisibility(flag);
@ -775,12 +800,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
public void checkMasterButtonsVisibility() {
boolean[] state = mIterator.hasValidPendingMissions();
Log.d(TAG, "checkMasterButtonsVisibility() running=" + state[0] + " paused=" + state[1]);
setButtonVisible(mPauseButton, state[0]);
setButtonVisible(mStartButton, state[1]);
setButtonVisible(mPauseButton, showButtons && state[0]);
setButtonVisible(mStartButton, showButtons && state[1]);
}
private static void setButtonVisible(MenuItem button, boolean visible) {
if (button.isVisible() != visible)
if (button != null && button.isVisible() != visible)
button.setVisible(visible);
}

View File

@ -10,24 +10,36 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.style.CharacterStyle;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.DecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.Toast;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.TooltipCompat;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.evernote.android.state.State;
import com.livefront.bridge.Bridge;
import com.nononsenseapps.filepicker.Utils;
import org.schabi.newpipe.R;
@ -35,6 +47,7 @@ import org.schabi.newpipe.settings.NewPipeSettings;
import org.schabi.newpipe.streams.io.NoFileManagerSafeGuard;
import org.schabi.newpipe.streams.io.StoredFileHelper;
import org.schabi.newpipe.util.FilePickerActivityHelper;
import org.schabi.newpipe.util.KeyboardUtil;
import java.io.File;
import java.io.IOException;
@ -52,6 +65,7 @@ public class MissionsFragment extends Fragment {
private SharedPreferences mPrefs;
private boolean mLinear;
private MenuItem mSearch;
private MenuItem mSwitch;
private MenuItem mClear = null;
private MenuItem mStart = null;
@ -64,9 +78,19 @@ public class MissionsFragment extends Fragment {
private LinearLayoutManager mLinearManager;
private Context mContext;
private View searchToolbarContainer;
private EditText searchEditText;
private View searchClear;
private TextWatcher textWatcher;
private DownloadManagerBinder mBinder;
private boolean mForceUpdate;
@State
String searchString;
@State
boolean wasSearchActive;
private DownloadMission unsafeMissionTarget = null;
private final ActivityResultLauncher<Intent> requestDownloadSaveAsLauncher =
registerForActivityResult(new StartActivityForResult(), this::requestDownloadSaveAsResult);
@ -87,6 +111,11 @@ public class MissionsFragment extends Fragment {
mBinder.enableNotifications(false);
updateList();
if (isSearchActive()) {
mAdapter.hideMenuButtons();
mAdapter.filter(getSearchEditString());
}
}
@Override
@ -132,6 +161,48 @@ public class MissionsFragment extends Fragment {
return v;
}
@Override
public void onViewCreated(@NonNull final View rootView, final Bundle savedInstanceState) {
super.onViewCreated(rootView, savedInstanceState);
initSearchViews();
initSearchListeners();
Bridge.restoreInstanceState(this, savedInstanceState);
if (savedInstanceState != null) {
if (wasSearchActive) {
searchEditText.setText(searchString);
showSearch();
}
}
requireActivity().getOnBackPressedDispatcher().addCallback(
getViewLifecycleOwner(),
new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
if (isSearchActive() && !TextUtils.isEmpty(getSearchEditString())) {
hideSearch();
} else {
setEnabled(false);
hideKeyboardSearch();
requireActivity().onBackPressed();
}
}
}
);
}
@Override
public void onSaveInstanceState(@NonNull final Bundle bundle) {
if (searchEditText != null) {
searchString = getSearchEditString();
wasSearchActive = isSearchActive();
}
super.onSaveInstanceState(bundle);
Bridge.saveInstanceState(this, bundle);
}
/**
* Added in API level 23.
*/
@ -174,12 +245,14 @@ public class MissionsFragment extends Fragment {
@Override
public void onPrepareOptionsMenu(Menu menu) {
mSearch = menu.findItem(R.id.action_search);
mSwitch = menu.findItem(R.id.switch_mode);
mClear = menu.findItem(R.id.clear_list);
mStart = menu.findItem(R.id.start_downloads);
mPause = menu.findItem(R.id.pause_downloads);
if (mAdapter != null) setAdapterButtons();
if (mSearch != null) mSearch.setVisible(!isSearchActive());
super.onPrepareOptionsMenu(menu);
}
@ -200,6 +273,10 @@ public class MissionsFragment extends Fragment {
case R.id.pause_downloads:
mBinder.getDownloadManager().pauseAllMissions(false);
mAdapter.refreshMissionItems();// update items view
return true;
case R.id.action_search:
showSearch();
return true;
default:
return super.onOptionsItemSelected(item);
}
@ -246,8 +323,8 @@ public class MissionsFragment extends Fragment {
if (mSwitch != null) {
mSwitch.setIcon(mLinear
? R.drawable.ic_apps
: R.drawable.ic_list);
? R.drawable.ic_apps
: R.drawable.ic_list);
mSwitch.setTitle(mLinear ? R.string.grid : R.string.list);
mPrefs.edit().putBoolean("linear", mLinear).apply();
}
@ -338,4 +415,110 @@ public class MissionsFragment extends Fragment {
Toast.makeText(mContext, R.string.general_error, Toast.LENGTH_LONG).show();
}
}
private void initSearchViews() {
searchToolbarContainer = requireActivity().findViewById(R.id.toolbar_search_container);
searchEditText = searchToolbarContainer.findViewById(R.id.toolbar_search_edit_text);
searchClear = searchToolbarContainer.findViewById(R.id.toolbar_search_clear);
}
private void initSearchListeners() {
searchClear.setOnClickListener(v -> {
if (TextUtils.isEmpty(getSearchEditString())) {
hideSearch();
return;
}
searchEditText.setText("");
showKeyboardSearch();
});
TooltipCompat.setTooltipText(searchClear, getString(R.string.clear));
if (textWatcher != null) {
searchEditText.removeTextChangedListener(textWatcher);
}
textWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(final CharSequence s, final int start,
final int count, final int after) {
// Do nothing, old text is already clean
}
@Override
public void onTextChanged(final CharSequence s, final int start,
final int before, final int count) {
// Changes are handled in afterTextChanged; CharSequence cannot be changed here.
}
@Override
public void afterTextChanged(final Editable s) {
// Remove rich text formatting
for (final CharacterStyle span : s.getSpans(0, s.length(), CharacterStyle.class)) {
s.removeSpan(span);
}
if (mAdapter != null) mAdapter.filter(s.toString());
}
};
searchEditText.addTextChangedListener(textWatcher);
searchEditText.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == EditorInfo.IME_ACTION_SEARCH ||
(event != null
&& event.getKeyCode() == KeyEvent.KEYCODE_ENTER
&& event.getAction() == KeyEvent.ACTION_DOWN)) {
hideKeyboardSearch();
return true;
}
return false;
});
}
private void showSearch() {
if (mSearch != null) mSearch.setVisible(false);
if (mAdapter != null) mAdapter.hideMenuButtons();
showKeyboardSearch();
if (TextUtils.isEmpty(getSearchEditString())) {
searchToolbarContainer.setTranslationX(100);
searchToolbarContainer.setAlpha(0.0f);
searchToolbarContainer.setVisibility(View.VISIBLE);
searchToolbarContainer.animate()
.translationX(0)
.alpha(1.0f)
.setDuration(200)
.setInterpolator(new DecelerateInterpolator()).start();
} else {
searchToolbarContainer.setTranslationX(0);
searchToolbarContainer.setAlpha(1.0f);
searchToolbarContainer.setVisibility(View.VISIBLE);
}
}
private void hideSearch() {
hideKeyboardSearch();
searchToolbarContainer.setVisibility(View.GONE);
if (!TextUtils.isEmpty(getSearchEditString())) searchEditText.setText("");
if (mSearch != null) mSearch.setVisible(true);
if (mAdapter != null) mAdapter.showMenuButtons();
}
private boolean isSearchActive() {
return searchToolbarContainer.getVisibility() == View.VISIBLE;
}
private String getSearchEditString() {
return searchEditText.getText().toString();
}
private void showKeyboardSearch() {
KeyboardUtil.showKeyboard(requireActivity(), searchEditText);
}
private void hideKeyboardSearch() {
KeyboardUtil.hideKeyboard(requireActivity(), searchEditText);
}
}

View File

@ -3,6 +3,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search"
android:title="@string/search"
app:showAsAction="always" />
<item
android:id="@+id/switch_mode"
android:icon="@drawable/ic_apps"

View File

@ -101,7 +101,7 @@
<string name="popup_remember_size_pos_summary">Letzte Größe und Position des Pop-ups merken</string>
<string name="show_search_suggestions_title">Suchvorschläge</string>
<string name="show_search_suggestions_summary">Vorschläge auswählen, die bei der Suche angezeigt werden sollen</string>
<string name="clear">löschen</string>
<string name="clear">Löschen</string>
<string name="best_resolution">Beste Auflösung</string>
<string name="title_activity_about">Über NewPipe</string>
<string name="tab_licenses">Lizenzen</string>