diff --git a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java index 741bda246..77b1413aa 100644 --- a/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java +++ b/app/src/main/java/org/schabi/newpipe/download/DownloadDialog.java @@ -299,7 +299,7 @@ public class DownloadDialog extends DialogFragment } dialogBinding.fileName.setText(FilenameUtils.createFilename(getContext(), - currentInfo.getName())); + getFileName())); selectedAudioIndex = ListHelper.getDefaultAudioFormat(getContext(), getWrappedAudioStreams().getStreamsList()); @@ -612,7 +612,7 @@ public class DownloadDialog extends DialogFragment } private void onItemSelectedSetFileName() { - final String fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName()); + final String fileName = FilenameUtils.createFilename(getContext(), getFileName()); final String prevFileName = Optional.ofNullable(dialogBinding.fileName.getText()) .map(Object::toString) .orElse(""); @@ -743,7 +743,24 @@ public class DownloadDialog extends DialogFragment final String str = Objects.requireNonNull(dialogBinding.fileName.getText()).toString() .trim(); - return FilenameUtils.createFilename(context, str.isEmpty() ? currentInfo.getName() : str); + return FilenameUtils.createFilename(context, str.isEmpty() ? getFileName() : str); + } + + private String getFileName() { + final SharedPreferences sharedPreferences = PreferenceManager + .getDefaultSharedPreferences(context); + + final boolean includeUploader = sharedPreferences.getBoolean( + context.getString(R.string.settings_file_name_include_uploader_key), false); + + final String name = currentInfo.getName(); + if (includeUploader) { + final String uploader = currentInfo.getUploaderName(); + if (uploader != null && !uploader.isEmpty()) { + return name + " - " + uploader; + } + } + return name; } private void showFailedDialog(@StringRes final int msg) { diff --git a/app/src/main/java/us/shandian/giga/service/DownloadManager.java b/app/src/main/java/us/shandian/giga/service/DownloadManager.java index 7a2055aaa..788a95f24 100644 --- a/app/src/main/java/us/shandian/giga/service/DownloadManager.java +++ b/app/src/main/java/us/shandian/giga/service/DownloadManager.java @@ -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 getSpecialItems() { synchronized (DownloadManager.this) { ArrayList 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); } diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java index 54ae2cfa4..792d30bbe 100644 --- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java +++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java @@ -117,6 +117,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb private final View mView; private final ArrayList mHidden; private Snackbar mSnackbar; + private boolean showButtons = true; private final CompositeDisposable compositeDisposable = new CompositeDisposable(); @@ -186,7 +187,7 @@ public class MissionAdapter extends Adapter 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); @@ -731,13 +732,25 @@ public class MissionAdapter extends Adapter 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() { @@ -757,7 +770,7 @@ public class MissionAdapter extends Adapter implements Handler.Callb public void setClearButton(MenuItem clearButton) { if (mClear == null) - clearButton.setVisible(mIterator.hasFinishedMissions()); + clearButton.setVisible(showButtons && mIterator.hasFinishedMissions()); mClear = clearButton; } @@ -771,6 +784,18 @@ public class MissionAdapter extends Adapter 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); @@ -779,12 +804,12 @@ public class MissionAdapter extends Adapter 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); } diff --git a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java index 690ed4a97..9e218827c 100644 --- a/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java +++ b/app/src/main/java/us/shandian/giga/ui/fragment/MissionsFragment.java @@ -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 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); + } + } diff --git a/app/src/main/res/menu/download_menu.xml b/app/src/main/res/menu/download_menu.xml index c12a7e768..faa3354dd 100644 --- a/app/src/main/res/menu/download_menu.xml +++ b/app/src/main/res/menu/download_menu.xml @@ -3,6 +3,12 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> + + Letzte Größe und Position des Pop-ups merken Suchvorschläge Vorschläge auswählen, die bei der Suche angezeigt werden sollen - löschen + Löschen Beste Auflösung Über NewPipe Lizenzen @@ -118,6 +118,8 @@ Erlaubte Zeichen im Dateinamen Ungültige Zeichen werden durch dieses Zeichen ersetzt Ersetzungszeichen + Name des Uploaders an den Dateinamen anhängen + Uploader im Dateinamen Buchstaben und Zahlen Abonnieren Abonniert diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml index ab6e9e345..cc72a398b 100644 --- a/app/src/main/res/values/settings_keys.xml +++ b/app/src/main/res/values/settings_keys.xml @@ -432,6 +432,7 @@ file_rename_charset file_replacement_character _ + file_include_uploader_name CHARSET_LETTERS_AND_DIGITS diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a2a5b6f1..3499c9a91 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -365,6 +365,8 @@ Allowed characters in filenames Invalid characters are replaced with this value Replacement character + Append uploader name to filename + Uploader in filename Letters and digits Most special characters diff --git a/app/src/main/res/xml/download_settings.xml b/app/src/main/res/xml/download_settings.xml index 27513d259..75278b4b2 100644 --- a/app/src/main/res/xml/download_settings.xml +++ b/app/src/main/res/xml/download_settings.xml @@ -53,6 +53,14 @@ app:singleLineTitle="false" app:iconSpaceReserved="false" /> + +