mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-07 01:51:27 +01:00
fix(YouTube - Settings): Use an overlay to show search results (#5806)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
@@ -1,87 +0,0 @@
|
|||||||
package app.revanced.extension.music.settings;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.preference.PreferenceFragment;
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.preference.ReVancedPreferenceFragment;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.BaseActivityHook;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hooks GoogleApiActivity to inject a custom ReVancedPreferenceFragment with a toolbar.
|
|
||||||
*/
|
|
||||||
public class GoogleApiActivityHook extends BaseActivityHook {
|
|
||||||
/**
|
|
||||||
* Injection point
|
|
||||||
* <p>
|
|
||||||
* Creates an instance of GoogleApiActivityHook for use in static initialization.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static GoogleApiActivityHook createInstance() {
|
|
||||||
// Must touch the Music settings to ensure the class is loaded and
|
|
||||||
// the values can be found when setting the UI preferences.
|
|
||||||
// Logging anything under non debug ensures this is set.
|
|
||||||
Logger.printInfo(() -> "Permanent repeat enabled: " + Settings.PERMANENT_REPEAT.get());
|
|
||||||
|
|
||||||
// YT Music always uses dark mode.
|
|
||||||
Utils.setIsDarkModeEnabled(true);
|
|
||||||
|
|
||||||
return new GoogleApiActivityHook();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the fixed theme for the activity.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void customizeActivityTheme(Activity activity) {
|
|
||||||
// Override the default YouTube Music theme to increase start padding of list items.
|
|
||||||
// Custom style located in resources/music/values/style.xml
|
|
||||||
activity.setTheme(Utils.getResourceIdentifier("Theme.ReVanced.YouTubeMusic.Settings", "style"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the resource ID for the YouTube Music settings layout.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected int getContentViewResourceId() {
|
|
||||||
return Utils.getResourceIdentifier("revanced_music_settings_with_toolbar", "layout");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the fixed background color for the toolbar.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected int getToolbarBackgroundColor() {
|
|
||||||
return Utils.getResourceColor("ytm_color_black");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the navigation icon with a color filter applied.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected Drawable getNavigationIcon() {
|
|
||||||
Drawable navigationIcon = ReVancedPreferenceFragment.getBackButtonDrawable();
|
|
||||||
navigationIcon.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
|
||||||
return navigationIcon;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the click listener that finishes the activity when the navigation icon is clicked.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected View.OnClickListener getNavigationClickListener(Activity activity) {
|
|
||||||
return view -> activity.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new ReVancedPreferenceFragment for the activity.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected PreferenceFragment createPreferenceFragment() {
|
|
||||||
return new ReVancedPreferenceFragment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package app.revanced.extension.music.settings;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.preference.PreferenceFragment;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
|
||||||
|
import app.revanced.extension.music.settings.search.MusicSearchViewController;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.BaseActivityHook;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hooks GoogleApiActivity to inject a custom {@link MusicPreferenceFragment} with a toolbar and search.
|
||||||
|
*/
|
||||||
|
public class MusicActivityHook extends BaseActivityHook {
|
||||||
|
|
||||||
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
public static MusicSearchViewController searchViewController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static void initialize(Activity parentActivity) {
|
||||||
|
// Must touch the Music settings to ensure the class is loaded and
|
||||||
|
// the values can be found when setting the UI preferences.
|
||||||
|
// Logging anything under non debug ensures this is set.
|
||||||
|
Logger.printInfo(() -> "Permanent repeat enabled: " + Settings.PERMANENT_REPEAT.get());
|
||||||
|
|
||||||
|
// YT Music always uses dark mode.
|
||||||
|
Utils.setIsDarkModeEnabled(true);
|
||||||
|
|
||||||
|
BaseActivityHook.initialize(new MusicActivityHook(), parentActivity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the fixed theme for the activity.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void customizeActivityTheme(Activity activity) {
|
||||||
|
// Override the default YouTube Music theme to increase start padding of list items.
|
||||||
|
// Custom style located in resources/music/values/style.xml
|
||||||
|
activity.setTheme(Utils.getResourceIdentifierOrThrow(
|
||||||
|
"Theme.ReVanced.YouTubeMusic.Settings", "style"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the resource ID for the YouTube Music settings layout.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected int getContentViewResourceId() {
|
||||||
|
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the fixed background color for the toolbar.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected int getToolbarBackgroundColor() {
|
||||||
|
return Utils.getResourceColor("ytm_color_black");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the navigation icon with a color filter applied.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected Drawable getNavigationIcon() {
|
||||||
|
Drawable navigationIcon = MusicPreferenceFragment.getBackButtonDrawable();
|
||||||
|
navigationIcon.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
||||||
|
return navigationIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the click listener that finishes the activity when the navigation icon is clicked.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected View.OnClickListener getNavigationClickListener(Activity activity) {
|
||||||
|
return view -> {
|
||||||
|
if (searchViewController != null && searchViewController.isSearchActive()) {
|
||||||
|
searchViewController.closeSearch();
|
||||||
|
} else {
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds search view components to the toolbar for {@link MusicPreferenceFragment}.
|
||||||
|
*
|
||||||
|
* @param activity The activity hosting the toolbar.
|
||||||
|
* @param toolbar The configured toolbar.
|
||||||
|
* @param fragment The PreferenceFragment associated with the activity.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {
|
||||||
|
if (fragment instanceof MusicPreferenceFragment) {
|
||||||
|
searchViewController = MusicSearchViewController.addSearchViewComponents(
|
||||||
|
activity, toolbar, (MusicPreferenceFragment) fragment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new {@link MusicPreferenceFragment} for the activity.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected PreferenceFragment createPreferenceFragment() {
|
||||||
|
return new MusicPreferenceFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* <p>
|
||||||
|
* Overrides {@link Activity#finish()} of the injection Activity.
|
||||||
|
*
|
||||||
|
* @return if the original activity finish method should be allowed to run.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static boolean handleFinish() {
|
||||||
|
return MusicSearchViewController.handleFinish(searchViewController);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package app.revanced.extension.music.settings;
|
|||||||
|
|
||||||
import static java.lang.Boolean.FALSE;
|
import static java.lang.Boolean.FALSE;
|
||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package app.revanced.extension.music.settings.preference;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.MusicActivityHook;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference fragment for ReVanced settings.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class MusicPreferenceFragment extends ToolbarPreferenceFragment {
|
||||||
|
/**
|
||||||
|
* The main PreferenceScreen used to display the current set of preferences.
|
||||||
|
*/
|
||||||
|
private PreferenceScreen preferenceScreen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the preference fragment.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void initialize() {
|
||||||
|
super.initialize();
|
||||||
|
|
||||||
|
try {
|
||||||
|
preferenceScreen = getPreferenceScreen();
|
||||||
|
Utils.sortPreferenceGroups(preferenceScreen);
|
||||||
|
setPreferenceScreenToolbar(preferenceScreen);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the fragment starts.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
try {
|
||||||
|
// Initialize search controller if needed
|
||||||
|
if (MusicActivityHook.searchViewController != null) {
|
||||||
|
// Trigger search data collection after fragment is ready.
|
||||||
|
MusicActivityHook.searchViewController.initializeSearchData();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onStart failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets toolbar for all nested preference screens.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void customizeToolbar(Toolbar toolbar) {
|
||||||
|
MusicActivityHook.setToolbarLayoutParams(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform actions after toolbar setup.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {
|
||||||
|
if (MusicActivityHook.searchViewController != null
|
||||||
|
&& MusicActivityHook.searchViewController.isSearchActive()) {
|
||||||
|
toolbar.post(() -> MusicActivityHook.searchViewController.closeSearch());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the preference screen for external access by SearchViewController.
|
||||||
|
*/
|
||||||
|
public PreferenceScreen getPreferenceScreenForSearch() {
|
||||||
|
return preferenceScreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
package app.revanced.extension.music.settings.preference;
|
|
||||||
|
|
||||||
import android.widget.Toolbar;
|
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.GoogleApiActivityHook;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preference fragment for ReVanced settings.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"deprecation", "NewApi"})
|
|
||||||
public class ReVancedPreferenceFragment extends ToolbarPreferenceFragment {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the preference fragment.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void initialize() {
|
|
||||||
super.initialize();
|
|
||||||
|
|
||||||
try {
|
|
||||||
Utils.sortPreferenceGroups(getPreferenceScreen());
|
|
||||||
setPreferenceScreenToolbar(getPreferenceScreen());
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "initialize failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets toolbar for all nested preference screens.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void customizeToolbar(Toolbar toolbar) {
|
|
||||||
GoogleApiActivityHook.setToolbarLayoutParams(toolbar);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package app.revanced.extension.music.settings.search;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchResultsAdapter;
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchViewController;
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchResultItem;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Music-specific search results adapter.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class MusicSearchResultsAdapter extends BaseSearchResultsAdapter {
|
||||||
|
|
||||||
|
public MusicSearchResultsAdapter(Context context, List<BaseSearchResultItem> items,
|
||||||
|
BaseSearchViewController.BasePreferenceFragment fragment,
|
||||||
|
BaseSearchViewController searchViewController) {
|
||||||
|
super(context, items, fragment, searchViewController);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PreferenceScreen getMainPreferenceScreen() {
|
||||||
|
return fragment.getPreferenceScreenForSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package app.revanced.extension.music.settings.search;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import app.revanced.extension.music.settings.preference.MusicPreferenceFragment;
|
||||||
|
import app.revanced.extension.shared.settings.search.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Music-specific search view controller implementation.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class MusicSearchViewController extends BaseSearchViewController {
|
||||||
|
|
||||||
|
public static MusicSearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar,
|
||||||
|
MusicPreferenceFragment fragment) {
|
||||||
|
return new MusicSearchViewController(activity, toolbar, fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MusicSearchViewController(Activity activity, Toolbar toolbar, MusicPreferenceFragment fragment) {
|
||||||
|
super(activity, toolbar, new PreferenceFragmentAdapter(fragment));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BaseSearchResultsAdapter createSearchResultsAdapter() {
|
||||||
|
return new MusicSearchResultsAdapter(activity, filteredSearchItems, fragment, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isSpecialPreferenceGroup(Preference preference) {
|
||||||
|
// Music doesn't have SponsorBlock, so no special groups.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupSpecialPreferenceListeners(BaseSearchResultItem item) {
|
||||||
|
// Music doesn't have special preferences.
|
||||||
|
// This method can be empty or handle music-specific preferences if any.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static method for handling Activity finish
|
||||||
|
public static boolean handleFinish(MusicSearchViewController searchViewController) {
|
||||||
|
if (searchViewController != null && searchViewController.isSearchActive()) {
|
||||||
|
searchViewController.closeSearch();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapter to wrap MusicPreferenceFragment to BasePreferenceFragment interface.
|
||||||
|
private record PreferenceFragmentAdapter(MusicPreferenceFragment fragment) implements BasePreferenceFragment {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PreferenceScreen getPreferenceScreenForSearch() {
|
||||||
|
return fragment.getPreferenceScreenForSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView() {
|
||||||
|
return fragment.getView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return fragment.getActivity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ import java.util.Locale;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.requests.Requester;
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
import app.revanced.extension.shared.requests.Route;
|
import app.revanced.extension.shared.requests.Route;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class GmsCoreSupport {
|
public class GmsCoreSupport {
|
||||||
@@ -80,17 +81,17 @@ public class GmsCoreSupport {
|
|||||||
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
// Create the custom dialog.
|
// Create the custom dialog.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
str("gms_core_dialog_title"), // Title.
|
str("gms_core_dialog_title"), // Title.
|
||||||
str(dialogMessageRef), // Message.
|
str(dialogMessageRef), // Message.
|
||||||
null, // No EditText.
|
null, // No EditText.
|
||||||
str(positiveButtonTextRef), // OK button text.
|
str(positiveButtonTextRef), // OK button text.
|
||||||
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
|
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
|
||||||
null, // No Cancel button action.
|
null, // No Cancel button action.
|
||||||
null, // No Neutral button text.
|
null, // No Neutral button text.
|
||||||
null, // No Neutral button action.
|
null, // No Neutral button action.
|
||||||
true // Dismiss dialog when onNeutralClick.
|
true // Dismiss dialog when onNeutralClick.
|
||||||
);
|
);
|
||||||
|
|
||||||
Dialog dialog = dialogPair.first;
|
Dialog dialog = dialogPair.first;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import android.annotation.SuppressLint;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.app.DialogFragment;
|
import android.app.DialogFragment;
|
||||||
|
import android.content.ClipData;
|
||||||
|
import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
@@ -12,9 +14,6 @@ import android.content.pm.PackageManager;
|
|||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.graphics.drawable.ShapeDrawable;
|
|
||||||
import android.graphics.drawable.shapes.RoundRectShape;
|
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -23,9 +22,6 @@ import android.os.Looper;
|
|||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceGroup;
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
|
||||||
import android.util.DisplayMetrics;
|
import android.util.DisplayMetrics;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
@@ -37,13 +33,9 @@ import android.view.Window;
|
|||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.view.animation.Animation;
|
import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.ScrollView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
@@ -69,6 +61,7 @@ import app.revanced.extension.shared.settings.BaseSettings;
|
|||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
||||||
|
|
||||||
|
@SuppressWarnings("NewApi")
|
||||||
public class Utils {
|
public class Utils {
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
@@ -278,41 +271,63 @@ public class Utils {
|
|||||||
* @return zero, if the resource is not found.
|
* @return zero, if the resource is not found.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("DiscouragedApi")
|
@SuppressLint("DiscouragedApi")
|
||||||
public static int getResourceIdentifier(Context context, String resourceIdentifierName, String type) {
|
public static int getResourceIdentifier(Context context, String resourceIdentifierName, @Nullable String type) {
|
||||||
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
|
return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getResourceIdentifierOrThrow(Context context, String resourceIdentifierName, @Nullable String type) {
|
||||||
|
final int resourceId = getResourceIdentifier(context, resourceIdentifierName, type);
|
||||||
|
if (resourceId == 0) {
|
||||||
|
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
|
||||||
|
+ " type: " + type);
|
||||||
|
}
|
||||||
|
return resourceId;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return zero, if the resource is not found.
|
* @return zero, if the resource is not found.
|
||||||
|
* @see #getResourceIdentifierOrThrow(String, String)
|
||||||
*/
|
*/
|
||||||
public static int getResourceIdentifier(String resourceIdentifierName, String type) {
|
public static int getResourceIdentifier(String resourceIdentifierName, @Nullable String type) {
|
||||||
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
return getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The resource identifier, or throws an exception if not found.
|
||||||
|
*/
|
||||||
|
public static int getResourceIdentifierOrThrow(String resourceIdentifierName, @Nullable String type) {
|
||||||
|
final int resourceId = getResourceIdentifier(getContext(), resourceIdentifierName, type);
|
||||||
|
if (resourceId == 0) {
|
||||||
|
throw new Resources.NotFoundException("No resource id exists with name: " + resourceIdentifierName
|
||||||
|
+ " type: " + type);
|
||||||
|
}
|
||||||
|
return resourceId;
|
||||||
|
}
|
||||||
|
|
||||||
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static int getResourceInteger(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer"));
|
return getContext().getResources().getInteger(getResourceIdentifierOrThrow(resourceIdentifierName, "integer"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static Animation getResourceAnimation(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim"));
|
return AnimationUtils.loadAnimation(getContext(), getResourceIdentifierOrThrow(resourceIdentifierName, "anim"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static int getResourceColor(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
//noinspection deprecation
|
//noinspection deprecation
|
||||||
return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color"));
|
return getContext().getResources().getColor(getResourceIdentifierOrThrow(resourceIdentifierName, "color"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static int getResourceDimensionPixelSize(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
return getContext().getResources().getDimensionPixelSize(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static float getResourceDimension(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen"));
|
return getContext().getResources().getDimension(getResourceIdentifierOrThrow(resourceIdentifierName, "dimen"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
|
public static String[] getResourceStringArray(String resourceIdentifierName) throws Resources.NotFoundException {
|
||||||
return getContext().getResources().getStringArray(getResourceIdentifier(resourceIdentifierName, "array"));
|
return getContext().getResources().getStringArray(getResourceIdentifierOrThrow(resourceIdentifierName, "array"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface MatchFilter<T> {
|
public interface MatchFilter<T> {
|
||||||
@@ -323,13 +338,9 @@ public class Utils {
|
|||||||
* Includes sub children.
|
* Includes sub children.
|
||||||
*/
|
*/
|
||||||
public static <R extends View> R getChildViewByResourceName(View view, String str) {
|
public static <R extends View> R getChildViewByResourceName(View view, String str) {
|
||||||
var child = view.findViewById(Utils.getResourceIdentifier(str, "id"));
|
var child = view.findViewById(Utils.getResourceIdentifierOrThrow(str, "id"));
|
||||||
if (child != null) {
|
//noinspection unchecked
|
||||||
//noinspection unchecked
|
return (R) child;
|
||||||
return (R) child;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new IllegalArgumentException("View with resource name not found: " + str);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -415,9 +426,9 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void setClipboard(CharSequence text) {
|
public static void setClipboard(CharSequence text) {
|
||||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context
|
ClipboardManager clipboard = (ClipboardManager) context
|
||||||
.getSystemService(Context.CLIPBOARD_SERVICE);
|
.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
|
ClipData clip = ClipData.newPlainText("ReVanced", text);
|
||||||
clipboard.setPrimaryClip(clip);
|
clipboard.setPrimaryClip(clip);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -747,396 +758,32 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a custom dialog with a styled layout, including a title, message, buttons, and an
|
* Configures the parameters of a dialog window, including its width, gravity, vertical offset and background dimming.
|
||||||
* optional EditText. The dialog's appearance adapts to the app's dark mode setting, with
|
* The width is calculated as a percentage of the screen's portrait width and the vertical offset is specified in DIP.
|
||||||
* rounded corners and customizable button actions. Buttons adjust dynamically to their text
|
* The default dialog background is removed to allow for custom styling.
|
||||||
* content and are arranged in a single row if they fit within 80% of the screen width,
|
|
||||||
* with the Neutral button aligned to the left and OK/Cancel buttons centered on the right.
|
|
||||||
* If buttons do not fit, each is placed on a separate row, all aligned to the right.
|
|
||||||
*
|
*
|
||||||
* @param context Context used to create the dialog.
|
* @param window The {@link Window} object to configure.
|
||||||
* @param title Title text of the dialog.
|
* @param gravity The gravity for positioning the dialog (e.g., {@link Gravity#BOTTOM}).
|
||||||
* @param message Message text of the dialog (supports Spanned for HTML), or null if replaced by EditText.
|
* @param yOffsetDip The vertical offset from the gravity position in DIP.
|
||||||
* @param editText EditText to include in the dialog, or null if no EditText is needed.
|
* @param widthPercentage The width of the dialog as a percentage of the screen's portrait width (0-100).
|
||||||
* @param okButtonText OK button text, or null to use the default "OK" string.
|
* @param dimAmount If true, sets the background dim amount to 0 (no dimming); if false, leaves the default dim amount.
|
||||||
* @param onOkClick Action to perform when the OK button is clicked.
|
|
||||||
* @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed.
|
|
||||||
* @param neutralButtonText Neutral button text, or null if no Neutral button is needed.
|
|
||||||
* @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed.
|
|
||||||
* @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked.
|
|
||||||
* @return The Dialog and its main LinearLayout container.
|
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("ExtractMethodRecommender")
|
public static void setDialogWindowParameters(Window window, int gravity, int yOffsetDip, int widthPercentage, boolean dimAmount) {
|
||||||
public static Pair<Dialog, LinearLayout> createCustomDialog(
|
|
||||||
Context context, String title, CharSequence message, @Nullable EditText editText,
|
|
||||||
String okButtonText, Runnable onOkClick, Runnable onCancelClick,
|
|
||||||
@Nullable String neutralButtonText, @Nullable Runnable onNeutralClick,
|
|
||||||
boolean dismissDialogOnNeutralClick
|
|
||||||
) {
|
|
||||||
Logger.printDebug(() -> "Creating custom dialog with title: " + title);
|
|
||||||
|
|
||||||
Dialog dialog = new Dialog(context);
|
|
||||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
|
|
||||||
|
|
||||||
// Preset size constants.
|
|
||||||
final int dip4 = dipToPixels(4);
|
|
||||||
final int dip8 = dipToPixels(8);
|
|
||||||
final int dip16 = dipToPixels(16);
|
|
||||||
final int dip24 = dipToPixels(24);
|
|
||||||
|
|
||||||
// Create main layout.
|
|
||||||
LinearLayout mainLayout = new LinearLayout(context);
|
|
||||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
mainLayout.setPadding(dip24, dip16, dip24, dip24);
|
|
||||||
// Set rounded rectangle background.
|
|
||||||
ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
|
|
||||||
createCornerRadii(28), null, null));
|
|
||||||
mainBackground.getPaint().setColor(getDialogBackgroundColor()); // Dialog background.
|
|
||||||
mainLayout.setBackground(mainBackground);
|
|
||||||
|
|
||||||
// Title.
|
|
||||||
if (!TextUtils.isEmpty(title)) {
|
|
||||||
TextView titleView = new TextView(context);
|
|
||||||
titleView.setText(title);
|
|
||||||
titleView.setTypeface(Typeface.DEFAULT_BOLD);
|
|
||||||
titleView.setTextSize(18);
|
|
||||||
titleView.setTextColor(getAppForegroundColor());
|
|
||||||
titleView.setGravity(Gravity.CENTER);
|
|
||||||
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
);
|
|
||||||
layoutParams.setMargins(0, 0, 0, dip16);
|
|
||||||
titleView.setLayoutParams(layoutParams);
|
|
||||||
mainLayout.addView(titleView);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create content container (message/EditText) inside a ScrollView only if message or editText is provided.
|
|
||||||
ScrollView contentScrollView = null;
|
|
||||||
LinearLayout contentContainer;
|
|
||||||
if (message != null || editText != null) {
|
|
||||||
contentScrollView = new ScrollView(context);
|
|
||||||
contentScrollView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar.
|
|
||||||
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
|
||||||
if (editText != null) {
|
|
||||||
ShapeDrawable scrollViewBackground = new ShapeDrawable(new RoundRectShape(
|
|
||||||
createCornerRadii(10), null, null));
|
|
||||||
scrollViewBackground.getPaint().setColor(getEditTextBackground());
|
|
||||||
contentScrollView.setPadding(dip8, dip8, dip8, dip8);
|
|
||||||
contentScrollView.setBackground(scrollViewBackground);
|
|
||||||
contentScrollView.setClipToOutline(true);
|
|
||||||
}
|
|
||||||
LinearLayout.LayoutParams contentParams = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
0,
|
|
||||||
1.0f // Weight to take available space.
|
|
||||||
);
|
|
||||||
contentScrollView.setLayoutParams(contentParams);
|
|
||||||
contentContainer = new LinearLayout(context);
|
|
||||||
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
contentScrollView.addView(contentContainer);
|
|
||||||
|
|
||||||
// Message (if not replaced by EditText).
|
|
||||||
if (editText == null) {
|
|
||||||
TextView messageView = new TextView(context);
|
|
||||||
messageView.setText(message); // Supports Spanned (HTML).
|
|
||||||
messageView.setTextSize(16);
|
|
||||||
messageView.setTextColor(getAppForegroundColor());
|
|
||||||
// Enable HTML link clicking if the message contains links.
|
|
||||||
if (message instanceof Spanned) {
|
|
||||||
messageView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
}
|
|
||||||
LinearLayout.LayoutParams messageParams = new LinearLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
);
|
|
||||||
messageView.setLayoutParams(messageParams);
|
|
||||||
contentContainer.addView(messageView);
|
|
||||||
}
|
|
||||||
|
|
||||||
// EditText (if provided).
|
|
||||||
if (editText != null) {
|
|
||||||
// Remove EditText from its current parent, if any.
|
|
||||||
ViewGroup parent = (ViewGroup) editText.getParent();
|
|
||||||
if (parent != null) {
|
|
||||||
parent.removeView(editText);
|
|
||||||
}
|
|
||||||
// Style the EditText to match the dialog theme.
|
|
||||||
editText.setTextColor(getAppForegroundColor());
|
|
||||||
editText.setBackgroundColor(Color.TRANSPARENT);
|
|
||||||
editText.setPadding(0, 0, 0, 0);
|
|
||||||
LinearLayout.LayoutParams editTextParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
|
||||||
);
|
|
||||||
contentContainer.addView(editText, editTextParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Button container.
|
|
||||||
LinearLayout buttonContainer = new LinearLayout(context);
|
|
||||||
buttonContainer.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
buttonContainer.removeAllViews();
|
|
||||||
LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
|
||||||
);
|
|
||||||
buttonContainerParams.setMargins(0, dip16, 0, 0);
|
|
||||||
buttonContainer.setLayoutParams(buttonContainerParams);
|
|
||||||
|
|
||||||
// Lists to track buttons.
|
|
||||||
List<Button> buttons = new ArrayList<>();
|
|
||||||
List<Integer> buttonWidths = new ArrayList<>();
|
|
||||||
|
|
||||||
// Create buttons in order: Neutral, Cancel, OK.
|
|
||||||
if (neutralButtonText != null && onNeutralClick != null) {
|
|
||||||
Button neutralButton = addButton(
|
|
||||||
context,
|
|
||||||
neutralButtonText,
|
|
||||||
onNeutralClick,
|
|
||||||
false,
|
|
||||||
dismissDialogOnNeutralClick,
|
|
||||||
dialog
|
|
||||||
);
|
|
||||||
buttons.add(neutralButton);
|
|
||||||
neutralButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
|
||||||
buttonWidths.add(neutralButton.getMeasuredWidth());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onCancelClick != null) {
|
|
||||||
Button cancelButton = addButton(
|
|
||||||
context,
|
|
||||||
context.getString(android.R.string.cancel),
|
|
||||||
onCancelClick,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
dialog
|
|
||||||
);
|
|
||||||
buttons.add(cancelButton);
|
|
||||||
cancelButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
|
||||||
buttonWidths.add(cancelButton.getMeasuredWidth());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (onOkClick != null) {
|
|
||||||
Button okButton = addButton(
|
|
||||||
context,
|
|
||||||
okButtonText != null ? okButtonText : context.getString(android.R.string.ok),
|
|
||||||
onOkClick,
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
dialog
|
|
||||||
);
|
|
||||||
buttons.add(okButton);
|
|
||||||
okButton.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
|
||||||
buttonWidths.add(okButton.getMeasuredWidth());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle button layout.
|
|
||||||
int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
|
|
||||||
int totalWidth = 0;
|
|
||||||
for (Integer width : buttonWidths) {
|
|
||||||
totalWidth += width;
|
|
||||||
}
|
|
||||||
if (buttonWidths.size() > 1) {
|
|
||||||
totalWidth += (buttonWidths.size() - 1) * dip8; // Add margins for gaps.
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buttons.size() == 1) {
|
|
||||||
// Single button: stretch to full width.
|
|
||||||
Button singleButton = buttons.get(0);
|
|
||||||
LinearLayout singleContainer = new LinearLayout(context);
|
|
||||||
singleContainer.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
singleContainer.setGravity(Gravity.CENTER);
|
|
||||||
ViewGroup parent = (ViewGroup) singleButton.getParent();
|
|
||||||
if (parent != null) {
|
|
||||||
parent.removeView(singleButton);
|
|
||||||
}
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
dipToPixels(36)
|
|
||||||
);
|
|
||||||
params.setMargins(0, 0, 0, 0);
|
|
||||||
singleButton.setLayoutParams(params);
|
|
||||||
singleContainer.addView(singleButton);
|
|
||||||
buttonContainer.addView(singleContainer);
|
|
||||||
} else if (buttons.size() > 1) {
|
|
||||||
// Check if buttons fit in one row.
|
|
||||||
if (totalWidth <= screenWidth * 0.8) {
|
|
||||||
// Single row: Neutral, Cancel, OK.
|
|
||||||
LinearLayout rowContainer = new LinearLayout(context);
|
|
||||||
rowContainer.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
rowContainer.setGravity(Gravity.CENTER);
|
|
||||||
rowContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
|
||||||
));
|
|
||||||
|
|
||||||
// Add all buttons with proportional weights and specific margins.
|
|
||||||
for (int i = 0; i < buttons.size(); i++) {
|
|
||||||
Button button = buttons.get(i);
|
|
||||||
ViewGroup parent = (ViewGroup) button.getParent();
|
|
||||||
if (parent != null) {
|
|
||||||
parent.removeView(button);
|
|
||||||
}
|
|
||||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
|
||||||
0,
|
|
||||||
dipToPixels(36),
|
|
||||||
buttonWidths.get(i) // Use measured width as weight.
|
|
||||||
);
|
|
||||||
// Set margins based on button type and combination.
|
|
||||||
if (buttons.size() == 2) {
|
|
||||||
// Neutral + OK or Cancel + OK.
|
|
||||||
if (i == 0) { // Neutral or Cancel.
|
|
||||||
params.setMargins(0, 0, dip4, 0);
|
|
||||||
} else { // OK
|
|
||||||
params.setMargins(dip4, 0, 0, 0);
|
|
||||||
}
|
|
||||||
} else if (buttons.size() == 3) {
|
|
||||||
if (i == 0) { // Neutral.
|
|
||||||
params.setMargins(0, 0, dip4, 0);
|
|
||||||
} else if (i == 1) { // Cancel
|
|
||||||
params.setMargins(dip4, 0, dip4, 0);
|
|
||||||
} else { // OK
|
|
||||||
params.setMargins(dip4, 0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
button.setLayoutParams(params);
|
|
||||||
rowContainer.addView(button);
|
|
||||||
}
|
|
||||||
buttonContainer.addView(rowContainer);
|
|
||||||
} else {
|
|
||||||
// Multiple rows: OK, Cancel, Neutral.
|
|
||||||
List<Button> reorderedButtons = new ArrayList<>();
|
|
||||||
// Reorder: OK, Cancel, Neutral.
|
|
||||||
if (onOkClick != null) {
|
|
||||||
reorderedButtons.add(buttons.get(buttons.size() - 1));
|
|
||||||
}
|
|
||||||
if (onCancelClick != null) {
|
|
||||||
reorderedButtons.add(buttons.get((neutralButtonText != null && onNeutralClick != null) ? 1 : 0));
|
|
||||||
}
|
|
||||||
if (neutralButtonText != null && onNeutralClick != null) {
|
|
||||||
reorderedButtons.add(buttons.get(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add each button in its own row with spacers.
|
|
||||||
for (int i = 0; i < reorderedButtons.size(); i++) {
|
|
||||||
Button button = reorderedButtons.get(i);
|
|
||||||
LinearLayout singleContainer = new LinearLayout(context);
|
|
||||||
singleContainer.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
singleContainer.setGravity(Gravity.CENTER);
|
|
||||||
singleContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
dipToPixels(36)
|
|
||||||
));
|
|
||||||
ViewGroup parent = (ViewGroup) button.getParent();
|
|
||||||
if (parent != null) {
|
|
||||||
parent.removeView(button);
|
|
||||||
}
|
|
||||||
LinearLayout.LayoutParams buttonParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
dipToPixels(36)
|
|
||||||
);
|
|
||||||
buttonParams.setMargins(0, 0, 0, 0);
|
|
||||||
button.setLayoutParams(buttonParams);
|
|
||||||
singleContainer.addView(button);
|
|
||||||
buttonContainer.addView(singleContainer);
|
|
||||||
|
|
||||||
// Add a spacer between the buttons (except the last one).
|
|
||||||
// Adding a margin between buttons is not suitable, as it conflicts with the single row layout.
|
|
||||||
if (i < reorderedButtons.size() - 1) {
|
|
||||||
View spacer = new View(context);
|
|
||||||
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
dipToPixels(8)
|
|
||||||
);
|
|
||||||
spacer.setLayoutParams(spacerParams);
|
|
||||||
buttonContainer.addView(spacer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ScrollView to main layout only if content exist.
|
|
||||||
if (contentScrollView != null) {
|
|
||||||
mainLayout.addView(contentScrollView);
|
|
||||||
}
|
|
||||||
mainLayout.addView(buttonContainer);
|
|
||||||
dialog.setContentView(mainLayout);
|
|
||||||
|
|
||||||
// Set dialog window attributes.
|
|
||||||
Window window = dialog.getWindow();
|
|
||||||
if (window != null) {
|
|
||||||
setDialogWindowParameters(window);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Pair<>(dialog, mainLayout);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setDialogWindowParameters(Window window) {
|
|
||||||
WindowManager.LayoutParams params = window.getAttributes();
|
WindowManager.LayoutParams params = window.getAttributes();
|
||||||
|
|
||||||
DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
|
DisplayMetrics displayMetrics = Resources.getSystem().getDisplayMetrics();
|
||||||
int portraitWidth = (int) (displayMetrics.widthPixels * 0.9);
|
int portraitWidth = Math.min(displayMetrics.widthPixels, displayMetrics.heightPixels);
|
||||||
|
|
||||||
if (Resources.getSystem().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
params.width = (int) (portraitWidth * (widthPercentage / 100.0f)); // Set width based on parameters.
|
||||||
portraitWidth = (int) Math.min(portraitWidth, displayMetrics.heightPixels * 0.9);
|
|
||||||
}
|
|
||||||
params.width = portraitWidth;
|
|
||||||
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||||
params.gravity = Gravity.CENTER;
|
params.gravity = gravity;
|
||||||
window.setAttributes(params);
|
params.y = yOffsetDip > 0 ? dipToPixels(yOffsetDip) : 0;
|
||||||
window.setBackgroundDrawable(null); // Remove default dialog background.
|
if (dimAmount) {
|
||||||
}
|
params.dimAmount = 0f;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
window.setAttributes(params); // Apply window attributes.
|
||||||
* Adds a styled button to a dialog's button container with customizable text, click behavior, and appearance.
|
window.setBackgroundDrawable(null); // Remove default dialog background
|
||||||
* The button's background and text colors adapt to the app's dark mode setting. Buttons stretch to full width
|
|
||||||
* when on separate rows or proportionally based on content when in a single row (Neutral, Cancel, OK order).
|
|
||||||
* When wrapped to separate rows, buttons are ordered OK, Cancel, Neutral.
|
|
||||||
*
|
|
||||||
* @param context Context to create the button and access resources.
|
|
||||||
* @param buttonText Button text to display.
|
|
||||||
* @param onClick Action to perform when the button is clicked, or null if no action is required.
|
|
||||||
* @param isOkButton If this is the OK button, which uses distinct background and text colors.
|
|
||||||
* @param dismissDialog If the dialog should be dismissed when the button is clicked.
|
|
||||||
* @param dialog The Dialog to dismiss when the button is clicked.
|
|
||||||
* @return The created Button.
|
|
||||||
*/
|
|
||||||
private static Button addButton(Context context, String buttonText, Runnable onClick,
|
|
||||||
boolean isOkButton, boolean dismissDialog, Dialog dialog) {
|
|
||||||
Button button = new Button(context, null, 0);
|
|
||||||
button.setText(buttonText);
|
|
||||||
button.setTextSize(14);
|
|
||||||
button.setAllCaps(false);
|
|
||||||
button.setSingleLine(true);
|
|
||||||
button.setEllipsize(android.text.TextUtils.TruncateAt.END);
|
|
||||||
button.setGravity(Gravity.CENTER);
|
|
||||||
|
|
||||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(createCornerRadii(20), null, null));
|
|
||||||
int backgroundColor = isOkButton
|
|
||||||
? getOkButtonBackgroundColor() // Background color for OK button (inversion).
|
|
||||||
: getCancelOrNeutralButtonBackgroundColor(); // Background color for Cancel or Neutral buttons.
|
|
||||||
background.getPaint().setColor(backgroundColor);
|
|
||||||
button.setBackground(background);
|
|
||||||
|
|
||||||
button.setTextColor(isDarkModeEnabled()
|
|
||||||
? (isOkButton ? Color.BLACK : Color.WHITE)
|
|
||||||
: (isOkButton ? Color.WHITE : Color.BLACK));
|
|
||||||
|
|
||||||
// Set internal padding.
|
|
||||||
final int dip16 = dipToPixels(16);
|
|
||||||
button.setPadding(dip16, 0, dip16, 0);
|
|
||||||
|
|
||||||
button.setOnClickListener(v -> {
|
|
||||||
if (onClick != null) {
|
|
||||||
onClick.run();
|
|
||||||
}
|
|
||||||
if (dismissDialog) {
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return button;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1323,9 +970,9 @@ public class Utils {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort a PreferenceGroup and all it's sub groups by title or key.
|
* Sort a PreferenceGroup and all it's sub groups by title or key.
|
||||||
*
|
* <p>
|
||||||
* Sort order is determined by the preferences key {@link Sort} suffix.
|
* Sort order is determined by the preferences key {@link Sort} suffix.
|
||||||
*
|
* <p>
|
||||||
* If a preference has no key or no {@link Sort} suffix,
|
* If a preference has no key or no {@link Sort} suffix,
|
||||||
* then the preferences are left unsorted.
|
* then the preferences are left unsorted.
|
||||||
*/
|
*/
|
||||||
@@ -1388,7 +1035,7 @@ public class Utils {
|
|||||||
* Set all preferences to multiline titles if the device is not using an English variant.
|
* Set all preferences to multiline titles if the device is not using an English variant.
|
||||||
* The English strings are heavily scrutinized and all titles fit on screen
|
* The English strings are heavily scrutinized and all titles fit on screen
|
||||||
* except 2 or 3 preference strings and those do not affect readability.
|
* except 2 or 3 preference strings and those do not affect readability.
|
||||||
*
|
* <p>
|
||||||
* Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
|
* Allowing multiline for those 2 or 3 English preferences looks weird and out of place,
|
||||||
* and visually it looks better to clip the text and keep all titles 1 line.
|
* and visually it looks better to clip the text and keep all titles 1 line.
|
||||||
*/
|
*/
|
||||||
@@ -1497,9 +1144,9 @@ public class Utils {
|
|||||||
blue = Math.round(blue + (255 - blue) * t);
|
blue = Math.round(blue + (255 - blue) * t);
|
||||||
} else {
|
} else {
|
||||||
// Darken or no change: Scale toward black.
|
// Darken or no change: Scale toward black.
|
||||||
red *= factor;
|
red = Math.round(red * factor);
|
||||||
green *= factor;
|
green = Math.round(green * factor);
|
||||||
blue *= factor;
|
blue = Math.round(blue * factor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure values are within [0, 255].
|
// Ensure values are within [0, 255].
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package app.revanced.extension.shared.checks;
|
|||||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@@ -26,6 +25,7 @@ import java.util.Collection;
|
|||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
abstract class Check {
|
abstract class Check {
|
||||||
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2;
|
||||||
@@ -93,7 +93,7 @@ abstract class Check {
|
|||||||
|
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
// Create the custom dialog.
|
// Create the custom dialog.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
activity,
|
activity,
|
||||||
str("revanced_check_environment_failed_title"), // Title.
|
str("revanced_check_environment_failed_title"), // Title.
|
||||||
message, // Message.
|
message, // Message.
|
||||||
@@ -127,7 +127,8 @@ abstract class Check {
|
|||||||
|
|
||||||
// Add icon to the dialog.
|
// Add icon to the dialog.
|
||||||
ImageView iconView = new ImageView(activity);
|
ImageView iconView = new ImageView(activity);
|
||||||
iconView.setImageResource(Utils.getResourceIdentifier("revanced_ic_dialog_alert", "drawable"));
|
iconView.setImageResource(Utils.getResourceIdentifierOrThrow(
|
||||||
|
"revanced_ic_dialog_alert", "drawable"));
|
||||||
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
||||||
iconView.setPadding(0, 0, 0, 0);
|
iconView.setPadding(0, 0, 0, 0);
|
||||||
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
|
||||||
@@ -158,8 +159,8 @@ abstract class Check {
|
|||||||
Button ignoreButton;
|
Button ignoreButton;
|
||||||
|
|
||||||
// Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
|
// Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
|
||||||
if (buttonContainer.getChildCount() == 1 && buttonContainer.getChildAt(0) instanceof LinearLayout) {
|
if (buttonContainer.getChildCount() == 1
|
||||||
LinearLayout rowContainer = (LinearLayout) buttonContainer.getChildAt(0);
|
&& buttonContainer.getChildAt(0) instanceof LinearLayout rowContainer) {
|
||||||
// Neutral button is the first child (index 0).
|
// Neutral button is the first child (index 0).
|
||||||
ignoreButton = (Button) rowContainer.getChildAt(0);
|
ignoreButton = (Button) rowContainer.getChildAt(0);
|
||||||
// OK button is the last child.
|
// OK button is the last child.
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package app.revanced.extension.shared.settings;
|
package app.revanced.extension.shared.settings;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
@@ -21,6 +24,15 @@ import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragme
|
|||||||
@SuppressWarnings({"deprecation", "NewApi"})
|
@SuppressWarnings({"deprecation", "NewApi"})
|
||||||
public abstract class BaseActivityHook extends Activity {
|
public abstract class BaseActivityHook extends Activity {
|
||||||
|
|
||||||
|
private static final int ID_REVANCED_SETTINGS_FRAGMENTS =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
|
||||||
|
private static final int ID_REVANCED_TOOLBAR_PARENT =
|
||||||
|
getResourceIdentifierOrThrow("revanced_toolbar_parent", "id");
|
||||||
|
public static final int LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_with_toolbar", "layout");
|
||||||
|
private static final int STRING_REVANCED_SETTINGS_TITLE =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_title", "string");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Layout parameters for the toolbar, extracted from the dummy toolbar.
|
* Layout parameters for the toolbar, extracted from the dummy toolbar.
|
||||||
*/
|
*/
|
||||||
@@ -55,13 +67,27 @@ public abstract class BaseActivityHook extends Activity {
|
|||||||
|
|
||||||
activity.getFragmentManager()
|
activity.getFragmentManager()
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
.replace(Utils.getResourceIdentifier("revanced_settings_fragments", "id"), fragment)
|
.replace(ID_REVANCED_SETTINGS_FRAGMENTS, fragment)
|
||||||
.commit();
|
.commit();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "initialize failure", ex);
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* Overrides the ReVanced settings language.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public static Context getAttachBaseContext(Context original) {
|
||||||
|
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
||||||
|
if (language == AppLanguage.DEFAULT) {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Utils.getContext();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and configures a toolbar for the activity, replacing a dummy placeholder.
|
* Creates and configures a toolbar for the activity, replacing a dummy placeholder.
|
||||||
*/
|
*/
|
||||||
@@ -69,8 +95,7 @@ public abstract class BaseActivityHook extends Activity {
|
|||||||
protected void createToolbar(Activity activity, PreferenceFragment fragment) {
|
protected void createToolbar(Activity activity, PreferenceFragment fragment) {
|
||||||
// Replace dummy placeholder toolbar.
|
// Replace dummy placeholder toolbar.
|
||||||
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
||||||
ViewGroup toolBarParent = activity.findViewById(
|
ViewGroup toolBarParent = activity.findViewById(ID_REVANCED_TOOLBAR_PARENT);
|
||||||
Utils.getResourceIdentifier("revanced_toolbar_parent", "id"));
|
|
||||||
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar");
|
ViewGroup dummyToolbar = Utils.getChildViewByResourceName(toolBarParent, "revanced_toolbar");
|
||||||
toolbarLayoutParams = dummyToolbar.getLayoutParams();
|
toolbarLayoutParams = dummyToolbar.getLayoutParams();
|
||||||
toolBarParent.removeView(dummyToolbar);
|
toolBarParent.removeView(dummyToolbar);
|
||||||
@@ -82,7 +107,7 @@ public abstract class BaseActivityHook extends Activity {
|
|||||||
toolbar.setBackgroundColor(getToolbarBackgroundColor());
|
toolbar.setBackgroundColor(getToolbarBackgroundColor());
|
||||||
toolbar.setNavigationIcon(getNavigationIcon());
|
toolbar.setNavigationIcon(getNavigationIcon());
|
||||||
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
|
toolbar.setNavigationOnClickListener(getNavigationClickListener(activity));
|
||||||
toolbar.setTitle(Utils.getResourceIdentifier("revanced_settings_title", "string"));
|
toolbar.setTitle(STRING_REVANCED_SETTINGS_TITLE);
|
||||||
|
|
||||||
final int margin = Utils.dipToPixels(16);
|
final int margin = Utils.dipToPixels(16);
|
||||||
toolbar.setTitleMarginStart(margin);
|
toolbar.setTitleMarginStart(margin);
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ public class BaseSettings {
|
|||||||
*/
|
*/
|
||||||
public static final BooleanSetting SHOW_MENU_ICONS = new BooleanSetting("revanced_show_menu_icons", TRUE, true);
|
public static final BooleanSetting SHOW_MENU_ICONS = new BooleanSetting("revanced_show_menu_icons", TRUE, true);
|
||||||
|
|
||||||
|
public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
|
||||||
|
public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "");
|
||||||
|
|
||||||
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true, "revanced_spoof_video_streams_user_dialog_message");
|
||||||
public static final EnumSetting<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
|
public static final EnumSetting<AppLanguage> SPOOF_VIDEO_STREAMS_LANGUAGE = new EnumSetting<>("revanced_spoof_video_streams_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability());
|
||||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(SPOOF_VIDEO_STREAMS));
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
package app.revanced.extension.shared.settings;
|
package app.revanced.extension.shared.settings;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.StringRef;
|
import app.revanced.extension.shared.StringRef;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||||
import org.json.JSONException;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
public abstract class Setting<T> {
|
public abstract class Setting<T> {
|
||||||
|
|
||||||
@@ -23,24 +32,49 @@ public abstract class Setting<T> {
|
|||||||
*/
|
*/
|
||||||
public interface Availability {
|
public interface Availability {
|
||||||
boolean isAvailable();
|
boolean isAvailable();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return parent settings (dependencies) of this availability.
|
||||||
|
*/
|
||||||
|
default List<Setting<?>> getParentSettings() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Availability based on a single parent setting being enabled.
|
* Availability based on a single parent setting being enabled.
|
||||||
*/
|
*/
|
||||||
public static Availability parent(BooleanSetting parent) {
|
public static Availability parent(BooleanSetting parent) {
|
||||||
return parent::get;
|
return new Availability() {
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return parent.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return Collections.singletonList(parent);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Availability based on all parents being enabled.
|
* Availability based on all parents being enabled.
|
||||||
*/
|
*/
|
||||||
public static Availability parentsAll(BooleanSetting... parents) {
|
public static Availability parentsAll(BooleanSetting... parents) {
|
||||||
return () -> {
|
return new Availability() {
|
||||||
for (BooleanSetting parent : parents) {
|
@Override
|
||||||
if (!parent.get()) return false;
|
public boolean isAvailable() {
|
||||||
|
for (BooleanSetting parent : parents) {
|
||||||
|
if (!parent.get()) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return Collections.unmodifiableList(Arrays.asList(parents));
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,11 +82,19 @@ public abstract class Setting<T> {
|
|||||||
* Availability based on any parent being enabled.
|
* Availability based on any parent being enabled.
|
||||||
*/
|
*/
|
||||||
public static Availability parentsAny(BooleanSetting... parents) {
|
public static Availability parentsAny(BooleanSetting... parents) {
|
||||||
return () -> {
|
return new Availability() {
|
||||||
for (BooleanSetting parent : parents) {
|
@Override
|
||||||
if (parent.get()) return true;
|
public boolean isAvailable() {
|
||||||
|
for (BooleanSetting parent : parents) {
|
||||||
|
if (parent.get()) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return Collections.unmodifiableList(Arrays.asList(parents));
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +154,7 @@ public abstract class Setting<T> {
|
|||||||
* @return All settings that have been created, sorted by keys.
|
* @return All settings that have been created, sorted by keys.
|
||||||
*/
|
*/
|
||||||
private static List<Setting<?>> allLoadedSettingsSorted() {
|
private static List<Setting<?>> allLoadedSettingsSorted() {
|
||||||
|
//noinspection ComparatorCombinators
|
||||||
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
|
Collections.sort(SETTINGS, (Setting<?> o1, Setting<?> o2) -> o1.key.compareTo(o2.key));
|
||||||
return allLoadedSettings();
|
return allLoadedSettings();
|
||||||
}
|
}
|
||||||
@@ -207,9 +250,7 @@ public abstract class Setting<T> {
|
|||||||
|
|
||||||
SETTINGS.add(this);
|
SETTINGS.add(this);
|
||||||
if (PATH_TO_SETTINGS.put(key, this) != null) {
|
if (PATH_TO_SETTINGS.put(key, this) != null) {
|
||||||
// Debug setting may not be created yet so using Logger may cause an initialization crash.
|
Logger.printException(() -> this.getClass().getSimpleName()
|
||||||
// Show a toast instead.
|
|
||||||
Utils.showToastLong(this.getClass().getSimpleName()
|
|
||||||
+ " error: Duplicate Setting key found: " + key);
|
+ " error: Duplicate Setting key found: " + key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,10 +272,10 @@ public abstract class Setting<T> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Migrate an old Setting value previously stored in a different SharedPreference.
|
* Migrate an old Setting value previously stored in a different SharedPreference.
|
||||||
*
|
* <p>
|
||||||
* This method will be deleted in the future.
|
* This method will be deleted in the future.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings({"rawtypes", "NewApi"})
|
||||||
public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) {
|
public static void migrateFromOldPreferences(SharedPrefCategory oldPrefs, Setting setting, String settingKey) {
|
||||||
if (!oldPrefs.preferences.contains(settingKey)) {
|
if (!oldPrefs.preferences.contains(settingKey)) {
|
||||||
return; // Nothing to do.
|
return; // Nothing to do.
|
||||||
@@ -254,7 +295,7 @@ public abstract class Setting<T> {
|
|||||||
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
|
migratedValue = oldPrefs.getString(settingKey, (String) newValue);
|
||||||
} else {
|
} else {
|
||||||
Logger.printException(() -> "Unknown setting: " + setting);
|
Logger.printException(() -> "Unknown setting: " + setting);
|
||||||
// Remove otherwise it'll show a toast on every launch
|
// Remove otherwise it'll show a toast on every launch.
|
||||||
oldPrefs.preferences.edit().remove(settingKey).apply();
|
oldPrefs.preferences.edit().remove(settingKey).apply();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -273,7 +314,7 @@ public abstract class Setting<T> {
|
|||||||
/**
|
/**
|
||||||
* Sets, but does _not_ persistently save the value.
|
* Sets, but does _not_ persistently save the value.
|
||||||
* This method is only to be used by the Settings preference code.
|
* This method is only to be used by the Settings preference code.
|
||||||
*
|
* <p>
|
||||||
* This intentionally is a static method to deter
|
* This intentionally is a static method to deter
|
||||||
* accidental usage when {@link #save(Object)} was intended.
|
* accidental usage when {@link #save(Object)} was intended.
|
||||||
*/
|
*/
|
||||||
@@ -349,6 +390,14 @@ public abstract class Setting<T> {
|
|||||||
return availability == null || availability.isAvailable();
|
return availability == null || availability.isAvailable();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the parent Settings that this setting depends on.
|
||||||
|
* @return List of parent Settings (e.g., BooleanSetting or EnumSetting), or empty list if no dependencies exist.
|
||||||
|
*/
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return availability == null ? Collections.emptyList() : availability.getParentSettings();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the currently set value is the same as {@link #defaultValue}
|
* @return if the currently set value is the same as {@link #defaultValue}
|
||||||
*/
|
*/
|
||||||
@@ -467,9 +516,12 @@ public abstract class Setting<T> {
|
|||||||
callback.settingsImported(alertDialogContext);
|
callback.settingsImported(alertDialogContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils.showToastLong(numberOfSettingsImported == 0
|
// Use a delay, otherwise the toast can move about on screen from the dismissing dialog.
|
||||||
? str("revanced_settings_import_reset")
|
final int numberOfSettingsImportedFinal = numberOfSettingsImported;
|
||||||
: str("revanced_settings_import_success", numberOfSettingsImported));
|
Utils.runOnMainThreadDelayed(() -> Utils.showToastLong(numberOfSettingsImportedFinal == 0
|
||||||
|
? str("revanced_settings_import_reset")
|
||||||
|
: str("revanced_settings_import_success", numberOfSettingsImportedFinal)),
|
||||||
|
150);
|
||||||
|
|
||||||
return rebootSettingChanged;
|
return rebootSettingChanged;
|
||||||
} catch (JSONException | IllegalArgumentException ex) {
|
} catch (JSONException | IllegalArgumentException ex) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import app.revanced.extension.shared.Utils;
|
|||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||||
@@ -124,7 +125,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
|
|
||||||
showingUserDialogMessage = true;
|
showingUserDialogMessage = true;
|
||||||
|
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
confirmDialogTitle, // Title.
|
confirmDialogTitle, // Title.
|
||||||
Objects.requireNonNull(setting.userDialogMessage).toString(), // No message.
|
Objects.requireNonNull(setting.userDialogMessage).toString(), // No message.
|
||||||
@@ -248,7 +249,8 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
Setting.privateSetValueFromString(setting, listPref.getValue());
|
Setting.privateSetValueFromString(setting, listPref.getValue());
|
||||||
}
|
}
|
||||||
updateListPreferenceSummary(listPref, setting);
|
updateListPreferenceSummary(listPref, setting);
|
||||||
} else {
|
} else if (!pref.getClass().equals(Preference.class)) {
|
||||||
|
// Ignore root preference class because there is no data to sync.
|
||||||
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
|
Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,7 +304,8 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
restartDialogButtonText = str("revanced_settings_restart");
|
restartDialogButtonText = str("revanced_settings_restart");
|
||||||
}
|
}
|
||||||
|
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(context,
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
|
context,
|
||||||
restartDialogTitle, // Title.
|
restartDialogTitle, // Title.
|
||||||
restartDialogMessage, // Message.
|
restartDialogMessage, // Message.
|
||||||
null, // No EditText.
|
null, // No EditText.
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.SpannedString;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.BulletSpan;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the summary text bullet points into Spanned text for better presentation.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class BulletPointPreference extends Preference {
|
||||||
|
|
||||||
|
public static SpannedString formatIntoBulletPoints(CharSequence source) {
|
||||||
|
SpannableStringBuilder builder = new SpannableStringBuilder(source);
|
||||||
|
|
||||||
|
int lineStart = 0;
|
||||||
|
int length = builder.length();
|
||||||
|
|
||||||
|
while (lineStart < length) {
|
||||||
|
int lineEnd = TextUtils.indexOf(builder, '\n', lineStart);
|
||||||
|
if (lineEnd < 0) lineEnd = length;
|
||||||
|
|
||||||
|
// Apply BulletSpan only if the line starts with the '•' character.
|
||||||
|
if (lineEnd > lineStart && builder.charAt(lineStart) == '•') {
|
||||||
|
int deleteEnd = lineStart + 1; // remove the bullet itself
|
||||||
|
|
||||||
|
// If there's a single space right after the bullet, remove that too.
|
||||||
|
if (deleteEnd < builder.length() && builder.charAt(deleteEnd) == ' ') {
|
||||||
|
deleteEnd++;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.delete(lineStart, deleteEnd);
|
||||||
|
|
||||||
|
// Apply the BulletSpan to the remainder of that line.
|
||||||
|
builder.setSpan(new BulletSpan(20),
|
||||||
|
lineStart,
|
||||||
|
lineEnd - (deleteEnd - lineStart), // adjust for deleted chars.
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update total length and lineEnd after deletion.
|
||||||
|
length = builder.length();
|
||||||
|
final int removed = deleteEnd - lineStart;
|
||||||
|
lineEnd -= removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStart = lineEnd + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new SpannedString(builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSummary(CharSequence summary) {
|
||||||
|
super.setSummary(formatIntoBulletPoints(summary));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.settings.preference.BulletPointPreference.formatIntoBulletPoints;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.SwitchPreference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the summary text bullet points into Spanned text for better presentation.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class BulletPointSwitchPreference extends SwitchPreference {
|
||||||
|
|
||||||
|
public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointSwitchPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public BulletPointSwitchPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSummary(CharSequence summary) {
|
||||||
|
super.setSummary(formatIntoBulletPoints(summary));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSummaryOn(CharSequence summaryOn) {
|
||||||
|
super.setSummaryOn(formatIntoBulletPoints(summaryOn));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSummaryOff(CharSequence summaryOff) {
|
||||||
|
super.setSummaryOff(formatIntoBulletPoints(summaryOff));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -13,20 +13,20 @@ import android.os.Bundle;
|
|||||||
import android.preference.EditTextPreference;
|
import android.preference.EditTextPreference;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.text.style.ForegroundColorSpan;
|
|
||||||
import android.text.style.RelativeSizeSpan;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
import android.view.Gravity;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewParent;
|
import android.view.ViewParent;
|
||||||
import android.widget.*;
|
import android.widget.EditText;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
@@ -35,6 +35,8 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.settings.StringSetting;
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
|
import app.revanced.extension.shared.ui.ColorDot;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom preference for selecting a color via a hexadecimal code or a color picker dialog.
|
* A custom preference for selecting a color via a hexadecimal code or a color picker dialog.
|
||||||
@@ -43,100 +45,98 @@ import app.revanced.extension.shared.settings.StringSetting;
|
|||||||
*/
|
*/
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ColorPickerPreference extends EditTextPreference {
|
public class ColorPickerPreference extends EditTextPreference {
|
||||||
|
/** Length of a valid color string of format #RRGGBB (without alpha) or #AARRGGBB (with alpha). */
|
||||||
|
public static final int COLOR_STRING_LENGTH_WITHOUT_ALPHA = 7;
|
||||||
|
public static final int COLOR_STRING_LENGTH_WITH_ALPHA = 9;
|
||||||
|
|
||||||
/**
|
/** Matches everything that is not a hex number/letter. */
|
||||||
* Character to show the color appearance.
|
|
||||||
*/
|
|
||||||
public static final String COLOR_DOT_STRING = "⬤";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Length of a valid color string of format #RRGGBB.
|
|
||||||
*/
|
|
||||||
public static final int COLOR_STRING_LENGTH = 7;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matches everything that is not a hex number/letter.
|
|
||||||
*/
|
|
||||||
private static final Pattern PATTERN_NOT_HEX = Pattern.compile("[^0-9A-Fa-f]");
|
private static final Pattern PATTERN_NOT_HEX = Pattern.compile("[^0-9A-Fa-f]");
|
||||||
|
|
||||||
/**
|
/** Alpha for dimming when the preference is disabled. */
|
||||||
* Alpha for dimming when the preference is disabled.
|
public static final float DISABLED_ALPHA = 0.5f; // 50%
|
||||||
*/
|
|
||||||
private static final float DISABLED_ALPHA = 0.5f; // 50%
|
|
||||||
|
|
||||||
/**
|
/** View displaying a colored dot in the widget area. */
|
||||||
* View displaying a colored dot in the widget area.
|
|
||||||
*/
|
|
||||||
private View widgetColorDot;
|
private View widgetColorDot;
|
||||||
|
|
||||||
/**
|
/** Dialog View displaying a colored dot for the selected color preview in the dialog. */
|
||||||
* Current color in RGB format (without alpha).
|
private View dialogColorDot;
|
||||||
*/
|
|
||||||
|
/** Current color, including alpha channel if opacity slider is enabled. */
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private int currentColor;
|
private int currentColor;
|
||||||
|
|
||||||
/**
|
/** Associated setting for storing the color value. */
|
||||||
* Associated setting for storing the color value.
|
|
||||||
*/
|
|
||||||
private StringSetting colorSetting;
|
private StringSetting colorSetting;
|
||||||
|
|
||||||
/**
|
/** Dialog TextWatcher for the EditText to monitor color input changes. */
|
||||||
* Dialog TextWatcher for the EditText to monitor color input changes.
|
|
||||||
*/
|
|
||||||
private TextWatcher colorTextWatcher;
|
private TextWatcher colorTextWatcher;
|
||||||
|
|
||||||
/**
|
/** Dialog color picker view. */
|
||||||
* Dialog TextView displaying a colored dot for the selected color preview in the dialog.
|
protected ColorPickerView dialogColorPickerView;
|
||||||
*/
|
|
||||||
private TextView dialogColorPreview;
|
|
||||||
|
|
||||||
/**
|
/** Listener for color changes. */
|
||||||
* Dialog color picker view.
|
protected OnColorChangeListener colorChangeListener;
|
||||||
*/
|
|
||||||
private ColorPickerView dialogColorPickerView;
|
/** Whether the opacity slider is enabled. */
|
||||||
|
private boolean opacitySliderEnabled = false;
|
||||||
|
|
||||||
|
public static final int ID_REVANCED_COLOR_PICKER_VIEW =
|
||||||
|
getResourceIdentifierOrThrow("revanced_color_picker_view", "id");
|
||||||
|
public static final int ID_PREFERENCE_COLOR_DOT =
|
||||||
|
getResourceIdentifierOrThrow("preference_color_dot", "id");
|
||||||
|
public static final int LAYOUT_REVANCED_COLOR_DOT_WIDGET =
|
||||||
|
getResourceIdentifierOrThrow("revanced_color_dot_widget", "layout");
|
||||||
|
public static final int LAYOUT_REVANCED_COLOR_PICKER =
|
||||||
|
getResourceIdentifierOrThrow("revanced_color_picker", "layout");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes non valid hex characters, converts to all uppercase,
|
* Removes non valid hex characters, converts to all uppercase,
|
||||||
* and adds # character to the start if not present.
|
* and adds # character to the start if not present.
|
||||||
*/
|
*/
|
||||||
public static String cleanupColorCodeString(String colorString) {
|
public static String cleanupColorCodeString(String colorString, boolean includeAlpha) {
|
||||||
// Remove non-hex chars, convert to uppercase, and ensure correct length
|
|
||||||
String result = "#" + PATTERN_NOT_HEX.matcher(colorString)
|
String result = "#" + PATTERN_NOT_HEX.matcher(colorString)
|
||||||
.replaceAll("").toUpperCase(Locale.ROOT);
|
.replaceAll("").toUpperCase(Locale.ROOT);
|
||||||
|
|
||||||
if (result.length() < COLOR_STRING_LENGTH) {
|
int maxLength = includeAlpha ? COLOR_STRING_LENGTH_WITH_ALPHA : COLOR_STRING_LENGTH_WITHOUT_ALPHA;
|
||||||
|
if (result.length() < maxLength) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.substring(0, COLOR_STRING_LENGTH);
|
return result.substring(0, maxLength);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param color RGB color, without an alpha channel.
|
* @param color Color, with or without alpha channel.
|
||||||
* @return #RRGGBB hex color string
|
* @param includeAlpha Whether to include the alpha channel in the output string.
|
||||||
|
* @return #RRGGBB or #AARRGGBB hex color string
|
||||||
*/
|
*/
|
||||||
public static String getColorString(@ColorInt int color) {
|
public static String getColorString(@ColorInt int color, boolean includeAlpha) {
|
||||||
String colorString = String.format("#%06X", color);
|
if (includeAlpha) {
|
||||||
if ((color & 0xFF000000) != 0) {
|
return String.format("#%08X", color);
|
||||||
// Likely a bug somewhere.
|
|
||||||
Logger.printException(() -> "getColorString: color has alpha channel: " + colorString);
|
|
||||||
}
|
}
|
||||||
return colorString;
|
color = color & 0x00FFFFFF; // Mask to strip alpha.
|
||||||
|
return String.format("#%06X", color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Spanned object for a colored dot using SpannableString.
|
* Interface for notifying color changes.
|
||||||
*
|
|
||||||
* @param color The RGB color (without alpha).
|
|
||||||
* @return A Spanned object with the colored dot.
|
|
||||||
*/
|
*/
|
||||||
public static Spanned getColorDot(@ColorInt int color) {
|
public interface OnColorChangeListener {
|
||||||
SpannableString spannable = new SpannableString(COLOR_DOT_STRING);
|
void onColorChanged(String key, int newColor);
|
||||||
spannable.setSpan(new ForegroundColorSpan(color | 0xFF000000), 0, COLOR_DOT_STRING.length(),
|
}
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
spannable.setSpan(new RelativeSizeSpan(1.5f), 0, 1,
|
/**
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
* Sets the listener for color changes.
|
||||||
return spannable;
|
*/
|
||||||
|
public void setOnColorChangeListener(OnColorChangeListener listener) {
|
||||||
|
this.colorChangeListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables the opacity slider in the color picker dialog.
|
||||||
|
*/
|
||||||
|
public void setOpacitySliderEnabled(boolean enabled) {
|
||||||
|
this.opacitySliderEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ColorPickerPreference(Context context) {
|
public ColorPickerPreference(Context context) {
|
||||||
@@ -158,9 +158,13 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
* Initializes the preference by setting up the EditText, loading the color, and set the widget layout.
|
* Initializes the preference by setting up the EditText, loading the color, and set the widget layout.
|
||||||
*/
|
*/
|
||||||
private void init() {
|
private void init() {
|
||||||
colorSetting = (StringSetting) Setting.getSettingFromPath(getKey());
|
if (getKey() != null) {
|
||||||
if (colorSetting == null) {
|
colorSetting = (StringSetting) Setting.getSettingFromPath(getKey());
|
||||||
Logger.printException(() -> "Could not find color setting for: " + getKey());
|
if (colorSetting == null) {
|
||||||
|
Logger.printException(() -> "Could not find color setting for: " + getKey());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.printDebug(() -> "initialized without key, settings will be loaded later");
|
||||||
}
|
}
|
||||||
|
|
||||||
EditText editText = getEditText();
|
EditText editText = getEditText();
|
||||||
@@ -171,27 +175,29 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Set the widget layout to a custom layout containing the colored dot.
|
// Set the widget layout to a custom layout containing the colored dot.
|
||||||
setWidgetLayoutResource(getResourceIdentifier("revanced_color_dot_widget", "layout"));
|
setWidgetLayoutResource(LAYOUT_REVANCED_COLOR_DOT_WIDGET);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the selected color and updates the UI and settings.
|
* Sets the selected color and updates the UI and settings.
|
||||||
*
|
|
||||||
* @param colorString The color in hexadecimal format (e.g., "#RRGGBB").
|
|
||||||
* @throws IllegalArgumentException If the color string is invalid.
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public final void setText(String colorString) {
|
public void setText(String colorString) {
|
||||||
try {
|
try {
|
||||||
Logger.printDebug(() -> "setText: " + colorString);
|
Logger.printDebug(() -> "setText: " + colorString);
|
||||||
super.setText(colorString);
|
super.setText(colorString);
|
||||||
|
|
||||||
currentColor = Color.parseColor(colorString) & 0x00FFFFFF;
|
currentColor = Color.parseColor(colorString);
|
||||||
if (colorSetting != null) {
|
if (colorSetting != null) {
|
||||||
colorSetting.save(getColorString(currentColor));
|
colorSetting.save(getColorString(currentColor, opacitySliderEnabled));
|
||||||
}
|
}
|
||||||
updateColorPreview();
|
updateDialogColorDot();
|
||||||
updateWidgetColorDot();
|
updateWidgetColorDot();
|
||||||
|
|
||||||
|
// Notify the listener about the color change.
|
||||||
|
if (colorChangeListener != null) {
|
||||||
|
colorChangeListener.onColorChanged(getKey(), currentColor);
|
||||||
|
}
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
// This code is reached if the user pastes settings json with an invalid color
|
// This code is reached if the user pastes settings json with an invalid color
|
||||||
// since this preference is updated with the new setting text.
|
// since this preference is updated with the new setting text.
|
||||||
@@ -203,38 +209,8 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onBindView(View view) {
|
|
||||||
super.onBindView(view);
|
|
||||||
|
|
||||||
widgetColorDot = view.findViewById(getResourceIdentifier(
|
|
||||||
"revanced_color_dot_widget", "id"));
|
|
||||||
widgetColorDot.setBackgroundResource(getResourceIdentifier(
|
|
||||||
"revanced_settings_circle_background", "drawable"));
|
|
||||||
widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
|
|
||||||
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the color preview TextView with a colored dot.
|
|
||||||
*/
|
|
||||||
private void updateColorPreview() {
|
|
||||||
if (dialogColorPreview != null) {
|
|
||||||
dialogColorPreview.setText(getColorDot(currentColor));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateWidgetColorDot() {
|
|
||||||
if (widgetColorDot != null) {
|
|
||||||
widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
|
|
||||||
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a TextWatcher to monitor changes in the EditText for color input.
|
* Creates a TextWatcher to monitor changes in the EditText for color input.
|
||||||
*
|
|
||||||
* @return A TextWatcher that updates the color preview on valid input.
|
|
||||||
*/
|
*/
|
||||||
private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) {
|
private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) {
|
||||||
return new TextWatcher() {
|
return new TextWatcher() {
|
||||||
@@ -250,15 +226,16 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
public void afterTextChanged(Editable edit) {
|
public void afterTextChanged(Editable edit) {
|
||||||
try {
|
try {
|
||||||
String colorString = edit.toString();
|
String colorString = edit.toString();
|
||||||
|
String sanitizedColorString = cleanupColorCodeString(colorString, opacitySliderEnabled);
|
||||||
String sanitizedColorString = cleanupColorCodeString(colorString);
|
|
||||||
if (!sanitizedColorString.equals(colorString)) {
|
if (!sanitizedColorString.equals(colorString)) {
|
||||||
edit.replace(0, colorString.length(), sanitizedColorString);
|
edit.replace(0, colorString.length(), sanitizedColorString);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sanitizedColorString.length() != COLOR_STRING_LENGTH) {
|
int expectedLength = opacitySliderEnabled
|
||||||
// User is still typing out the color.
|
? COLOR_STRING_LENGTH_WITH_ALPHA
|
||||||
|
: COLOR_STRING_LENGTH_WITHOUT_ALPHA;
|
||||||
|
if (sanitizedColorString.length() != expectedLength) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +243,7 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
if (currentColor != newColor) {
|
if (currentColor != newColor) {
|
||||||
Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString);
|
Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString);
|
||||||
currentColor = newColor;
|
currentColor = newColor;
|
||||||
updateColorPreview();
|
updateDialogColorDot();
|
||||||
updateWidgetColorDot();
|
updateWidgetColorDot();
|
||||||
colorPickerView.setColor(newColor);
|
colorPickerView.setColor(newColor);
|
||||||
}
|
}
|
||||||
@@ -279,32 +256,68 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a Dialog with a color preview and EditText for hex color input.
|
* Hook for subclasses to add a custom view to the top of the dialog.
|
||||||
*/
|
*/
|
||||||
|
@Nullable
|
||||||
|
protected View createExtraDialogContentView(Context context) {
|
||||||
|
return null; // Default implementation returns no extra view.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for subclasses to handle the OK button click.
|
||||||
|
*/
|
||||||
|
protected void onDialogOkClicked() {
|
||||||
|
// Default implementation does nothing.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook for subclasses to handle the Neutral button click.
|
||||||
|
*/
|
||||||
|
protected void onDialogNeutralClicked() {
|
||||||
|
// Default implementation.
|
||||||
|
try {
|
||||||
|
final int defaultColor = Color.parseColor(colorSetting.defaultValue);
|
||||||
|
dialogColorPickerView.setColor(defaultColor);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "Reset button failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void showDialog(Bundle state) {
|
protected void showDialog(Bundle state) {
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
|
|
||||||
|
// Create content container for all dialog views.
|
||||||
|
LinearLayout contentContainer = new LinearLayout(context);
|
||||||
|
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
// Add extra view from subclass if it exists.
|
||||||
|
View extraView = createExtraDialogContentView(context);
|
||||||
|
if (extraView != null) {
|
||||||
|
contentContainer.addView(extraView);
|
||||||
|
}
|
||||||
|
|
||||||
// Inflate color picker view.
|
// Inflate color picker view.
|
||||||
View colorPicker = LayoutInflater.from(context).inflate(
|
View colorPicker = LayoutInflater.from(context).inflate(LAYOUT_REVANCED_COLOR_PICKER, null);
|
||||||
getResourceIdentifier("revanced_color_picker", "layout"), null);
|
dialogColorPickerView = colorPicker.findViewById(ID_REVANCED_COLOR_PICKER_VIEW);
|
||||||
dialogColorPickerView = colorPicker.findViewById(
|
dialogColorPickerView.setOpacitySliderEnabled(opacitySliderEnabled);
|
||||||
getResourceIdentifier("revanced_color_picker_view", "id"));
|
|
||||||
dialogColorPickerView.setColor(currentColor);
|
dialogColorPickerView.setColor(currentColor);
|
||||||
|
contentContainer.addView(colorPicker);
|
||||||
|
|
||||||
// Horizontal layout for preview and EditText.
|
// Horizontal layout for preview and EditText.
|
||||||
LinearLayout inputLayout = new LinearLayout(context);
|
LinearLayout inputLayout = new LinearLayout(context);
|
||||||
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
|
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
inputLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
dialogColorPreview = new TextView(context);
|
dialogColorDot = new View(context);
|
||||||
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
dipToPixels(20),
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
dipToPixels(20)
|
||||||
);
|
);
|
||||||
previewParams.setMargins(dipToPixels(15), 0, dipToPixels(10), 0); // text dot has its own indents so 15, instead 16.
|
previewParams.setMargins(dipToPixels(16), 0, dipToPixels(10), 0);
|
||||||
dialogColorPreview.setLayoutParams(previewParams);
|
dialogColorDot.setLayoutParams(previewParams);
|
||||||
inputLayout.addView(dialogColorPreview);
|
inputLayout.addView(dialogColorDot);
|
||||||
updateColorPreview();
|
updateDialogColorDot();
|
||||||
|
|
||||||
EditText editText = getEditText();
|
EditText editText = getEditText();
|
||||||
ViewParent parent = editText.getParent();
|
ViewParent parent = editText.getParent();
|
||||||
@@ -315,7 +328,7 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
));
|
));
|
||||||
String currentColorString = getColorString(currentColor);
|
String currentColorString = getColorString(currentColor, opacitySliderEnabled);
|
||||||
editText.setText(currentColorString);
|
editText.setText(currentColorString);
|
||||||
editText.setSelection(currentColorString.length());
|
editText.setSelection(currentColorString.length());
|
||||||
editText.setTypeface(Typeface.MONOSPACE);
|
editText.setTypeface(Typeface.MONOSPACE);
|
||||||
@@ -334,16 +347,12 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
paddingView.setLayoutParams(params);
|
paddingView.setLayoutParams(params);
|
||||||
inputLayout.addView(paddingView);
|
inputLayout.addView(paddingView);
|
||||||
|
|
||||||
// Create content container for color picker and input layout.
|
|
||||||
LinearLayout contentContainer = new LinearLayout(context);
|
|
||||||
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
contentContainer.addView(colorPicker);
|
|
||||||
contentContainer.addView(inputLayout);
|
contentContainer.addView(inputLayout);
|
||||||
|
|
||||||
// Create ScrollView to wrap the content container.
|
// Create ScrollView to wrap the content container.
|
||||||
ScrollView contentScrollView = new ScrollView(context);
|
ScrollView contentScrollView = new ScrollView(context);
|
||||||
contentScrollView.setVerticalScrollBarEnabled(false); // Disable vertical scrollbar.
|
contentScrollView.setVerticalScrollBarEnabled(false);
|
||||||
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); // Disable overscroll effect.
|
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
0,
|
0,
|
||||||
@@ -352,51 +361,43 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
contentScrollView.setLayoutParams(scrollViewParams);
|
contentScrollView.setLayoutParams(scrollViewParams);
|
||||||
contentScrollView.addView(contentContainer);
|
contentScrollView.addView(contentContainer);
|
||||||
|
|
||||||
// Create custom dialog.
|
final int originalColor = currentColor;
|
||||||
final int originalColor = currentColor & 0x00FFFFFF;
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
|
||||||
context,
|
context,
|
||||||
getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"), // Title.
|
getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"),
|
||||||
null, // No message.
|
null,
|
||||||
null, // No EditText.
|
null,
|
||||||
null, // OK button text.
|
null,
|
||||||
() -> {
|
() -> { // OK button action.
|
||||||
// OK button action.
|
|
||||||
try {
|
try {
|
||||||
String colorString = editText.getText().toString();
|
String colorString = editText.getText().toString();
|
||||||
if (colorString.length() != COLOR_STRING_LENGTH) {
|
int expectedLength = opacitySliderEnabled
|
||||||
|
? COLOR_STRING_LENGTH_WITH_ALPHA
|
||||||
|
: COLOR_STRING_LENGTH_WITHOUT_ALPHA;
|
||||||
|
if (colorString.length() != expectedLength) {
|
||||||
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||||
setText(getColorString(originalColor));
|
setText(getColorString(originalColor, opacitySliderEnabled));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setText(colorString);
|
setText(colorString);
|
||||||
|
|
||||||
|
onDialogOkClicked();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
// Should never happen due to a bad color string,
|
// Should never happen due to a bad color string,
|
||||||
// since the text is validated and fixed while the user types.
|
// since the text is validated and fixed while the user types.
|
||||||
Logger.printException(() -> "OK button failure", ex);
|
Logger.printException(() -> "OK button failure", ex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() -> {
|
() -> { // Cancel button action.
|
||||||
// Cancel button action.
|
|
||||||
try {
|
try {
|
||||||
// Restore the original color.
|
setText(getColorString(originalColor, opacitySliderEnabled));
|
||||||
setText(getColorString(originalColor));
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Cancel button failure", ex);
|
Logger.printException(() -> "Cancel button failure", ex);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
str("revanced_settings_reset_color"), // Neutral button text.
|
str("revanced_settings_reset_color"), // Neutral button text.
|
||||||
() -> {
|
this::onDialogNeutralClicked, // Neutral button action.
|
||||||
// Neutral button action.
|
false // Do not dismiss dialog.
|
||||||
try {
|
|
||||||
final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
|
|
||||||
// Setting view color causes listener callback into this class.
|
|
||||||
dialogColorPickerView.setColor(defaultColor);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "Reset button failure", ex);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
false // Do not dismiss dialog when onNeutralClick.
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add the ScrollView to the dialog's main layout.
|
// Add the ScrollView to the dialog's main layout.
|
||||||
@@ -412,13 +413,13 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String updatedColorString = getColorString(color);
|
String updatedColorString = getColorString(color, opacitySliderEnabled);
|
||||||
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
|
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
|
||||||
currentColor = color;
|
currentColor = color;
|
||||||
editText.setText(updatedColorString);
|
editText.setText(updatedColorString);
|
||||||
editText.setSelection(updatedColorString.length());
|
editText.setSelection(updatedColorString.length());
|
||||||
|
|
||||||
updateColorPreview();
|
updateDialogColorDot();
|
||||||
updateWidgetColorDot();
|
updateWidgetColorDot();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -437,7 +438,7 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
colorTextWatcher = null;
|
colorTextWatcher = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogColorPreview = null;
|
dialogColorDot = null;
|
||||||
dialogColorPickerView = null;
|
dialogColorPickerView = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,4 +447,32 @@ public class ColorPickerPreference extends EditTextPreference {
|
|||||||
super.setEnabled(enabled);
|
super.setEnabled(enabled);
|
||||||
updateWidgetColorDot();
|
updateWidgetColorDot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBindView(View view) {
|
||||||
|
super.onBindView(view);
|
||||||
|
|
||||||
|
widgetColorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT);
|
||||||
|
updateWidgetColorDot();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWidgetColorDot() {
|
||||||
|
if (widgetColorDot == null) return;
|
||||||
|
|
||||||
|
ColorDot.applyColorDot(
|
||||||
|
widgetColorDot,
|
||||||
|
currentColor,
|
||||||
|
widgetColorDot.isEnabled()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDialogColorDot() {
|
||||||
|
if (dialogColorDot == null) return;
|
||||||
|
|
||||||
|
ColorDot.applyColorDot(
|
||||||
|
dialogColorDot,
|
||||||
|
currentColor,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,57 +23,73 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom color picker view that allows the user to select a color using a hue slider and a saturation-value selector.
|
* A custom color picker view that allows the user to select a color using a hue slider, a saturation-value selector
|
||||||
|
* and an optional opacity slider.
|
||||||
* This implementation is density-independent and responsive across different screen sizes and DPIs.
|
* This implementation is density-independent and responsive across different screen sizes and DPIs.
|
||||||
*
|
|
||||||
* <p>
|
* <p>
|
||||||
* This view displays two main components for color selection:
|
* This view displays three main components for color selection:
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li><b>Hue Bar:</b> A horizontal bar at the bottom that allows the user to select the hue component of the color.
|
* <li><b>Hue Bar:</b> A horizontal bar at the bottom that allows the user to select the hue component of the color.
|
||||||
* <li><b>Saturation-Value Selector:</b> A rectangular area above the hue bar that allows the user to select the saturation and value (brightness)
|
* <li><b>Saturation-Value Selector:</b> A rectangular area above the hue bar that allows the user to select the
|
||||||
* components of the color based on the selected hue.
|
* saturation and value (brightness) components of the color based on the selected hue.
|
||||||
|
* <li><b>Opacity Slider:</b> An optional horizontal bar below the hue bar that allows the user to adjust
|
||||||
|
* the opacity (alpha channel) of the color.
|
||||||
* </ul>
|
* </ul>
|
||||||
*
|
|
||||||
* <p>
|
* <p>
|
||||||
* The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar and the
|
* The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar,
|
||||||
* saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles).
|
* opacity slider, and the saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles).
|
||||||
*
|
|
||||||
* <p>
|
* <p>
|
||||||
* The selected color can be retrieved using {@link #getColor()} and can be set using {@link #setColor(int)}.
|
* The selected color can be retrieved using {@link #getColor()} and can be set using {@link #setColor(int)}.
|
||||||
* An {@link OnColorChangedListener} can be registered to receive notifications when the selected color changes.
|
* An {@link OnColorChangedListener} can be registered to receive notifications when the selected color changes.
|
||||||
*/
|
*/
|
||||||
public class ColorPickerView extends View {
|
public class ColorPickerView extends View {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface definition for a callback to be invoked when the selected color changes.
|
* Interface definition for a callback to be invoked when the selected color changes.
|
||||||
*/
|
*/
|
||||||
public interface OnColorChangedListener {
|
public interface OnColorChangedListener {
|
||||||
/**
|
/**
|
||||||
* Called when the selected color has changed.
|
* Called when the selected color has changed.
|
||||||
*
|
|
||||||
* Important: Callback color uses RGB format with zero alpha channel.
|
|
||||||
*
|
|
||||||
* @param color The new selected color.
|
|
||||||
*/
|
*/
|
||||||
void onColorChanged(@ColorInt int color);
|
void onColorChanged(@ColorInt int color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Expanded touch area for the hue bar to increase the touch-sensitive area. */
|
/** Expanded touch area for the hue and opacity bars to increase the touch-sensitive area. */
|
||||||
public static final float TOUCH_EXPANSION = dipToPixels(20f);
|
public static final float TOUCH_EXPANSION = dipToPixels(20f);
|
||||||
|
|
||||||
|
/** Margin between different areas of the view (saturation-value selector, hue bar, and opacity slider). */
|
||||||
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
|
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
|
||||||
|
|
||||||
|
/** Padding around the view. */
|
||||||
private static final float VIEW_PADDING = dipToPixels(16);
|
private static final float VIEW_PADDING = dipToPixels(16);
|
||||||
|
|
||||||
|
/** Height of the hue bar. */
|
||||||
private static final float HUE_BAR_HEIGHT = dipToPixels(12);
|
private static final float HUE_BAR_HEIGHT = dipToPixels(12);
|
||||||
|
|
||||||
|
/** Height of the opacity slider. */
|
||||||
|
private static final float OPACITY_BAR_HEIGHT = dipToPixels(12);
|
||||||
|
|
||||||
|
/** Corner radius for the hue bar. */
|
||||||
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
|
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
|
||||||
|
|
||||||
|
/** Corner radius for the opacity slider. */
|
||||||
|
private static final float OPACITY_CORNER_RADIUS = dipToPixels(6);
|
||||||
|
|
||||||
|
/** Radius of the selector handles. */
|
||||||
private static final float SELECTOR_RADIUS = dipToPixels(12);
|
private static final float SELECTOR_RADIUS = dipToPixels(12);
|
||||||
|
|
||||||
|
/** Stroke width for the selector handle outlines. */
|
||||||
private static final float SELECTOR_STROKE_WIDTH = 8;
|
private static final float SELECTOR_STROKE_WIDTH = 8;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hue fill radius. Use slightly smaller radius for the selector handle fill,
|
* Hue and opacity fill radius. Use slightly smaller radius for the selector handle fill,
|
||||||
* otherwise the anti-aliasing causes the fill color to bleed past the selector outline.
|
* otherwise the anti-aliasing causes the fill color to bleed past the selector outline.
|
||||||
*/
|
*/
|
||||||
private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2;
|
private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2;
|
||||||
|
|
||||||
/** Thin dark outline stroke width for the selector rings. */
|
/** Thin dark outline stroke width for the selector rings. */
|
||||||
private static final float SELECTOR_EDGE_STROKE_WIDTH = 1;
|
private static final float SELECTOR_EDGE_STROKE_WIDTH = 1;
|
||||||
|
|
||||||
|
/** Radius for the outer edge of the selector rings, including stroke width. */
|
||||||
public static final float SELECTOR_EDGE_RADIUS =
|
public static final float SELECTOR_EDGE_RADIUS =
|
||||||
SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2;
|
SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2;
|
||||||
|
|
||||||
@@ -85,6 +101,7 @@ public class ColorPickerView extends View {
|
|||||||
@ColorInt
|
@ColorInt
|
||||||
private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF");
|
private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF");
|
||||||
|
|
||||||
|
/** Precomputed array of hue colors for the hue bar (0-360 degrees). */
|
||||||
private static final int[] HUE_COLORS = new int[361];
|
private static final int[] HUE_COLORS = new int[361];
|
||||||
static {
|
static {
|
||||||
for (int i = 0; i < 361; i++) {
|
for (int i = 0; i < 361; i++) {
|
||||||
@@ -92,11 +109,16 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hue bar. */
|
/** Paint for the hue bar. */
|
||||||
private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
/** Saturation-value selector. */
|
|
||||||
|
/** Paint for the opacity slider. */
|
||||||
|
private final Paint opacityPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
|
/** Paint for the saturation-value selector. */
|
||||||
private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
/** Draggable selector. */
|
|
||||||
|
/** Paint for the draggable selector handles. */
|
||||||
private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
{
|
{
|
||||||
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
||||||
@@ -104,6 +126,10 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
/** Bounds of the hue bar. */
|
/** Bounds of the hue bar. */
|
||||||
private final RectF hueRect = new RectF();
|
private final RectF hueRect = new RectF();
|
||||||
|
|
||||||
|
/** Bounds of the opacity slider. */
|
||||||
|
private final RectF opacityRect = new RectF();
|
||||||
|
|
||||||
/** Bounds of the saturation-value selector. */
|
/** Bounds of the saturation-value selector. */
|
||||||
private final RectF saturationValueRect = new RectF();
|
private final RectF saturationValueRect = new RectF();
|
||||||
|
|
||||||
@@ -112,21 +138,35 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
/** Current hue value (0-360). */
|
/** Current hue value (0-360). */
|
||||||
private float hue = 0f;
|
private float hue = 0f;
|
||||||
|
|
||||||
/** Current saturation value (0-1). */
|
/** Current saturation value (0-1). */
|
||||||
private float saturation = 1f;
|
private float saturation = 1f;
|
||||||
|
|
||||||
/** Current value (brightness) value (0-1). */
|
/** Current value (brightness) value (0-1). */
|
||||||
private float value = 1f;
|
private float value = 1f;
|
||||||
|
|
||||||
/** The currently selected color in RGB format with no alpha channel. */
|
/** Current opacity value (0-1). */
|
||||||
|
private float opacity = 1f;
|
||||||
|
|
||||||
|
/** The currently selected color, including alpha channel if opacity slider is enabled. */
|
||||||
@ColorInt
|
@ColorInt
|
||||||
private int selectedColor;
|
private int selectedColor;
|
||||||
|
|
||||||
|
/** Listener for color change events. */
|
||||||
private OnColorChangedListener colorChangedListener;
|
private OnColorChangedListener colorChangedListener;
|
||||||
|
|
||||||
/** Track if we're currently dragging the hue or saturation handle. */
|
/** Tracks if the hue selector is being dragged. */
|
||||||
private boolean isDraggingHue;
|
private boolean isDraggingHue;
|
||||||
|
|
||||||
|
/** Tracks if the saturation-value selector is being dragged. */
|
||||||
private boolean isDraggingSaturation;
|
private boolean isDraggingSaturation;
|
||||||
|
|
||||||
|
/** Tracks if the opacity selector is being dragged. */
|
||||||
|
private boolean isDraggingOpacity;
|
||||||
|
|
||||||
|
/** Flag to enable/disable the opacity slider. */
|
||||||
|
private boolean opacitySliderEnabled = false;
|
||||||
|
|
||||||
public ColorPickerView(Context context) {
|
public ColorPickerView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
@@ -139,12 +179,32 @@ public class ColorPickerView extends View {
|
|||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables the opacity slider.
|
||||||
|
*/
|
||||||
|
public void setOpacitySliderEnabled(boolean enabled) {
|
||||||
|
if (opacitySliderEnabled != enabled) {
|
||||||
|
opacitySliderEnabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
opacity = 1f; // Reset to fully opaque when disabled.
|
||||||
|
updateSelectedColor();
|
||||||
|
}
|
||||||
|
updateOpacityShader();
|
||||||
|
requestLayout(); // Trigger re-measure to account for opacity slider.
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the view, ensuring a consistent aspect ratio and minimum dimensions.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
||||||
|
|
||||||
final int minWidth = Utils.dipToPixels(250);
|
final int minWidth = dipToPixels(250);
|
||||||
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
|
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS)
|
||||||
|
+ (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0);
|
||||||
|
|
||||||
int width = resolveSize(minWidth, widthMeasureSpec);
|
int width = resolveSize(minWidth, widthMeasureSpec);
|
||||||
int height = resolveSize(minHeight, heightMeasureSpec);
|
int height = resolveSize(minHeight, heightMeasureSpec);
|
||||||
@@ -154,7 +214,8 @@ public class ColorPickerView extends View {
|
|||||||
height = Math.max(height, minHeight);
|
height = Math.max(height, minHeight);
|
||||||
|
|
||||||
// Adjust height to maintain desired aspect ratio if possible.
|
// Adjust height to maintain desired aspect ratio if possible.
|
||||||
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
|
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS)
|
||||||
|
+ (opacitySliderEnabled ? (int) (OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS) : 0);
|
||||||
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
|
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
|
||||||
height = desiredHeight;
|
height = desiredHeight;
|
||||||
}
|
}
|
||||||
@@ -163,17 +224,16 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the size of the view changes.
|
* Updates the view's layout when its size changes, recalculating bounds and shaders.
|
||||||
* This method calculates and sets the bounds of the hue bar and saturation-value selector.
|
|
||||||
* It also creates the necessary shaders for the gradients.
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
||||||
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
||||||
|
|
||||||
// Calculate bounds with hue bar at the bottom.
|
// Calculate bounds with hue bar and optional opacity bar at the bottom.
|
||||||
final float effectiveWidth = width - (2 * VIEW_PADDING);
|
final float effectiveWidth = width - (2 * VIEW_PADDING);
|
||||||
final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS;
|
final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS
|
||||||
|
- (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0);
|
||||||
|
|
||||||
// Adjust rectangles to account for padding and density-independent dimensions.
|
// Adjust rectangles to account for padding and density-independent dimensions.
|
||||||
saturationValueRect.set(
|
saturationValueRect.set(
|
||||||
@@ -185,18 +245,28 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
hueRect.set(
|
hueRect.set(
|
||||||
VIEW_PADDING,
|
VIEW_PADDING,
|
||||||
height - VIEW_PADDING - HUE_BAR_HEIGHT,
|
height - VIEW_PADDING - HUE_BAR_HEIGHT - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0),
|
||||||
VIEW_PADDING + effectiveWidth,
|
VIEW_PADDING + effectiveWidth,
|
||||||
height - VIEW_PADDING
|
height - VIEW_PADDING - (opacitySliderEnabled ? OPACITY_BAR_HEIGHT + MARGIN_BETWEEN_AREAS : 0)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (opacitySliderEnabled) {
|
||||||
|
opacityRect.set(
|
||||||
|
VIEW_PADDING,
|
||||||
|
height - VIEW_PADDING - OPACITY_BAR_HEIGHT,
|
||||||
|
VIEW_PADDING + effectiveWidth,
|
||||||
|
height - VIEW_PADDING
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Update the shaders.
|
// Update the shaders.
|
||||||
updateHueShader();
|
updateHueShader();
|
||||||
updateSaturationValueShader();
|
updateSaturationValueShader();
|
||||||
|
updateOpacityShader();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the hue full spectrum (0-360 degrees).
|
* Updates the shader for the hue bar to reflect the color gradient.
|
||||||
*/
|
*/
|
||||||
private void updateHueShader() {
|
private void updateHueShader() {
|
||||||
LinearGradient hueShader = new LinearGradient(
|
LinearGradient hueShader = new LinearGradient(
|
||||||
@@ -211,8 +281,29 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the shader for the saturation-value selector based on the currently selected hue.
|
* Updates the shader for the opacity slider to reflect the current RGB color with varying opacity.
|
||||||
* This method creates a combined shader that blends a saturation gradient with a value gradient.
|
*/
|
||||||
|
private void updateOpacityShader() {
|
||||||
|
if (!opacitySliderEnabled) {
|
||||||
|
opacityPaint.setShader(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a linear gradient for opacity from transparent to opaque, using the current RGB color.
|
||||||
|
int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
|
||||||
|
LinearGradient opacityShader = new LinearGradient(
|
||||||
|
opacityRect.left, opacityRect.top,
|
||||||
|
opacityRect.right, opacityRect.top,
|
||||||
|
rgbColor & 0x00FFFFFF, // Fully transparent
|
||||||
|
rgbColor | 0xFF000000, // Fully opaque
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
opacityPaint.setShader(opacityShader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the shader for the saturation-value selector to reflect the current hue.
|
||||||
*/
|
*/
|
||||||
private void updateSaturationValueShader() {
|
private void updateSaturationValueShader() {
|
||||||
// Create a saturation-value gradient based on the current hue.
|
// Create a saturation-value gradient based on the current hue.
|
||||||
@@ -232,7 +323,6 @@ public class ColorPickerView extends View {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Create a linear gradient for the value (brightness) from white to black (vertical).
|
// Create a linear gradient for the value (brightness) from white to black (vertical).
|
||||||
//noinspection ExtractMethodRecommender
|
|
||||||
LinearGradient valShader = new LinearGradient(
|
LinearGradient valShader = new LinearGradient(
|
||||||
saturationValueRect.left, saturationValueRect.top,
|
saturationValueRect.left, saturationValueRect.top,
|
||||||
saturationValueRect.left, saturationValueRect.bottom,
|
saturationValueRect.left, saturationValueRect.bottom,
|
||||||
@@ -249,11 +339,7 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draws the color picker view on the canvas.
|
* Draws the color picker components, including the saturation-value selector, hue bar, opacity slider, and their respective handles.
|
||||||
* This method draws the saturation-value selector, the hue bar with rounded corners,
|
|
||||||
* and the draggable handles.
|
|
||||||
*
|
|
||||||
* @param canvas The canvas on which to draw.
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onDraw(Canvas canvas) {
|
protected void onDraw(Canvas canvas) {
|
||||||
@@ -263,49 +349,67 @@ public class ColorPickerView extends View {
|
|||||||
// Draw the hue bar.
|
// Draw the hue bar.
|
||||||
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
|
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
|
||||||
|
|
||||||
|
// Draw the opacity bar if enabled.
|
||||||
|
if (opacitySliderEnabled) {
|
||||||
|
canvas.drawRoundRect(opacityRect, OPACITY_CORNER_RADIUS, OPACITY_CORNER_RADIUS, opacityPaint);
|
||||||
|
}
|
||||||
|
|
||||||
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
||||||
final float hueSelectorY = hueRect.centerY();
|
final float hueSelectorY = hueRect.centerY();
|
||||||
|
|
||||||
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||||
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||||
|
|
||||||
// Draw the saturation and hue selector handle filled with the selected color.
|
// Draw the saturation and hue selector handles filled with their respective colors (fully opaque).
|
||||||
hsvArray[0] = hue;
|
hsvArray[0] = hue;
|
||||||
final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray);
|
final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray); // Force opaque for hue handle.
|
||||||
|
final int satHandleColor = Color.HSVToColor(0xFF, new float[]{hue, saturation, value}); // Force opaque for sat-val handle.
|
||||||
selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
|
|
||||||
selectorPaint.setColor(hueHandleColor);
|
selectorPaint.setColor(hueHandleColor);
|
||||||
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||||
|
|
||||||
selectorPaint.setColor(selectedColor | 0xFF000000);
|
selectorPaint.setColor(satHandleColor);
|
||||||
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||||
|
|
||||||
|
if (opacitySliderEnabled) {
|
||||||
|
final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
|
||||||
|
final float opacitySelectorY = opacityRect.centerY();
|
||||||
|
selectorPaint.setColor(selectedColor); // Use full ARGB color to show opacity.
|
||||||
|
canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw white outlines for the handles.
|
// Draw white outlines for the handles.
|
||||||
selectorPaint.setColor(SELECTOR_OUTLINE_COLOR);
|
selectorPaint.setColor(SELECTOR_OUTLINE_COLOR);
|
||||||
selectorPaint.setStyle(Paint.Style.STROKE);
|
selectorPaint.setStyle(Paint.Style.STROKE);
|
||||||
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
||||||
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint);
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||||
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint);
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||||
|
if (opacitySliderEnabled) {
|
||||||
|
final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
|
||||||
|
final float opacitySelectorY = opacityRect.centerY();
|
||||||
|
canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||||
|
}
|
||||||
|
|
||||||
// Draw thin dark outlines for the handles at the outer edge of the white outline.
|
// Draw thin dark outlines for the handles at the outer edge of the white outline.
|
||||||
selectorPaint.setColor(SELECTOR_EDGE_COLOR);
|
selectorPaint.setColor(SELECTOR_EDGE_COLOR);
|
||||||
selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH);
|
selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH);
|
||||||
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||||
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||||
|
if (opacitySliderEnabled) {
|
||||||
|
final float opacitySelectorX = opacityRect.left + opacity * opacityRect.width();
|
||||||
|
final float opacitySelectorY = opacityRect.centerY();
|
||||||
|
canvas.drawCircle(opacitySelectorX, opacitySelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles touch events on the view.
|
* Handles touch events to allow dragging of the hue, saturation-value, and opacity selectors.
|
||||||
* This method determines whether the touch event occurred within the hue bar or the saturation-value selector,
|
|
||||||
* updates the corresponding values (hue, saturation, value), and invalidates the view to trigger a redraw.
|
|
||||||
* <p>
|
|
||||||
* In addition to testing if the touch is within the strict rectangles, an expanded hit area (by selectorRadius)
|
|
||||||
* is used so that the draggable handles remain active even when half of the handle is outside the drawn bounds.
|
|
||||||
*
|
*
|
||||||
* @param event The motion event.
|
* @param event The motion event.
|
||||||
* @return True if the event was handled, false otherwise.
|
* @return True if the event was handled, false otherwise.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("ClickableViewAccessibility") // performClick is not overridden, but not needed in this case.
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouchEvent(MotionEvent event) {
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
try {
|
try {
|
||||||
@@ -314,13 +418,19 @@ public class ColorPickerView extends View {
|
|||||||
final int action = event.getAction();
|
final int action = event.getAction();
|
||||||
Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y);
|
Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y);
|
||||||
|
|
||||||
// Define touch expansion for the hue bar.
|
// Define touch expansion for the hue and opacity bars.
|
||||||
RectF expandedHueRect = new RectF(
|
RectF expandedHueRect = new RectF(
|
||||||
hueRect.left,
|
hueRect.left,
|
||||||
hueRect.top - TOUCH_EXPANSION,
|
hueRect.top - TOUCH_EXPANSION,
|
||||||
hueRect.right,
|
hueRect.right,
|
||||||
hueRect.bottom + TOUCH_EXPANSION
|
hueRect.bottom + TOUCH_EXPANSION
|
||||||
);
|
);
|
||||||
|
RectF expandedOpacityRect = opacitySliderEnabled ? new RectF(
|
||||||
|
opacityRect.left,
|
||||||
|
opacityRect.top - TOUCH_EXPANSION,
|
||||||
|
opacityRect.right,
|
||||||
|
opacityRect.bottom + TOUCH_EXPANSION
|
||||||
|
) : new RectF();
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case MotionEvent.ACTION_DOWN:
|
case MotionEvent.ACTION_DOWN:
|
||||||
@@ -331,7 +441,10 @@ public class ColorPickerView extends View {
|
|||||||
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||||
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||||
|
|
||||||
// Create hit areas for both handles.
|
final float opacitySelectorX = opacitySliderEnabled ? opacityRect.left + opacity * opacityRect.width() : 0;
|
||||||
|
final float opacitySelectorY = opacitySliderEnabled ? opacityRect.centerY() : 0;
|
||||||
|
|
||||||
|
// Create hit areas for all handles.
|
||||||
RectF hueHitRect = new RectF(
|
RectF hueHitRect = new RectF(
|
||||||
hueSelectorX - SELECTOR_RADIUS,
|
hueSelectorX - SELECTOR_RADIUS,
|
||||||
hueSelectorY - SELECTOR_RADIUS,
|
hueSelectorY - SELECTOR_RADIUS,
|
||||||
@@ -344,14 +457,23 @@ public class ColorPickerView extends View {
|
|||||||
satSelectorX + SELECTOR_RADIUS,
|
satSelectorX + SELECTOR_RADIUS,
|
||||||
valSelectorY + SELECTOR_RADIUS
|
valSelectorY + SELECTOR_RADIUS
|
||||||
);
|
);
|
||||||
|
RectF opacityHitRect = opacitySliderEnabled ? new RectF(
|
||||||
|
opacitySelectorX - SELECTOR_RADIUS,
|
||||||
|
opacitySelectorY - SELECTOR_RADIUS,
|
||||||
|
opacitySelectorX + SELECTOR_RADIUS,
|
||||||
|
opacitySelectorY + SELECTOR_RADIUS
|
||||||
|
) : new RectF();
|
||||||
|
|
||||||
// Check if the touch started on a handle or within the expanded hue bar area.
|
// Check if the touch started on a handle or within the expanded bar areas.
|
||||||
if (hueHitRect.contains(x, y)) {
|
if (hueHitRect.contains(x, y)) {
|
||||||
isDraggingHue = true;
|
isDraggingHue = true;
|
||||||
updateHueFromTouch(x);
|
updateHueFromTouch(x);
|
||||||
} else if (satValHitRect.contains(x, y)) {
|
} else if (satValHitRect.contains(x, y)) {
|
||||||
isDraggingSaturation = true;
|
isDraggingSaturation = true;
|
||||||
updateSaturationValueFromTouch(x, y);
|
updateSaturationValueFromTouch(x, y);
|
||||||
|
} else if (opacitySliderEnabled && opacityHitRect.contains(x, y)) {
|
||||||
|
isDraggingOpacity = true;
|
||||||
|
updateOpacityFromTouch(x);
|
||||||
} else if (expandedHueRect.contains(x, y)) {
|
} else if (expandedHueRect.contains(x, y)) {
|
||||||
// Handle touch within the expanded hue bar area.
|
// Handle touch within the expanded hue bar area.
|
||||||
isDraggingHue = true;
|
isDraggingHue = true;
|
||||||
@@ -359,6 +481,9 @@ public class ColorPickerView extends View {
|
|||||||
} else if (saturationValueRect.contains(x, y)) {
|
} else if (saturationValueRect.contains(x, y)) {
|
||||||
isDraggingSaturation = true;
|
isDraggingSaturation = true;
|
||||||
updateSaturationValueFromTouch(x, y);
|
updateSaturationValueFromTouch(x, y);
|
||||||
|
} else if (opacitySliderEnabled && expandedOpacityRect.contains(x, y)) {
|
||||||
|
isDraggingOpacity = true;
|
||||||
|
updateOpacityFromTouch(x);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -368,6 +493,8 @@ public class ColorPickerView extends View {
|
|||||||
updateHueFromTouch(x);
|
updateHueFromTouch(x);
|
||||||
} else if (isDraggingSaturation) {
|
} else if (isDraggingSaturation) {
|
||||||
updateSaturationValueFromTouch(x, y);
|
updateSaturationValueFromTouch(x, y);
|
||||||
|
} else if (isDraggingOpacity) {
|
||||||
|
updateOpacityFromTouch(x);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -375,6 +502,7 @@ public class ColorPickerView extends View {
|
|||||||
case MotionEvent.ACTION_CANCEL:
|
case MotionEvent.ACTION_CANCEL:
|
||||||
isDraggingHue = false;
|
isDraggingHue = false;
|
||||||
isDraggingSaturation = false;
|
isDraggingSaturation = false;
|
||||||
|
isDraggingOpacity = false;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -385,9 +513,7 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the hue value based on touch position, clamping to valid range.
|
* Updates the hue value based on a touch event.
|
||||||
*
|
|
||||||
* @param x The x-coordinate of the touch position.
|
|
||||||
*/
|
*/
|
||||||
private void updateHueFromTouch(float x) {
|
private void updateHueFromTouch(float x) {
|
||||||
// Clamp x to the hue rectangle bounds.
|
// Clamp x to the hue rectangle bounds.
|
||||||
@@ -399,14 +525,12 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
hue = updatedHue;
|
hue = updatedHue;
|
||||||
updateSaturationValueShader();
|
updateSaturationValueShader();
|
||||||
|
updateOpacityShader();
|
||||||
updateSelectedColor();
|
updateSelectedColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates saturation and value based on touch position, clamping to valid range.
|
* Updates the saturation and value based on a touch event.
|
||||||
*
|
|
||||||
* @param x The x-coordinate of the touch position.
|
|
||||||
* @param y The y-coordinate of the touch position.
|
|
||||||
*/
|
*/
|
||||||
private void updateSaturationValueFromTouch(float x, float y) {
|
private void updateSaturationValueFromTouch(float x, float y) {
|
||||||
// Clamp x and y to the saturation-value rectangle bounds.
|
// Clamp x and y to the saturation-value rectangle bounds.
|
||||||
@@ -421,14 +545,34 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
saturation = updatedSaturation;
|
saturation = updatedSaturation;
|
||||||
value = updatedValue;
|
value = updatedValue;
|
||||||
|
updateOpacityShader();
|
||||||
updateSelectedColor();
|
updateSelectedColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the selected color and notifies listeners.
|
* Updates the opacity value based on a touch event.
|
||||||
|
*/
|
||||||
|
private void updateOpacityFromTouch(float x) {
|
||||||
|
if (!opacitySliderEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final float clampedX = Utils.clamp(x, opacityRect.left, opacityRect.right);
|
||||||
|
final float updatedOpacity = (clampedX - opacityRect.left) / opacityRect.width();
|
||||||
|
if (opacity == updatedOpacity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
opacity = updatedOpacity;
|
||||||
|
updateSelectedColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the selected color based on the current hue, saturation, value, and opacity.
|
||||||
*/
|
*/
|
||||||
private void updateSelectedColor() {
|
private void updateSelectedColor() {
|
||||||
final int updatedColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
|
final int rgbColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
|
||||||
|
final int updatedColor = opacitySliderEnabled
|
||||||
|
? (rgbColor & 0x00FFFFFF) | (((int) (opacity * 255)) << 24)
|
||||||
|
: (rgbColor & 0x00FFFFFF) | 0xFF000000;
|
||||||
|
|
||||||
if (selectedColor != updatedColor) {
|
if (selectedColor != updatedColor) {
|
||||||
selectedColor = updatedColor;
|
selectedColor = updatedColor;
|
||||||
@@ -444,19 +588,16 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the currently selected color.
|
* Sets the selected color, updating the hue, saturation, value and opacity sliders accordingly.
|
||||||
*
|
|
||||||
* @param color The color to set in either ARGB or RGB format.
|
|
||||||
*/
|
*/
|
||||||
public void setColor(@ColorInt int color) {
|
public void setColor(@ColorInt int color) {
|
||||||
color &= 0x00FFFFFF;
|
|
||||||
if (selectedColor == color) {
|
if (selectedColor == color) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the selected color.
|
// Update the selected color.
|
||||||
selectedColor = color;
|
selectedColor = color;
|
||||||
Logger.printDebug(() -> "setColor: " + getColorString(selectedColor));
|
Logger.printDebug(() -> "setColor: " + getColorString(selectedColor, opacitySliderEnabled));
|
||||||
|
|
||||||
// Convert the ARGB color to HSV values.
|
// Convert the ARGB color to HSV values.
|
||||||
float[] hsv = new float[3];
|
float[] hsv = new float[3];
|
||||||
@@ -466,9 +607,11 @@ public class ColorPickerView extends View {
|
|||||||
hue = hsv[0];
|
hue = hsv[0];
|
||||||
saturation = hsv[1];
|
saturation = hsv[1];
|
||||||
value = hsv[2];
|
value = hsv[2];
|
||||||
|
opacity = opacitySliderEnabled ? ((color >> 24) & 0xFF) / 255f : 1f;
|
||||||
|
|
||||||
// Update the saturation-value shader based on the new hue.
|
// Update the saturation-value shader based on the new hue.
|
||||||
updateSaturationValueShader();
|
updateSaturationValueShader();
|
||||||
|
updateOpacityShader();
|
||||||
|
|
||||||
// Notify the listener if it's set.
|
// Notify the listener if it's set.
|
||||||
if (colorChangedListener != null) {
|
if (colorChangedListener != null) {
|
||||||
@@ -481,8 +624,6 @@ public class ColorPickerView extends View {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the currently selected color.
|
* Gets the currently selected color.
|
||||||
*
|
|
||||||
* @return The selected color in RGB format with no alpha channel.
|
|
||||||
*/
|
*/
|
||||||
@ColorInt
|
@ColorInt
|
||||||
public int getColor() {
|
public int getColor() {
|
||||||
@@ -490,9 +631,7 @@ public class ColorPickerView extends View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets the listener to be notified when the selected color changes.
|
* Sets a listener to be notified when the selected color changes.
|
||||||
*
|
|
||||||
* @param listener The listener to set.
|
|
||||||
*/
|
*/
|
||||||
public void setOnColorChangedListener(OnColorChangedListener listener) {
|
public void setOnColorChangedListener(OnColorChangedListener listener) {
|
||||||
colorChangedListener = listener;
|
colorChangedListener = listener;
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extended ColorPickerPreference that enables the opacity slider for color selection.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class ColorPickerWithOpacitySliderPreference extends ColorPickerPreference {
|
||||||
|
|
||||||
|
public ColorPickerWithOpacitySliderPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerWithOpacitySliderPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the preference with opacity slider enabled.
|
||||||
|
*/
|
||||||
|
private void init() {
|
||||||
|
// Enable the opacity slider for alpha channel support.
|
||||||
|
setOpacitySliderEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -9,18 +11,86 @@ import android.util.Pair;
|
|||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.*;
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator.
|
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator,
|
||||||
|
* supports a static summary and highlighted entries for search functionality.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class CustomDialogListPreference extends ListPreference {
|
public class CustomDialogListPreference extends ListPreference {
|
||||||
|
|
||||||
|
public static final int ID_REVANCED_CHECK_ICON =
|
||||||
|
getResourceIdentifierOrThrow("revanced_check_icon", "id");
|
||||||
|
public static final int ID_REVANCED_CHECK_ICON_PLACEHOLDER =
|
||||||
|
getResourceIdentifierOrThrow("revanced_check_icon_placeholder", "id");
|
||||||
|
public static final int ID_REVANCED_ITEM_TEXT =
|
||||||
|
getResourceIdentifierOrThrow("revanced_item_text", "id");
|
||||||
|
public static final int LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED =
|
||||||
|
getResourceIdentifierOrThrow("revanced_custom_list_item_checked", "layout");
|
||||||
|
|
||||||
|
private String staticSummary = null;
|
||||||
|
private CharSequence[] highlightedEntriesForDialog = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a static summary that will not be overwritten by value changes.
|
||||||
|
*/
|
||||||
|
public void setStaticSummary(String summary) {
|
||||||
|
this.staticSummary = summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the static summary if set, otherwise null.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public String getStaticSummary() {
|
||||||
|
return staticSummary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always return static summary if set.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public CharSequence getSummary() {
|
||||||
|
if (staticSummary != null) {
|
||||||
|
return staticSummary;
|
||||||
|
}
|
||||||
|
return super.getSummary();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets highlighted entries for display in the dialog.
|
||||||
|
* These entries are used only for the current dialog and are automatically cleared.
|
||||||
|
*/
|
||||||
|
public void setHighlightedEntriesForDialog(CharSequence[] highlightedEntries) {
|
||||||
|
this.highlightedEntriesForDialog = highlightedEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears highlighted entries after the dialog is closed.
|
||||||
|
*/
|
||||||
|
public void clearHighlightedEntriesForDialog() {
|
||||||
|
this.highlightedEntriesForDialog = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns entries for display in the dialog.
|
||||||
|
* If highlighted entries exist, they are used; otherwise, the original entries are returned.
|
||||||
|
*/
|
||||||
|
private CharSequence[] getEntriesForDialog() {
|
||||||
|
return highlightedEntriesForDialog != null ? highlightedEntriesForDialog : getEntries();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom ArrayAdapter to handle checkmark visibility.
|
* Custom ArrayAdapter to handle checkmark visibility.
|
||||||
*/
|
*/
|
||||||
@@ -35,8 +105,10 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
final CharSequence[] entryValues;
|
final CharSequence[] entryValues;
|
||||||
String selectedValue;
|
String selectedValue;
|
||||||
|
|
||||||
public ListPreferenceArrayAdapter(Context context, int resource, CharSequence[] entries,
|
public ListPreferenceArrayAdapter(Context context, int resource,
|
||||||
CharSequence[] entryValues, String selectedValue) {
|
CharSequence[] entries,
|
||||||
|
CharSequence[] entryValues,
|
||||||
|
String selectedValue) {
|
||||||
super(context, resource, entries);
|
super(context, resource, entries);
|
||||||
this.layoutResourceId = resource;
|
this.layoutResourceId = resource;
|
||||||
this.entryValues = entryValues;
|
this.entryValues = entryValues;
|
||||||
@@ -53,19 +125,16 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||||
view = inflater.inflate(layoutResourceId, parent, false);
|
view = inflater.inflate(layoutResourceId, parent, false);
|
||||||
holder = new SubViewDataContainer();
|
holder = new SubViewDataContainer();
|
||||||
holder.checkIcon = view.findViewById(Utils.getResourceIdentifier(
|
holder.checkIcon = view.findViewById(ID_REVANCED_CHECK_ICON);
|
||||||
"revanced_check_icon", "id"));
|
holder.placeholder = view.findViewById(ID_REVANCED_CHECK_ICON_PLACEHOLDER);
|
||||||
holder.placeholder = view.findViewById(Utils.getResourceIdentifier(
|
holder.itemText = view.findViewById(ID_REVANCED_ITEM_TEXT);
|
||||||
"revanced_check_icon_placeholder", "id"));
|
|
||||||
holder.itemText = view.findViewById(Utils.getResourceIdentifier(
|
|
||||||
"revanced_item_text", "id"));
|
|
||||||
view.setTag(holder);
|
view.setTag(holder);
|
||||||
} else {
|
} else {
|
||||||
holder = (SubViewDataContainer) view.getTag();
|
holder = (SubViewDataContainer) view.getTag();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set text.
|
CharSequence itemText = getItem(position);
|
||||||
holder.itemText.setText(getItem(position));
|
holder.itemText.setText(itemText);
|
||||||
holder.itemText.setTextColor(Utils.getAppForegroundColor());
|
holder.itemText.setTextColor(Utils.getAppForegroundColor());
|
||||||
|
|
||||||
// Show or hide checkmark and placeholder.
|
// Show or hide checkmark and placeholder.
|
||||||
@@ -103,6 +172,9 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
protected void showDialog(Bundle state) {
|
protected void showDialog(Bundle state) {
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
|
|
||||||
|
CharSequence[] entriesToShow = getEntriesForDialog();
|
||||||
|
CharSequence[] entryValues = getEntryValues();
|
||||||
|
|
||||||
// Create ListView.
|
// Create ListView.
|
||||||
ListView listView = new ListView(context);
|
ListView listView = new ListView(context);
|
||||||
listView.setId(android.R.id.list);
|
listView.setId(android.R.id.list);
|
||||||
@@ -111,9 +183,9 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
// Create custom adapter for the ListView.
|
// Create custom adapter for the ListView.
|
||||||
ListPreferenceArrayAdapter adapter = new ListPreferenceArrayAdapter(
|
ListPreferenceArrayAdapter adapter = new ListPreferenceArrayAdapter(
|
||||||
context,
|
context,
|
||||||
Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"),
|
LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED,
|
||||||
getEntries(),
|
entriesToShow,
|
||||||
getEntryValues(),
|
entryValues,
|
||||||
getValue()
|
getValue()
|
||||||
);
|
);
|
||||||
listView.setAdapter(adapter);
|
listView.setAdapter(adapter);
|
||||||
@@ -121,7 +193,6 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
// Set checked item.
|
// Set checked item.
|
||||||
String currentValue = getValue();
|
String currentValue = getValue();
|
||||||
if (currentValue != null) {
|
if (currentValue != null) {
|
||||||
CharSequence[] entryValues = getEntryValues();
|
|
||||||
for (int i = 0, length = entryValues.length; i < length; i++) {
|
for (int i = 0, length = entryValues.length; i < length; i++) {
|
||||||
if (currentValue.equals(entryValues[i].toString())) {
|
if (currentValue.equals(entryValues[i].toString())) {
|
||||||
listView.setItemChecked(i, true);
|
listView.setItemChecked(i, true);
|
||||||
@@ -132,19 +203,23 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create the custom dialog without OK button.
|
// Create the custom dialog without OK button.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
getTitle() != null ? getTitle().toString() : "",
|
getTitle() != null ? getTitle().toString() : "",
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null, // No OK button text.
|
null,
|
||||||
null, // No OK button action.
|
null,
|
||||||
() -> {}, // Cancel button action (just dismiss).
|
this::clearHighlightedEntriesForDialog, // Cancel button action.
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
Dialog dialog = dialogPair.first;
|
||||||
|
// Add a listener to clear when the dialog is closed in any way.
|
||||||
|
dialog.setOnDismissListener(dialogInterface -> clearHighlightedEntriesForDialog());
|
||||||
|
|
||||||
// Add the ListView to the main layout.
|
// Add the ListView to the main layout.
|
||||||
LinearLayout mainLayout = dialogPair.second;
|
LinearLayout mainLayout = dialogPair.second;
|
||||||
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
|
||||||
@@ -156,16 +231,28 @@ public class CustomDialogListPreference extends ListPreference {
|
|||||||
|
|
||||||
// Handle item click to select value and dismiss dialog.
|
// Handle item click to select value and dismiss dialog.
|
||||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||||
String selectedValue = getEntryValues()[position].toString();
|
String selectedValue = entryValues[position].toString();
|
||||||
if (callChangeListener(selectedValue)) {
|
if (callChangeListener(selectedValue)) {
|
||||||
setValue(selectedValue);
|
setValue(selectedValue);
|
||||||
|
|
||||||
|
// Update summaries from the original entries (without highlighting).
|
||||||
|
if (staticSummary == null) {
|
||||||
|
CharSequence[] originalEntries = getEntries();
|
||||||
|
if (originalEntries != null && position < originalEntries.length) {
|
||||||
|
setSummary(originalEntries[position]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
adapter.setSelectedValue(selectedValue);
|
adapter.setSelectedValue(selectedValue);
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
dialogPair.first.dismiss();
|
|
||||||
|
// Clear highlighted entries before closing.
|
||||||
|
clearHighlightedEntriesForDialog();
|
||||||
|
dialog.dismiss();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show the dialog.
|
// Show the dialog.
|
||||||
dialogPair.first.show();
|
dialog.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package app.revanced.extension.shared.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@@ -10,21 +9,17 @@ import android.os.Bundle;
|
|||||||
import android.preference.EditTextPreference;
|
import android.preference.EditTextPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.View;
|
import android.view.inputmethod.InputMethodManager;
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.drawable.ShapeDrawable;
|
|
||||||
import android.graphics.drawable.shapes.RoundRectShape;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
||||||
@@ -82,7 +77,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
EditText editText = getEditText();
|
EditText editText = getEditText();
|
||||||
|
|
||||||
// Create a custom dialog with the EditText.
|
// Create a custom dialog with the EditText.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
str("revanced_pref_import_export_title"), // Title.
|
str("revanced_pref_import_export_title"), // Title.
|
||||||
null, // No message (EditText replaces it).
|
null, // No message (EditText replaces it).
|
||||||
@@ -98,6 +93,20 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
true // Dismiss dialog when onNeutralClick.
|
true // Dismiss dialog when onNeutralClick.
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If there are no settings yet, then show the on screen keyboard and bring focus to
|
||||||
|
// the edit text. This makes it easier to paste saved settings after a reinstall.
|
||||||
|
dialogPair.first.setOnShowListener(dialogInterface -> {
|
||||||
|
if (existingSettings.isEmpty()) {
|
||||||
|
editText.postDelayed(() -> {
|
||||||
|
editText.requestFocus();
|
||||||
|
|
||||||
|
InputMethodManager inputMethodManager = (InputMethodManager)
|
||||||
|
editText.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
inputMethodManager.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Show the dialog.
|
// Show the dialog.
|
||||||
dialogPair.first.show();
|
dialogPair.first.show();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import android.os.Handler;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.view.Gravity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.webkit.WebView;
|
import android.webkit.WebView;
|
||||||
@@ -228,10 +229,10 @@ class WebViewDialog extends Dialog {
|
|||||||
|
|
||||||
setContentView(mainLayout);
|
setContentView(mainLayout);
|
||||||
|
|
||||||
// Set dialog window attributes
|
// Set dialog window attributes.
|
||||||
Window window = getWindow();
|
Window window = getWindow();
|
||||||
if (window != null) {
|
if (window != null) {
|
||||||
Utils.setDialogWindowParameters(window);
|
Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 90, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ import androidx.annotation.Nullable;
|
|||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ResettableEditTextPreference extends EditTextPreference {
|
public class ResettableEditTextPreference extends EditTextPreference {
|
||||||
@@ -66,7 +66,7 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
|||||||
|
|
||||||
// Create custom dialog.
|
// Create custom dialog.
|
||||||
String neutralButtonText = (setting != null) ? str("revanced_settings_reset") : null;
|
String neutralButtonText = (setting != null) ? str("revanced_settings_reset") : null;
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
getTitle() != null ? getTitle().toString() : "", // Title.
|
getTitle() != null ? getTitle().toString() : "", // Title.
|
||||||
null, // Message is replaced by EditText.
|
null, // Message is replaced by EditText.
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ public class ToolbarPreferenceFragment extends AbstractPreferenceFragment {
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("UseCompatLoadingForDrawables")
|
@SuppressLint("UseCompatLoadingForDrawables")
|
||||||
public static Drawable getBackButtonDrawable() {
|
public static Drawable getBackButtonDrawable() {
|
||||||
final int backButtonResource = Utils.getResourceIdentifier(
|
final int backButtonResource = Utils.getResourceIdentifierOrThrow(
|
||||||
"revanced_settings_toolbar_arrow_left", "drawable");
|
"revanced_settings_toolbar_arrow_left", "drawable");
|
||||||
Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
|
Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
|
||||||
customizeBackButtonDrawable(drawable);
|
customizeBackButtonDrawable(drawable);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
@@ -0,0 +1,365 @@
|
|||||||
|
package app.revanced.extension.shared.settings.search;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.preference.ListPreference;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.SwitchPreference;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.BackgroundColorSpan;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
||||||
|
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||||
|
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for search result items, defining common fields and behavior.
|
||||||
|
*/
|
||||||
|
public abstract class BaseSearchResultItem {
|
||||||
|
// Enum to represent view types.
|
||||||
|
public enum ViewType {
|
||||||
|
REGULAR,
|
||||||
|
SWITCH,
|
||||||
|
LIST,
|
||||||
|
COLOR_PICKER,
|
||||||
|
GROUP_HEADER,
|
||||||
|
NO_RESULTS,
|
||||||
|
URL_LINK;
|
||||||
|
|
||||||
|
// Get the corresponding layout resource ID.
|
||||||
|
public int getLayoutResourceId() {
|
||||||
|
return switch (this) {
|
||||||
|
case REGULAR, URL_LINK -> getResourceIdentifier("revanced_preference_search_result_regular");
|
||||||
|
case SWITCH -> getResourceIdentifier("revanced_preference_search_result_switch");
|
||||||
|
case LIST -> getResourceIdentifier("revanced_preference_search_result_list");
|
||||||
|
case COLOR_PICKER -> getResourceIdentifier("revanced_preference_search_result_color");
|
||||||
|
case GROUP_HEADER -> getResourceIdentifier("revanced_preference_search_result_group_header");
|
||||||
|
case NO_RESULTS -> getResourceIdentifier("revanced_preference_search_no_result");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getResourceIdentifier(String name) {
|
||||||
|
// Placeholder for actual resource identifier retrieval.
|
||||||
|
return Utils.getResourceIdentifierOrThrow(name, "layout");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String navigationPath;
|
||||||
|
final List<String> navigationKeys;
|
||||||
|
final ViewType preferenceType;
|
||||||
|
CharSequence highlightedTitle;
|
||||||
|
CharSequence highlightedSummary;
|
||||||
|
boolean highlightingApplied;
|
||||||
|
|
||||||
|
BaseSearchResultItem(String navPath, List<String> navKeys, ViewType type) {
|
||||||
|
this.navigationPath = navPath;
|
||||||
|
this.navigationKeys = new ArrayList<>(navKeys != null ? navKeys : Collections.emptyList());
|
||||||
|
this.preferenceType = type;
|
||||||
|
this.highlightedTitle = "";
|
||||||
|
this.highlightedSummary = "";
|
||||||
|
this.highlightingApplied = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract boolean matchesQuery(String query);
|
||||||
|
abstract void applyHighlighting(Pattern queryPattern);
|
||||||
|
abstract void clearHighlighting();
|
||||||
|
|
||||||
|
// Shared method for highlighting text with search query.
|
||||||
|
protected static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
|
||||||
|
if (TextUtils.isEmpty(text)) return text;
|
||||||
|
|
||||||
|
final int adjustedColor = Utils.adjustColorBrightness(
|
||||||
|
Utils.getAppBackgroundColor(), 0.95f, 1.20f);
|
||||||
|
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
|
||||||
|
SpannableStringBuilder spannable = new SpannableStringBuilder(text);
|
||||||
|
|
||||||
|
Matcher matcher = queryPattern.matcher(text);
|
||||||
|
while (matcher.find()) {
|
||||||
|
spannable.setSpan(highlightSpan, matcher.start(), matcher.end(),
|
||||||
|
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spannable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search result item for group headers (navigation path only).
|
||||||
|
*/
|
||||||
|
public static class GroupHeaderItem extends BaseSearchResultItem {
|
||||||
|
GroupHeaderItem(String navPath, List<String> navKeys) {
|
||||||
|
super(navPath, navKeys, ViewType.GROUP_HEADER);
|
||||||
|
this.highlightedTitle = navPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
boolean matchesQuery(String query) {
|
||||||
|
return false; // Headers are not directly searchable.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void applyHighlighting(Pattern queryPattern) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void clearHighlighting() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search result item for preferences, handling type-specific data and search text.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public static class PreferenceSearchItem extends BaseSearchResultItem {
|
||||||
|
public final Preference preference;
|
||||||
|
final String searchableText;
|
||||||
|
final CharSequence originalTitle;
|
||||||
|
final CharSequence originalSummary;
|
||||||
|
final CharSequence originalSummaryOn;
|
||||||
|
final CharSequence originalSummaryOff;
|
||||||
|
final CharSequence[] originalEntries;
|
||||||
|
private CharSequence[] highlightedEntries;
|
||||||
|
private boolean entriesHighlightingApplied;
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
private int color;
|
||||||
|
|
||||||
|
// Store last applied highlighting pattern to reapply when needed.
|
||||||
|
Pattern lastQueryPattern;
|
||||||
|
|
||||||
|
PreferenceSearchItem(Preference pref, String navPath, List<String> navKeys) {
|
||||||
|
super(navPath, navKeys, determineType(pref));
|
||||||
|
this.preference = pref;
|
||||||
|
this.originalTitle = pref.getTitle() != null ? pref.getTitle() : "";
|
||||||
|
this.originalSummary = pref.getSummary();
|
||||||
|
this.highlightedTitle = this.originalTitle;
|
||||||
|
this.highlightedSummary = this.originalSummary != null ? this.originalSummary : "";
|
||||||
|
this.color = 0;
|
||||||
|
this.lastQueryPattern = null;
|
||||||
|
|
||||||
|
// Initialize type-specific fields.
|
||||||
|
FieldInitializationResult result = initTypeSpecificFields(pref);
|
||||||
|
this.originalSummaryOn = result.summaryOn;
|
||||||
|
this.originalSummaryOff = result.summaryOff;
|
||||||
|
this.originalEntries = result.entries;
|
||||||
|
|
||||||
|
// Build searchable text.
|
||||||
|
this.searchableText = buildSearchableText(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FieldInitializationResult {
|
||||||
|
CharSequence summaryOn = null;
|
||||||
|
CharSequence summaryOff = null;
|
||||||
|
CharSequence[] entries = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ViewType determineType(Preference pref) {
|
||||||
|
if (pref instanceof SwitchPreference) return ViewType.SWITCH;
|
||||||
|
if (pref instanceof ListPreference) return ViewType.LIST;
|
||||||
|
if (pref instanceof ColorPickerPreference) return ViewType.COLOR_PICKER;
|
||||||
|
if (pref instanceof UrlLinkPreference) return ViewType.URL_LINK;
|
||||||
|
if ("no_results_placeholder".equals(pref.getKey())) return ViewType.NO_RESULTS;
|
||||||
|
return ViewType.REGULAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FieldInitializationResult initTypeSpecificFields(Preference pref) {
|
||||||
|
FieldInitializationResult result = new FieldInitializationResult();
|
||||||
|
|
||||||
|
if (pref instanceof SwitchPreference switchPref) {
|
||||||
|
result.summaryOn = switchPref.getSummaryOn();
|
||||||
|
result.summaryOff = switchPref.getSummaryOff();
|
||||||
|
} else if (pref instanceof ColorPickerPreference colorPref) {
|
||||||
|
String colorString = colorPref.getText();
|
||||||
|
this.color = TextUtils.isEmpty(colorString) ? 0 : Color.parseColor(colorString);
|
||||||
|
} else if (pref instanceof ListPreference listPref) {
|
||||||
|
result.entries = listPref.getEntries();
|
||||||
|
if (result.entries != null) {
|
||||||
|
this.highlightedEntries = new CharSequence[result.entries.length];
|
||||||
|
System.arraycopy(result.entries, 0, this.highlightedEntries, 0, result.entries.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.entriesHighlightingApplied = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildSearchableText(Preference pref) {
|
||||||
|
StringBuilder searchBuilder = new StringBuilder();
|
||||||
|
String key = pref.getKey();
|
||||||
|
String normalizedKey = "";
|
||||||
|
if (key != null) {
|
||||||
|
// Normalize preference key by removing the common "revanced_" prefix
|
||||||
|
// so that users can search by the meaningful part only.
|
||||||
|
normalizedKey = key.startsWith("revanced_")
|
||||||
|
? key.substring("revanced_".length())
|
||||||
|
: key;
|
||||||
|
}
|
||||||
|
appendText(searchBuilder, normalizedKey);
|
||||||
|
appendText(searchBuilder, originalTitle);
|
||||||
|
appendText(searchBuilder, originalSummary);
|
||||||
|
|
||||||
|
// Add type-specific searchable content.
|
||||||
|
if (pref instanceof ListPreference) {
|
||||||
|
if (originalEntries != null) {
|
||||||
|
for (CharSequence entry : originalEntries) {
|
||||||
|
appendText(searchBuilder, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (pref instanceof SwitchPreference) {
|
||||||
|
appendText(searchBuilder, originalSummaryOn);
|
||||||
|
appendText(searchBuilder, originalSummaryOff);
|
||||||
|
} else if (pref instanceof ColorPickerPreference) {
|
||||||
|
appendText(searchBuilder, ColorPickerPreference.getColorString(color, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include navigation path in searchable text.
|
||||||
|
appendText(searchBuilder, navigationPath);
|
||||||
|
|
||||||
|
return searchBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendText(StringBuilder builder, CharSequence text) {
|
||||||
|
if (!TextUtils.isEmpty(text)) {
|
||||||
|
if (builder.length() > 0) builder.append(" ");
|
||||||
|
builder.append(Utils.removePunctuationToLowercase(text));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the current effective summary for this preference, considering state-dependent summaries.
|
||||||
|
*/
|
||||||
|
public CharSequence getCurrentEffectiveSummary() {
|
||||||
|
if (preference instanceof CustomDialogListPreference customPref) {
|
||||||
|
String staticSum = customPref.getStaticSummary();
|
||||||
|
if (staticSum != null) {
|
||||||
|
return staticSum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (preference instanceof SwitchPreference switchPref) {
|
||||||
|
boolean currentState = switchPref.isChecked();
|
||||||
|
return currentState
|
||||||
|
? (originalSummaryOn != null ? originalSummaryOn :
|
||||||
|
originalSummary != null ? originalSummary : "")
|
||||||
|
: (originalSummaryOff != null ? originalSummaryOff :
|
||||||
|
originalSummary != null ? originalSummary : "");
|
||||||
|
} else if (preference instanceof ListPreference listPref) {
|
||||||
|
String value = listPref.getValue();
|
||||||
|
CharSequence[] entries = listPref.getEntries();
|
||||||
|
CharSequence[] entryValues = listPref.getEntryValues();
|
||||||
|
if (value != null && entries != null && entryValues != null) {
|
||||||
|
for (int i = 0, length = entries.length; i < length; i++) {
|
||||||
|
if (value.equals(entryValues[i].toString())) {
|
||||||
|
return originalEntries != null && i < originalEntries.length && originalEntries[i] != null
|
||||||
|
? originalEntries[i]
|
||||||
|
: originalSummary != null ? originalSummary : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalSummary != null ? originalSummary : "";
|
||||||
|
}
|
||||||
|
return originalSummary != null ? originalSummary : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this search result item matches the provided query.
|
||||||
|
* Uses case-insensitive matching against the searchable text.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
boolean matchesQuery(String query) {
|
||||||
|
return searchableText.contains(Utils.removePunctuationToLowercase(query));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get highlighted entries to show in dialog.
|
||||||
|
*/
|
||||||
|
public CharSequence[] getHighlightedEntries() {
|
||||||
|
return highlightedEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether highlighting is applied to entries.
|
||||||
|
*/
|
||||||
|
public boolean isEntriesHighlightingApplied() {
|
||||||
|
return entriesHighlightingApplied;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights the search query in the title and summary.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void applyHighlighting(Pattern queryPattern) {
|
||||||
|
this.lastQueryPattern = queryPattern;
|
||||||
|
// Highlight the title.
|
||||||
|
highlightedTitle = highlightSearchQuery(originalTitle, queryPattern);
|
||||||
|
|
||||||
|
// Get the current effective summary and highlight it.
|
||||||
|
CharSequence currentSummary = getCurrentEffectiveSummary();
|
||||||
|
highlightedSummary = highlightSearchQuery(currentSummary, queryPattern);
|
||||||
|
|
||||||
|
// Highlight the entries.
|
||||||
|
if (preference instanceof ListPreference && originalEntries != null) {
|
||||||
|
highlightedEntries = new CharSequence[originalEntries.length];
|
||||||
|
for (int i = 0, length = originalEntries.length; i < length; i++) {
|
||||||
|
if (originalEntries[i] != null) {
|
||||||
|
highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern);
|
||||||
|
} else {
|
||||||
|
highlightedEntries[i] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entriesHighlightingApplied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
highlightingApplied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all search query highlighting and restores original state completely.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
void clearHighlighting() {
|
||||||
|
if (!highlightingApplied) return;
|
||||||
|
|
||||||
|
// Restore original title.
|
||||||
|
highlightedTitle = originalTitle;
|
||||||
|
|
||||||
|
// Restore current effective summary without highlighting.
|
||||||
|
highlightedSummary = getCurrentEffectiveSummary();
|
||||||
|
|
||||||
|
// Restore original entries.
|
||||||
|
if (originalEntries != null && highlightedEntries != null) {
|
||||||
|
System.arraycopy(originalEntries, 0, highlightedEntries, 0,
|
||||||
|
Math.min(originalEntries.length, highlightedEntries.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
entriesHighlightingApplied = false;
|
||||||
|
highlightingApplied = false;
|
||||||
|
lastQueryPattern = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes highlighting for dynamic summaries (like switch preferences).
|
||||||
|
* Should be called when the preference state changes.
|
||||||
|
*/
|
||||||
|
public void refreshHighlighting() {
|
||||||
|
if (highlightingApplied && lastQueryPattern != null) {
|
||||||
|
CharSequence currentSummary = getCurrentEffectiveSummary();
|
||||||
|
highlightedSummary = highlightSearchQuery(currentSummary, lastQueryPattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setColor(int newColor) {
|
||||||
|
this.color = newColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
public int getColor() {
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,621 @@
|
|||||||
|
package app.revanced.extension.shared.settings.search;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
import static app.revanced.extension.shared.settings.search.BaseSearchViewController.DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON;
|
||||||
|
|
||||||
|
import android.animation.AnimatorSet;
|
||||||
|
import android.animation.ArgbEvaluator;
|
||||||
|
import android.animation.ObjectAnimator;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.preference.ListPreference;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceGroup;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.preference.SwitchPreference;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.AbsListView;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ListAdapter;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.Switch;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
||||||
|
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||||
|
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
|
||||||
|
import app.revanced.extension.shared.ui.ColorDot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract adapter for displaying search results in overlay ListView with ViewHolder pattern.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public abstract class BaseSearchResultsAdapter extends ArrayAdapter<BaseSearchResultItem> {
|
||||||
|
protected final LayoutInflater inflater;
|
||||||
|
protected final BaseSearchViewController.BasePreferenceFragment fragment;
|
||||||
|
protected final BaseSearchViewController searchViewController;
|
||||||
|
protected AnimatorSet currentAnimator;
|
||||||
|
protected abstract PreferenceScreen getMainPreferenceScreen();
|
||||||
|
|
||||||
|
protected static final int BLINK_DURATION = 400;
|
||||||
|
protected static final int PAUSE_BETWEEN_BLINKS = 100;
|
||||||
|
|
||||||
|
protected static final int ID_PREFERENCE_TITLE = getResourceIdentifierOrThrow(
|
||||||
|
"preference_title", "id");
|
||||||
|
protected static final int ID_PREFERENCE_SUMMARY = getResourceIdentifierOrThrow(
|
||||||
|
"preference_summary", "id");
|
||||||
|
protected static final int ID_PREFERENCE_PATH = getResourceIdentifierOrThrow(
|
||||||
|
"preference_path", "id");
|
||||||
|
protected static final int ID_PREFERENCE_SWITCH = getResourceIdentifierOrThrow(
|
||||||
|
"preference_switch", "id");
|
||||||
|
protected static final int ID_PREFERENCE_COLOR_DOT = getResourceIdentifierOrThrow(
|
||||||
|
"preference_color_dot", "id");
|
||||||
|
|
||||||
|
protected static class RegularViewHolder {
|
||||||
|
TextView titleView;
|
||||||
|
TextView summaryView;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class SwitchViewHolder {
|
||||||
|
TextView titleView;
|
||||||
|
TextView summaryView;
|
||||||
|
Switch switchWidget;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class ColorViewHolder {
|
||||||
|
TextView titleView;
|
||||||
|
TextView summaryView;
|
||||||
|
View colorDot;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class GroupHeaderViewHolder {
|
||||||
|
TextView pathView;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static class NoResultsViewHolder {
|
||||||
|
TextView titleView;
|
||||||
|
TextView summaryView;
|
||||||
|
ImageView iconView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BaseSearchResultsAdapter(Context context, List<BaseSearchResultItem> items,
|
||||||
|
BaseSearchViewController.BasePreferenceFragment fragment,
|
||||||
|
BaseSearchViewController searchViewController) {
|
||||||
|
super(context, 0, items);
|
||||||
|
this.inflater = LayoutInflater.from(context);
|
||||||
|
this.fragment = fragment;
|
||||||
|
this.searchViewController = searchViewController;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
BaseSearchResultItem item = getItem(position);
|
||||||
|
return item == null ? 0 : item.preferenceType.ordinal();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getViewTypeCount() {
|
||||||
|
return BaseSearchResultItem.ViewType.values().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||||
|
BaseSearchResultItem item = getItem(position);
|
||||||
|
if (item == null) return new View(getContext());
|
||||||
|
// Use the ViewType enum.
|
||||||
|
BaseSearchResultItem.ViewType viewType = item.preferenceType;
|
||||||
|
// Create or reuse preference view based on type.
|
||||||
|
return createPreferenceView(item, convertView, viewType, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled(int position) {
|
||||||
|
BaseSearchResultItem item = getItem(position);
|
||||||
|
// Disable for NO_RESULTS items to prevent ripple/selection.
|
||||||
|
return item != null && item.preferenceType != BaseSearchResultItem.ViewType.NO_RESULTS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or reuses a view for the given SearchResultItem.
|
||||||
|
* <p>
|
||||||
|
* Thanks to {@link #getItemViewType(int)} and {@link #getViewTypeCount()}, ListView knows
|
||||||
|
* how many different row types exist and keeps a separate "recycling pool" for each.
|
||||||
|
* That means convertView passed here is ALWAYS of the correct type for this position.
|
||||||
|
* So only need to check if (view == null), and if so – inflate a new layout and create the proper ViewHolder.
|
||||||
|
*/
|
||||||
|
protected View createPreferenceView(BaseSearchResultItem item, View convertView,
|
||||||
|
BaseSearchResultItem.ViewType viewType, ViewGroup parent) {
|
||||||
|
View view = convertView;
|
||||||
|
if (view == null) {
|
||||||
|
view = inflateViewForType(viewType, parent);
|
||||||
|
createViewHolderForType(view, viewType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the cached ViewHolder.
|
||||||
|
Object holder = view.getTag();
|
||||||
|
bindDataToViewHolder(item, holder, viewType, view);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected View inflateViewForType(BaseSearchResultItem.ViewType viewType, ViewGroup parent) {
|
||||||
|
return inflater.inflate(viewType.getLayoutResourceId(), parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void createViewHolderForType(View view, BaseSearchResultItem.ViewType viewType) {
|
||||||
|
switch (viewType) {
|
||||||
|
case REGULAR, LIST, URL_LINK -> {
|
||||||
|
RegularViewHolder regularHolder = new RegularViewHolder();
|
||||||
|
regularHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
|
||||||
|
regularHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
|
||||||
|
view.setTag(regularHolder);
|
||||||
|
}
|
||||||
|
case SWITCH -> {
|
||||||
|
SwitchViewHolder switchHolder = new SwitchViewHolder();
|
||||||
|
switchHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
|
||||||
|
switchHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
|
||||||
|
switchHolder.switchWidget = view.findViewById(ID_PREFERENCE_SWITCH);
|
||||||
|
view.setTag(switchHolder);
|
||||||
|
}
|
||||||
|
case COLOR_PICKER -> {
|
||||||
|
ColorViewHolder colorHolder = new ColorViewHolder();
|
||||||
|
colorHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
|
||||||
|
colorHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
|
||||||
|
colorHolder.colorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT);
|
||||||
|
view.setTag(colorHolder);
|
||||||
|
}
|
||||||
|
case GROUP_HEADER -> {
|
||||||
|
GroupHeaderViewHolder groupHolder = new GroupHeaderViewHolder();
|
||||||
|
groupHolder.pathView = view.findViewById(ID_PREFERENCE_PATH);
|
||||||
|
view.setTag(groupHolder);
|
||||||
|
}
|
||||||
|
case NO_RESULTS -> {
|
||||||
|
NoResultsViewHolder noResultsHolder = new NoResultsViewHolder();
|
||||||
|
noResultsHolder.titleView = view.findViewById(ID_PREFERENCE_TITLE);
|
||||||
|
noResultsHolder.summaryView = view.findViewById(ID_PREFERENCE_SUMMARY);
|
||||||
|
noResultsHolder.iconView = view.findViewById(android.R.id.icon);
|
||||||
|
view.setTag(noResultsHolder);
|
||||||
|
}
|
||||||
|
default -> throw new IllegalStateException("Unknown viewType: " + viewType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void bindDataToViewHolder(BaseSearchResultItem item, Object holder,
|
||||||
|
BaseSearchResultItem.ViewType viewType, View view) {
|
||||||
|
switch (viewType) {
|
||||||
|
case REGULAR, URL_LINK, LIST -> bindRegularViewHolder(item, (RegularViewHolder) holder, view);
|
||||||
|
case SWITCH -> bindSwitchViewHolder(item, (SwitchViewHolder) holder, view);
|
||||||
|
case COLOR_PICKER -> bindColorViewHolder(item, (ColorViewHolder) holder, view);
|
||||||
|
case GROUP_HEADER -> bindGroupHeaderViewHolder(item, (GroupHeaderViewHolder) holder, view);
|
||||||
|
case NO_RESULTS -> bindNoResultsViewHolder(item, (NoResultsViewHolder) holder);
|
||||||
|
default -> throw new IllegalStateException("Unknown viewType: " + viewType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void bindRegularViewHolder(BaseSearchResultItem item, RegularViewHolder holder, View view) {
|
||||||
|
BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
|
||||||
|
prefItem.refreshHighlighting();
|
||||||
|
holder.titleView.setText(item.highlightedTitle);
|
||||||
|
holder.summaryView.setText(item.highlightedSummary);
|
||||||
|
holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
|
||||||
|
setupPreferenceView(view, holder.titleView, holder.summaryView, prefItem.preference,
|
||||||
|
() -> {
|
||||||
|
handlePreferenceClick(prefItem.preference);
|
||||||
|
if (prefItem.preference instanceof ListPreference) {
|
||||||
|
prefItem.refreshHighlighting();
|
||||||
|
holder.summaryView.setText(prefItem.getCurrentEffectiveSummary());
|
||||||
|
holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() -> navigateAndScrollToPreference(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void bindSwitchViewHolder(BaseSearchResultItem item, SwitchViewHolder holder, View view) {
|
||||||
|
BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
|
||||||
|
SwitchPreference switchPref = (SwitchPreference) prefItem.preference;
|
||||||
|
holder.titleView.setText(item.highlightedTitle);
|
||||||
|
holder.switchWidget.setBackground(null); // Remove ripple/highlight.
|
||||||
|
// Sync switch state with preference without animation.
|
||||||
|
boolean currentState = switchPref.isChecked();
|
||||||
|
if (holder.switchWidget.isChecked() != currentState) {
|
||||||
|
holder.switchWidget.setChecked(currentState);
|
||||||
|
holder.switchWidget.jumpDrawablesToCurrentState();
|
||||||
|
}
|
||||||
|
prefItem.refreshHighlighting();
|
||||||
|
holder.summaryView.setText(prefItem.highlightedSummary);
|
||||||
|
holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
|
||||||
|
setupPreferenceView(view, holder.titleView, holder.summaryView, switchPref,
|
||||||
|
() -> {
|
||||||
|
boolean newState = !switchPref.isChecked();
|
||||||
|
switchPref.setChecked(newState);
|
||||||
|
holder.switchWidget.setChecked(newState);
|
||||||
|
prefItem.refreshHighlighting();
|
||||||
|
holder.summaryView.setText(prefItem.getCurrentEffectiveSummary());
|
||||||
|
holder.summaryView.setVisibility(TextUtils.isEmpty(prefItem.highlightedSummary) ? View.GONE : View.VISIBLE);
|
||||||
|
if (switchPref.getOnPreferenceChangeListener() != null) {
|
||||||
|
switchPref.getOnPreferenceChangeListener().onPreferenceChange(switchPref, newState);
|
||||||
|
}
|
||||||
|
notifyDataSetChanged();
|
||||||
|
},
|
||||||
|
() -> navigateAndScrollToPreference(item));
|
||||||
|
holder.switchWidget.setEnabled(switchPref.isEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void bindColorViewHolder(BaseSearchResultItem item, ColorViewHolder holder, View view) {
|
||||||
|
BaseSearchResultItem.PreferenceSearchItem prefItem = (BaseSearchResultItem.PreferenceSearchItem) item;
|
||||||
|
holder.titleView.setText(item.highlightedTitle);
|
||||||
|
holder.summaryView.setText(item.highlightedSummary);
|
||||||
|
holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
|
||||||
|
ColorDot.applyColorDot(holder.colorDot, prefItem.getColor(), prefItem.preference.isEnabled());
|
||||||
|
setupPreferenceView(view, holder.titleView, holder.summaryView, prefItem.preference,
|
||||||
|
() -> handlePreferenceClick(prefItem.preference),
|
||||||
|
() -> navigateAndScrollToPreference(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void bindGroupHeaderViewHolder(BaseSearchResultItem item, GroupHeaderViewHolder holder, View view) {
|
||||||
|
holder.pathView.setText(item.highlightedTitle);
|
||||||
|
view.setOnClickListener(v -> navigateToTargetScreen(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void bindNoResultsViewHolder(BaseSearchResultItem item, NoResultsViewHolder holder) {
|
||||||
|
holder.titleView.setText(item.highlightedTitle);
|
||||||
|
holder.summaryView.setText(item.highlightedSummary);
|
||||||
|
holder.summaryView.setVisibility(TextUtils.isEmpty(item.highlightedSummary) ? View.GONE : View.VISIBLE);
|
||||||
|
holder.iconView.setImageResource(DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up a preference view with click listeners and proper enabled state handling.
|
||||||
|
*/
|
||||||
|
protected void setupPreferenceView(View view, TextView titleView, TextView summaryView, Preference preference,
|
||||||
|
Runnable onClickAction, Runnable onLongClickAction) {
|
||||||
|
boolean enabled = preference.isEnabled();
|
||||||
|
|
||||||
|
// To enable long-click navigation for disabled settings, manually control the enabled state of the title
|
||||||
|
// and summary and disable the ripple effect instead of using 'view.setEnabled(enabled)'.
|
||||||
|
|
||||||
|
titleView.setEnabled(enabled);
|
||||||
|
summaryView.setEnabled(enabled);
|
||||||
|
|
||||||
|
if (!enabled) view.setBackground(null); // Disable ripple effect.
|
||||||
|
|
||||||
|
// In light mode, alpha 0.5 is applied to a disabled title automatically,
|
||||||
|
// but in dark mode it needs to be applied manually.
|
||||||
|
if (Utils.isDarkModeEnabled()) {
|
||||||
|
titleView.setAlpha(enabled ? 1.0f : ColorPickerPreference.DISABLED_ALPHA);
|
||||||
|
}
|
||||||
|
// Set up click and long-click listeners.
|
||||||
|
view.setOnClickListener(enabled ? v -> onClickAction.run() : null);
|
||||||
|
view.setOnLongClickListener(v -> {
|
||||||
|
onLongClickAction.run();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the settings screen containing the given search result item and triggers scrolling.
|
||||||
|
*/
|
||||||
|
protected void navigateAndScrollToPreference(BaseSearchResultItem item) {
|
||||||
|
// No navigation for URL_LINK items.
|
||||||
|
if (item.preferenceType == BaseSearchResultItem.ViewType.URL_LINK) return;
|
||||||
|
|
||||||
|
PreferenceScreen targetScreen = navigateToTargetScreen(item);
|
||||||
|
if (targetScreen == null) return;
|
||||||
|
if (!(item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem)) return;
|
||||||
|
|
||||||
|
Preference targetPreference = prefItem.preference;
|
||||||
|
|
||||||
|
fragment.getView().post(() -> {
|
||||||
|
ListView listView = targetScreen == getMainPreferenceScreen()
|
||||||
|
? getPreferenceListView()
|
||||||
|
: targetScreen.getDialog().findViewById(android.R.id.list);
|
||||||
|
|
||||||
|
if (listView == null) return;
|
||||||
|
|
||||||
|
int targetPosition = findPreferencePosition(targetPreference, listView);
|
||||||
|
if (targetPosition == -1) return;
|
||||||
|
|
||||||
|
int firstVisible = listView.getFirstVisiblePosition();
|
||||||
|
int lastVisible = listView.getLastVisiblePosition();
|
||||||
|
|
||||||
|
if (targetPosition >= firstVisible && targetPosition <= lastVisible) {
|
||||||
|
// The preference is already visible, but still scroll it to the bottom of the list for consistency.
|
||||||
|
View child = listView.getChildAt(targetPosition - firstVisible);
|
||||||
|
if (child != null) {
|
||||||
|
// Calculate how much to scroll so the item is aligned at the bottom.
|
||||||
|
int scrollAmount = child.getBottom() - listView.getHeight();
|
||||||
|
if (scrollAmount > 0) {
|
||||||
|
// Perform smooth scroll animation for better user experience.
|
||||||
|
listView.smoothScrollBy(scrollAmount, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Highlight the preference once it is positioned.
|
||||||
|
highlightPreferenceAtPosition(listView, targetPosition);
|
||||||
|
} else {
|
||||||
|
// The preference is outside of the current visible range, scroll to it from the top.
|
||||||
|
listView.smoothScrollToPositionFromTop(targetPosition, 0);
|
||||||
|
|
||||||
|
Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
// Fallback runnable in case the OnScrollListener does not trigger.
|
||||||
|
Runnable fallback = () -> {
|
||||||
|
listView.setOnScrollListener(null);
|
||||||
|
highlightPreferenceAtPosition(listView, targetPosition);
|
||||||
|
};
|
||||||
|
// Post fallback with a small delay.
|
||||||
|
handler.postDelayed(fallback, 350);
|
||||||
|
|
||||||
|
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
|
||||||
|
private boolean isScrolling = false;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScrollStateChanged(AbsListView view, int scrollState) {
|
||||||
|
if (scrollState == SCROLL_STATE_TOUCH_SCROLL || scrollState == SCROLL_STATE_FLING) {
|
||||||
|
// Mark that scrolling has started.
|
||||||
|
isScrolling = true;
|
||||||
|
}
|
||||||
|
if (scrollState == SCROLL_STATE_IDLE && isScrolling) {
|
||||||
|
// Scrolling is finished, cleanup listener and cancel fallback.
|
||||||
|
isScrolling = false;
|
||||||
|
listView.setOnScrollListener(null);
|
||||||
|
handler.removeCallbacks(fallback);
|
||||||
|
// Highlight the target preference when scrolling is done.
|
||||||
|
highlightPreferenceAtPosition(listView, targetPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Navigates to the final PreferenceScreen using preference keys or titles as fallback.
|
||||||
|
*/
|
||||||
|
protected PreferenceScreen navigateToTargetScreen(BaseSearchResultItem item) {
|
||||||
|
PreferenceScreen currentScreen = getMainPreferenceScreen();
|
||||||
|
Preference targetPref = null;
|
||||||
|
|
||||||
|
// Try key-based navigation first.
|
||||||
|
if (item.navigationKeys != null && !item.navigationKeys.isEmpty()) {
|
||||||
|
String finalKey = item.navigationKeys.get(item.navigationKeys.size() - 1);
|
||||||
|
targetPref = findPreferenceByKey(currentScreen, finalKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to title-based navigation.
|
||||||
|
if (targetPref == null && !TextUtils.isEmpty(item.navigationPath)) {
|
||||||
|
String[] pathSegments = item.navigationPath.split(" > ");
|
||||||
|
String finalSegment = pathSegments[pathSegments.length - 1].trim();
|
||||||
|
if (!TextUtils.isEmpty(finalSegment)) {
|
||||||
|
targetPref = findPreferenceByTitle(currentScreen, finalSegment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetPref instanceof PreferenceScreen targetScreen) {
|
||||||
|
handlePreferenceClick(targetScreen);
|
||||||
|
return targetScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentScreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively searches for a preference by title in a preference group.
|
||||||
|
*/
|
||||||
|
protected Preference findPreferenceByTitle(PreferenceGroup group, String title) {
|
||||||
|
for (int i = 0; i < group.getPreferenceCount(); i++) {
|
||||||
|
Preference pref = group.getPreference(i);
|
||||||
|
CharSequence prefTitle = pref.getTitle();
|
||||||
|
if (prefTitle != null && (prefTitle.toString().trim().equalsIgnoreCase(title)
|
||||||
|
|| normalizeString(prefTitle.toString()).equals(normalizeString(title)))) {
|
||||||
|
return pref;
|
||||||
|
}
|
||||||
|
if (pref instanceof PreferenceGroup) {
|
||||||
|
Preference found = findPreferenceByTitle((PreferenceGroup) pref, title);
|
||||||
|
if (found != null) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes string for comparison (removes extra characters, spaces etc).
|
||||||
|
*/
|
||||||
|
protected String normalizeString(String input) {
|
||||||
|
if (TextUtils.isEmpty(input)) return "";
|
||||||
|
return input.trim().toLowerCase().replaceAll("\\s+", " ").replaceAll("[^\\w\\s]", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the ListView from the PreferenceFragment.
|
||||||
|
*/
|
||||||
|
protected ListView getPreferenceListView() {
|
||||||
|
View fragmentView = fragment.getView();
|
||||||
|
if (fragmentView != null) {
|
||||||
|
ListView listView = findListViewInViewGroup(fragmentView);
|
||||||
|
if (listView != null) {
|
||||||
|
return listView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fragment.getActivity().findViewById(android.R.id.list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively searches for a ListView in a ViewGroup.
|
||||||
|
*/
|
||||||
|
protected ListView findListViewInViewGroup(View view) {
|
||||||
|
if (view instanceof ListView) {
|
||||||
|
return (ListView) view;
|
||||||
|
}
|
||||||
|
if (view instanceof ViewGroup group) {
|
||||||
|
for (int i = 0; i < group.getChildCount(); i++) {
|
||||||
|
ListView result = findListViewInViewGroup(group.getChildAt(i));
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the position of a preference in the ListView adapter.
|
||||||
|
*/
|
||||||
|
protected int findPreferencePosition(Preference targetPreference, ListView listView) {
|
||||||
|
ListAdapter adapter = listView.getAdapter();
|
||||||
|
if (adapter == null) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < adapter.getCount(); i++) {
|
||||||
|
Object item = adapter.getItem(i);
|
||||||
|
if (item == targetPreference) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
if (item instanceof Preference pref && targetPreference.getKey() != null) {
|
||||||
|
if (targetPreference.getKey().equals(pref.getKey())) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights a preference at the specified position with a blink effect.
|
||||||
|
*/
|
||||||
|
protected void highlightPreferenceAtPosition(ListView listView, int position) {
|
||||||
|
int firstVisible = listView.getFirstVisiblePosition();
|
||||||
|
if (position < firstVisible || position > listView.getLastVisiblePosition()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
View itemView = listView.getChildAt(position - firstVisible);
|
||||||
|
if (itemView != null) {
|
||||||
|
blinkView(itemView);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a smooth double-blink effect on a view's background without affecting the text.
|
||||||
|
* @param view The View to apply the animation to.
|
||||||
|
*/
|
||||||
|
protected void blinkView(View view) {
|
||||||
|
// If a previous animation is still running, cancel it to prevent conflicts.
|
||||||
|
if (currentAnimator != null && currentAnimator.isRunning()) {
|
||||||
|
currentAnimator.cancel();
|
||||||
|
}
|
||||||
|
int startColor = Utils.getAppBackgroundColor();
|
||||||
|
int highlightColor = Utils.adjustColorBrightness(
|
||||||
|
startColor,
|
||||||
|
Utils.isDarkModeEnabled() ? 1.25f : 0.8f
|
||||||
|
);
|
||||||
|
// Animator for transitioning from the start color to the highlight color.
|
||||||
|
ObjectAnimator fadeIn = ObjectAnimator.ofObject(
|
||||||
|
view,
|
||||||
|
"backgroundColor",
|
||||||
|
new ArgbEvaluator(),
|
||||||
|
startColor,
|
||||||
|
highlightColor
|
||||||
|
);
|
||||||
|
fadeIn.setDuration(BLINK_DURATION);
|
||||||
|
// Animator to return to the start color.
|
||||||
|
ObjectAnimator fadeOut = ObjectAnimator.ofObject(
|
||||||
|
view,
|
||||||
|
"backgroundColor",
|
||||||
|
new ArgbEvaluator(),
|
||||||
|
highlightColor,
|
||||||
|
startColor
|
||||||
|
);
|
||||||
|
fadeOut.setDuration(BLINK_DURATION);
|
||||||
|
|
||||||
|
currentAnimator = new AnimatorSet();
|
||||||
|
// Create the sequence: fadeIn -> fadeOut -> (pause) -> fadeIn -> fadeOut.
|
||||||
|
AnimatorSet firstBlink = new AnimatorSet();
|
||||||
|
firstBlink.playSequentially(fadeIn, fadeOut);
|
||||||
|
AnimatorSet secondBlink = new AnimatorSet();
|
||||||
|
secondBlink.playSequentially(fadeIn.clone(), fadeOut.clone()); // Use clones for the second blink.
|
||||||
|
|
||||||
|
currentAnimator.play(secondBlink).after(firstBlink).after(PAUSE_BETWEEN_BLINKS);
|
||||||
|
currentAnimator.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively finds a preference by key in a preference group.
|
||||||
|
*/
|
||||||
|
protected Preference findPreferenceByKey(PreferenceGroup group, String key) {
|
||||||
|
if (group == null || TextUtils.isEmpty(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First search on current level.
|
||||||
|
for (int i = 0; i < group.getPreferenceCount(); i++) {
|
||||||
|
Preference pref = group.getPreference(i);
|
||||||
|
if (key.equals(pref.getKey())) {
|
||||||
|
return pref;
|
||||||
|
}
|
||||||
|
if (pref instanceof PreferenceGroup) {
|
||||||
|
Preference found = findPreferenceByKey((PreferenceGroup) pref, key);
|
||||||
|
if (found != null) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles preference click actions by invoking the preference's performClick method via reflection.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("all")
|
||||||
|
private void handlePreferenceClick(Preference preference) {
|
||||||
|
try {
|
||||||
|
if (preference instanceof CustomDialogListPreference listPref) {
|
||||||
|
BaseSearchResultItem.PreferenceSearchItem searchItem =
|
||||||
|
searchViewController.findSearchItemByPreference(preference);
|
||||||
|
if (searchItem != null && searchItem.isEntriesHighlightingApplied()) {
|
||||||
|
listPref.setHighlightedEntriesForDialog(searchItem.getHighlightedEntries());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Method m = Preference.class.getDeclaredMethod("performClick", PreferenceScreen.class);
|
||||||
|
m.setAccessible(true);
|
||||||
|
m.invoke(preference, fragment.getPreferenceScreenForSearch());
|
||||||
|
} catch (Exception e) {
|
||||||
|
Logger.printException(() -> "Failed to invoke performClick()", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if a preference has navigation capability (can open a new screen).
|
||||||
|
*/
|
||||||
|
boolean hasNavigationCapability(Preference preference) {
|
||||||
|
// PreferenceScreen always allows navigation.
|
||||||
|
if (preference instanceof PreferenceScreen) return true;
|
||||||
|
// UrlLinkPreference does not navigate to a new screen, it opens an external URL.
|
||||||
|
if (preference instanceof UrlLinkPreference) return false;
|
||||||
|
// Other group types that might have their own screens.
|
||||||
|
if (preference instanceof PreferenceGroup) {
|
||||||
|
// Check if it has its own fragment or intent.
|
||||||
|
return preference.getIntent() != null || preference.getFragment() != null;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,653 @@
|
|||||||
|
package app.revanced.extension.shared.settings.search;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceCategory;
|
||||||
|
import android.preference.PreferenceGroup;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.view.inputmethod.EditorInfo;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.SearchView;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
||||||
|
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||||
|
import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract controller for managing the overlay search view in ReVanced settings.
|
||||||
|
* Subclasses must implement app-specific preference handling.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public abstract class BaseSearchViewController {
|
||||||
|
protected SearchView searchView;
|
||||||
|
protected FrameLayout searchContainer;
|
||||||
|
protected FrameLayout overlayContainer;
|
||||||
|
protected final Toolbar toolbar;
|
||||||
|
protected final Activity activity;
|
||||||
|
protected final BasePreferenceFragment fragment;
|
||||||
|
protected final CharSequence originalTitle;
|
||||||
|
protected BaseSearchResultsAdapter searchResultsAdapter;
|
||||||
|
protected final List<BaseSearchResultItem> allSearchItems;
|
||||||
|
protected final List<BaseSearchResultItem> filteredSearchItems;
|
||||||
|
protected final Map<String, BaseSearchResultItem> keyToSearchItem;
|
||||||
|
protected final InputMethodManager inputMethodManager;
|
||||||
|
protected SearchHistoryManager searchHistoryManager;
|
||||||
|
protected boolean isSearchActive;
|
||||||
|
protected boolean isShowingSearchHistory;
|
||||||
|
|
||||||
|
protected static final int MAX_SEARCH_RESULTS = 50; // Maximum number of search results displayed.
|
||||||
|
|
||||||
|
protected static final int ID_REVANCED_SEARCH_VIEW = getResourceIdentifierOrThrow("revanced_search_view", "id");
|
||||||
|
protected static final int ID_REVANCED_SEARCH_VIEW_CONTAINER = getResourceIdentifierOrThrow("revanced_search_view_container", "id");
|
||||||
|
protected static final int ID_ACTION_SEARCH = getResourceIdentifierOrThrow("action_search", "id");
|
||||||
|
protected static final int ID_REVANCED_SETTINGS_FRAGMENTS = getResourceIdentifierOrThrow("revanced_settings_fragments", "id");
|
||||||
|
public static final int DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON =
|
||||||
|
getResourceIdentifierOrThrow("revanced_settings_search_icon", "drawable");
|
||||||
|
protected static final int MENU_REVANCED_SEARCH_MENU =
|
||||||
|
getResourceIdentifierOrThrow("revanced_search_menu", "menu");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new BaseSearchViewController instance.
|
||||||
|
*
|
||||||
|
* @param activity The activity hosting the search view.
|
||||||
|
* @param toolbar The toolbar containing the search action.
|
||||||
|
* @param fragment The preference fragment to manage search preferences.
|
||||||
|
*/
|
||||||
|
protected BaseSearchViewController(Activity activity, Toolbar toolbar, BasePreferenceFragment fragment) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.toolbar = toolbar;
|
||||||
|
this.fragment = fragment;
|
||||||
|
this.originalTitle = toolbar.getTitle();
|
||||||
|
this.allSearchItems = new ArrayList<>();
|
||||||
|
this.filteredSearchItems = new ArrayList<>();
|
||||||
|
this.keyToSearchItem = new HashMap<>();
|
||||||
|
this.inputMethodManager = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
this.isShowingSearchHistory = false;
|
||||||
|
|
||||||
|
// Initialize components
|
||||||
|
initializeSearchView();
|
||||||
|
initializeOverlayContainer();
|
||||||
|
initializeSearchHistoryManager();
|
||||||
|
setupToolbarMenu();
|
||||||
|
setupListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the search view with proper configurations, such as background, query hint, and RTL support.
|
||||||
|
*/
|
||||||
|
private void initializeSearchView() {
|
||||||
|
// Retrieve SearchView and container from XML.
|
||||||
|
searchView = activity.findViewById(ID_REVANCED_SEARCH_VIEW);
|
||||||
|
EditText searchEditText = searchView.findViewById(Utils.getResourceIdentifierOrThrow(
|
||||||
|
"android:id/search_src_text", null));
|
||||||
|
// Disable fullscreen keyboard mode.
|
||||||
|
searchEditText.setImeOptions(searchEditText.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
||||||
|
|
||||||
|
searchContainer = activity.findViewById(ID_REVANCED_SEARCH_VIEW_CONTAINER);
|
||||||
|
|
||||||
|
// Set background and query hint.
|
||||||
|
searchView.setBackground(createBackgroundDrawable());
|
||||||
|
searchView.setQueryHint(str("revanced_settings_search_hint"));
|
||||||
|
|
||||||
|
// Configure RTL support based on app language.
|
||||||
|
AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get();
|
||||||
|
if (Utils.isRightToLeftLocale(appLanguage.getLocale())) {
|
||||||
|
searchView.setTextDirection(View.TEXT_DIRECTION_RTL);
|
||||||
|
searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the overlay container for displaying search results and history.
|
||||||
|
*/
|
||||||
|
private void initializeOverlayContainer() {
|
||||||
|
// Create overlay container for search results and history.
|
||||||
|
overlayContainer = new FrameLayout(activity);
|
||||||
|
overlayContainer.setVisibility(View.GONE);
|
||||||
|
overlayContainer.setBackgroundColor(Utils.getAppBackgroundColor());
|
||||||
|
overlayContainer.setElevation(Utils.dipToPixels(8));
|
||||||
|
|
||||||
|
// Container for search results.
|
||||||
|
FrameLayout searchResultsContainer = new FrameLayout(activity);
|
||||||
|
searchResultsContainer.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
// Create a ListView for the results.
|
||||||
|
ListView searchResultsListView = new ListView(activity);
|
||||||
|
searchResultsListView.setDivider(null);
|
||||||
|
searchResultsListView.setDividerHeight(0);
|
||||||
|
searchResultsAdapter = createSearchResultsAdapter();
|
||||||
|
searchResultsListView.setAdapter(searchResultsAdapter);
|
||||||
|
|
||||||
|
// Add results list into container.
|
||||||
|
searchResultsContainer.addView(searchResultsListView, new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
// Add results container into overlay.
|
||||||
|
overlayContainer.addView(searchResultsContainer, new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
// Add overlay to the main content container.
|
||||||
|
FrameLayout mainContainer = activity.findViewById(ID_REVANCED_SETTINGS_FRAGMENTS);
|
||||||
|
if (mainContainer != null) {
|
||||||
|
FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT);
|
||||||
|
overlayParams.gravity = Gravity.TOP;
|
||||||
|
mainContainer.addView(overlayContainer, overlayParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the search history manager with the specified overlay container and listener.
|
||||||
|
*/
|
||||||
|
private void initializeSearchHistoryManager() {
|
||||||
|
searchHistoryManager = new SearchHistoryManager(activity, overlayContainer, query -> {
|
||||||
|
searchView.setQuery(query, true);
|
||||||
|
hideSearchHistory();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Abstract methods that subclasses must implement.
|
||||||
|
protected abstract BaseSearchResultsAdapter createSearchResultsAdapter();
|
||||||
|
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||||
|
protected abstract boolean isSpecialPreferenceGroup(Preference preference);
|
||||||
|
protected abstract void setupSpecialPreferenceListeners(BaseSearchResultItem item);
|
||||||
|
|
||||||
|
// Abstract interface for preference fragments.
|
||||||
|
public interface BasePreferenceFragment {
|
||||||
|
PreferenceScreen getPreferenceScreenForSearch();
|
||||||
|
android.view.View getView();
|
||||||
|
Activity getActivity();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines whether a preference should be included in the search index.
|
||||||
|
*
|
||||||
|
* @param preference The preference to evaluate.
|
||||||
|
* @param currentDepth The current depth in the preference hierarchy.
|
||||||
|
* @param includeDepth The maximum depth to include in the search index.
|
||||||
|
* @return True if the preference should be included, false otherwise.
|
||||||
|
*/
|
||||||
|
protected boolean shouldIncludePreference(Preference preference, int currentDepth, int includeDepth) {
|
||||||
|
return includeDepth <= currentDepth
|
||||||
|
&& !(preference instanceof PreferenceCategory)
|
||||||
|
&& !isSpecialPreferenceGroup(preference)
|
||||||
|
&& !(preference instanceof PreferenceScreen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the toolbar menu for the search action.
|
||||||
|
*/
|
||||||
|
protected void setupToolbarMenu() {
|
||||||
|
toolbar.inflateMenu(MENU_REVANCED_SEARCH_MENU);
|
||||||
|
toolbar.setOnMenuItemClickListener(item -> {
|
||||||
|
if (item.getItemId() == ID_ACTION_SEARCH && !isSearchActive) {
|
||||||
|
openSearch();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configures listeners for the search view and toolbar navigation.
|
||||||
|
*/
|
||||||
|
protected void setupListeners() {
|
||||||
|
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextSubmit(String query) {
|
||||||
|
try {
|
||||||
|
String queryTrimmed = query.trim();
|
||||||
|
if (!queryTrimmed.isEmpty()) {
|
||||||
|
searchHistoryManager.saveSearchQuery(queryTrimmed);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onQueryTextSubmit failure", ex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextChange(String newText) {
|
||||||
|
try {
|
||||||
|
Logger.printDebug(() -> "Search query: " + newText);
|
||||||
|
|
||||||
|
String trimmedText = newText.trim();
|
||||||
|
if (!isSearchActive) {
|
||||||
|
Logger.printDebug(() -> "Search is not active, skipping query processing");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmedText.isEmpty()) {
|
||||||
|
// If empty query: show history.
|
||||||
|
hideSearchResults();
|
||||||
|
showSearchHistory();
|
||||||
|
} else {
|
||||||
|
// If has search text: hide history and show search results.
|
||||||
|
hideSearchHistory();
|
||||||
|
filterAndShowResults(newText);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onQueryTextChange failure", ex);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Set navigation click listener.
|
||||||
|
toolbar.setNavigationOnClickListener(view -> {
|
||||||
|
if (isSearchActive) {
|
||||||
|
closeSearch();
|
||||||
|
} else {
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes search data by collecting all searchable preferences from the fragment.
|
||||||
|
* This method should be called after the preference fragment is fully loaded.
|
||||||
|
* Runs on the UI thread to ensure proper access to preference components.
|
||||||
|
*/
|
||||||
|
public void initializeSearchData() {
|
||||||
|
allSearchItems.clear();
|
||||||
|
keyToSearchItem.clear();
|
||||||
|
// Wait until fragment is properly initialized.
|
||||||
|
activity.runOnUiThread(() -> {
|
||||||
|
try {
|
||||||
|
PreferenceScreen screen = fragment.getPreferenceScreenForSearch();
|
||||||
|
if (screen != null) {
|
||||||
|
collectSearchablePreferences(screen);
|
||||||
|
for (BaseSearchResultItem item : allSearchItems) {
|
||||||
|
if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
|
||||||
|
String key = prefItem.preference.getKey();
|
||||||
|
if (key != null) {
|
||||||
|
keyToSearchItem.put(key, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setupPreferenceListeners();
|
||||||
|
Logger.printDebug(() -> "Collected " + allSearchItems.size() + " searchable preferences");
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "Failed to initialize search data", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up listeners for preferences to keep search results in sync when preference values change.
|
||||||
|
*/
|
||||||
|
protected void setupPreferenceListeners() {
|
||||||
|
for (BaseSearchResultItem item : allSearchItems) {
|
||||||
|
// Skip non-preference items.
|
||||||
|
if (!(item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem)) continue;
|
||||||
|
Preference pref = prefItem.preference;
|
||||||
|
|
||||||
|
if (pref instanceof ColorPickerPreference colorPref) {
|
||||||
|
colorPref.setOnColorChangeListener((prefKey, newColor) -> {
|
||||||
|
BaseSearchResultItem.PreferenceSearchItem searchItem =
|
||||||
|
(BaseSearchResultItem.PreferenceSearchItem) keyToSearchItem.get(prefKey);
|
||||||
|
if (searchItem != null) {
|
||||||
|
searchItem.setColor(newColor);
|
||||||
|
refreshSearchResults();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (pref instanceof CustomDialogListPreference listPref) {
|
||||||
|
listPref.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
BaseSearchResultItem.PreferenceSearchItem searchItem =
|
||||||
|
(BaseSearchResultItem.PreferenceSearchItem) keyToSearchItem.get(preference.getKey());
|
||||||
|
if (searchItem == null) return true;
|
||||||
|
|
||||||
|
int index = listPref.findIndexOfValue(newValue.toString());
|
||||||
|
if (index >= 0) {
|
||||||
|
// Check if a static summary is set.
|
||||||
|
boolean isStaticSummary = listPref.getStaticSummary() != null;
|
||||||
|
if (!isStaticSummary) {
|
||||||
|
// Only update summary if it is not static.
|
||||||
|
CharSequence newSummary = listPref.getEntries()[index];
|
||||||
|
listPref.setSummary(newSummary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listPref.clearHighlightedEntriesForDialog();
|
||||||
|
searchItem.refreshHighlighting();
|
||||||
|
refreshSearchResults();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let subclasses handle special preferences.
|
||||||
|
setupSpecialPreferenceListeners(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects searchable preferences from a preference group.
|
||||||
|
*/
|
||||||
|
protected void collectSearchablePreferences(PreferenceGroup group) {
|
||||||
|
collectSearchablePreferencesWithKeys(group, "", new ArrayList<>(), 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects searchable preferences with their navigation paths and keys.
|
||||||
|
*
|
||||||
|
* @param group The preference group to collect from.
|
||||||
|
* @param parentPath The navigation path of the parent group.
|
||||||
|
* @param parentKeys The keys of parent preferences.
|
||||||
|
* @param includeDepth The maximum depth to include in the search index.
|
||||||
|
* @param currentDepth The current depth in the preference hierarchy.
|
||||||
|
*/
|
||||||
|
protected void collectSearchablePreferencesWithKeys(PreferenceGroup group, String parentPath,
|
||||||
|
List<String> parentKeys, int includeDepth, int currentDepth) {
|
||||||
|
if (group == null) return;
|
||||||
|
|
||||||
|
for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
|
||||||
|
Preference preference = group.getPreference(i);
|
||||||
|
|
||||||
|
// Add to search results only if it is not a category, special group, or PreferenceScreen.
|
||||||
|
if (shouldIncludePreference(preference, currentDepth, includeDepth)) {
|
||||||
|
allSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(
|
||||||
|
preference, parentPath, parentKeys));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the preference is a group, recurse into it.
|
||||||
|
if (preference instanceof PreferenceGroup subGroup) {
|
||||||
|
String newPath = parentPath;
|
||||||
|
List<String> newKeys = new ArrayList<>(parentKeys);
|
||||||
|
|
||||||
|
// Append the group title to the path and save key for navigation.
|
||||||
|
if (!isSpecialPreferenceGroup(preference)
|
||||||
|
&& !(preference instanceof NoTitlePreferenceCategory)) {
|
||||||
|
CharSequence title = preference.getTitle();
|
||||||
|
if (!TextUtils.isEmpty(title)) {
|
||||||
|
newPath = TextUtils.isEmpty(parentPath)
|
||||||
|
? title.toString()
|
||||||
|
: parentPath + " > " + title;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add key for navigation if this is a PreferenceScreen or group with navigation capability.
|
||||||
|
String key = preference.getKey();
|
||||||
|
if (!TextUtils.isEmpty(key) && (preference instanceof PreferenceScreen
|
||||||
|
|| searchResultsAdapter.hasNavigationCapability(preference))) {
|
||||||
|
newKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
collectSearchablePreferencesWithKeys(subGroup, newPath, newKeys, includeDepth, currentDepth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters all search items based on the provided query and displays results in the overlay.
|
||||||
|
* Applies highlighting to matching text and shows a "no results" message if nothing matches.
|
||||||
|
*/
|
||||||
|
protected void filterAndShowResults(String query) {
|
||||||
|
hideSearchHistory();
|
||||||
|
// Keep track of the previously displayed items to clear their highlights.
|
||||||
|
List<BaseSearchResultItem> previouslyDisplayedItems = new ArrayList<>(filteredSearchItems);
|
||||||
|
|
||||||
|
filteredSearchItems.clear();
|
||||||
|
|
||||||
|
String queryLower = Utils.removePunctuationToLowercase(query);
|
||||||
|
Pattern queryPattern = Pattern.compile(Pattern.quote(queryLower), Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
// Clear highlighting only for items that were previously visible.
|
||||||
|
// This avoids iterating through all items on every keystroke during filtering.
|
||||||
|
for (BaseSearchResultItem item : previouslyDisplayedItems) {
|
||||||
|
item.clearHighlighting();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect matched items first.
|
||||||
|
List<BaseSearchResultItem> matched = new ArrayList<>();
|
||||||
|
int matchCount = 0;
|
||||||
|
for (BaseSearchResultItem item : allSearchItems) {
|
||||||
|
if (matchCount >= MAX_SEARCH_RESULTS) break; // Stop after collecting max results.
|
||||||
|
if (item.matchesQuery(queryLower)) {
|
||||||
|
item.applyHighlighting(queryPattern);
|
||||||
|
matched.add(item);
|
||||||
|
matchCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build filteredSearchItems, inserting parent enablers for disabled dependents.
|
||||||
|
Set<String> addedParentKeys = new HashSet<>(2 * matched.size());
|
||||||
|
for (BaseSearchResultItem item : matched) {
|
||||||
|
if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
|
||||||
|
String key = prefItem.preference.getKey();
|
||||||
|
Setting<?> setting = (key != null) ? Setting.getSettingFromPath(key) : null;
|
||||||
|
if (setting != null && !setting.isAvailable()) {
|
||||||
|
List<Setting<?>> parentSettings = setting.getParentSettings();
|
||||||
|
for (Setting<?> parentSetting : parentSettings) {
|
||||||
|
BaseSearchResultItem parentItem = keyToSearchItem.get(parentSetting.key);
|
||||||
|
if (parentItem != null && !addedParentKeys.contains(parentSetting.key)) {
|
||||||
|
if (!parentItem.matchesQuery(queryLower)) {
|
||||||
|
// Apply highlighting to parent items even if they don't match the query.
|
||||||
|
// This ensures they get their current effective summary calculated.
|
||||||
|
parentItem.applyHighlighting(queryPattern);
|
||||||
|
filteredSearchItems.add(parentItem);
|
||||||
|
}
|
||||||
|
addedParentKeys.add(parentSetting.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
filteredSearchItems.add(item);
|
||||||
|
if (key != null) {
|
||||||
|
addedParentKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filteredSearchItems.isEmpty()) {
|
||||||
|
//noinspection ComparatorCombinators
|
||||||
|
Collections.sort(filteredSearchItems, (o1, o2) ->
|
||||||
|
o1.navigationPath.compareTo(o2.navigationPath)
|
||||||
|
);
|
||||||
|
List<BaseSearchResultItem> displayItems = new ArrayList<>();
|
||||||
|
String currentPath = null;
|
||||||
|
for (BaseSearchResultItem item : filteredSearchItems) {
|
||||||
|
if (!item.navigationPath.equals(currentPath)) {
|
||||||
|
BaseSearchResultItem header = new BaseSearchResultItem.GroupHeaderItem(item.navigationPath, item.navigationKeys);
|
||||||
|
displayItems.add(header);
|
||||||
|
currentPath = item.navigationPath;
|
||||||
|
}
|
||||||
|
displayItems.add(item);
|
||||||
|
}
|
||||||
|
filteredSearchItems.clear();
|
||||||
|
filteredSearchItems.addAll(displayItems);
|
||||||
|
}
|
||||||
|
// Show "No results found" if search results are empty.
|
||||||
|
if (filteredSearchItems.isEmpty()) {
|
||||||
|
Preference noResultsPreference = new Preference(activity);
|
||||||
|
noResultsPreference.setKey("no_results_placeholder");
|
||||||
|
noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
|
||||||
|
noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
|
||||||
|
noResultsPreference.setSelectable(false);
|
||||||
|
noResultsPreference.setIcon(DRAWABLE_REVANCED_SETTINGS_SEARCH_ICON);
|
||||||
|
filteredSearchItems.add(new BaseSearchResultItem.PreferenceSearchItem(noResultsPreference, "", Collections.emptyList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResultsAdapter.notifyDataSetChanged();
|
||||||
|
overlayContainer.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the search interface by showing the search view and hiding the menu item.
|
||||||
|
* Configures the UI for search mode, shows the keyboard, and displays search suggestions.
|
||||||
|
*/
|
||||||
|
protected void openSearch() {
|
||||||
|
isSearchActive = true;
|
||||||
|
toolbar.getMenu().findItem(ID_ACTION_SEARCH).setVisible(false);
|
||||||
|
toolbar.setTitle("");
|
||||||
|
searchContainer.setVisibility(View.VISIBLE);
|
||||||
|
searchView.requestFocus();
|
||||||
|
// Configure soft input mode to adjust layout and show keyboard.
|
||||||
|
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN
|
||||||
|
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||||
|
inputMethodManager.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT);
|
||||||
|
// Always show search history when opening search.
|
||||||
|
showSearchHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the search interface and restores the normal UI state.
|
||||||
|
* Hides the overlay, clears search results, dismisses the keyboard, and removes highlighting.
|
||||||
|
*/
|
||||||
|
public void closeSearch() {
|
||||||
|
isSearchActive = false;
|
||||||
|
isShowingSearchHistory = false;
|
||||||
|
|
||||||
|
searchHistoryManager.hideSearchHistoryContainer();
|
||||||
|
overlayContainer.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
filteredSearchItems.clear();
|
||||||
|
|
||||||
|
searchContainer.setVisibility(View.GONE);
|
||||||
|
toolbar.getMenu().findItem(ID_ACTION_SEARCH).setVisible(true);
|
||||||
|
toolbar.setTitle(originalTitle);
|
||||||
|
searchView.setQuery("", false);
|
||||||
|
// Hide keyboard and reset soft input mode.
|
||||||
|
inputMethodManager.hideSoftInputFromWindow(searchView.getWindowToken(), 0);
|
||||||
|
activity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_PAN);
|
||||||
|
// Clear highlighting for all search items.
|
||||||
|
for (BaseSearchResultItem item : allSearchItems) {
|
||||||
|
item.clearHighlighting();
|
||||||
|
}
|
||||||
|
|
||||||
|
searchResultsAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the search history if enabled.
|
||||||
|
*/
|
||||||
|
protected void showSearchHistory() {
|
||||||
|
if (searchHistoryManager.isSearchHistoryEnabled()) {
|
||||||
|
overlayContainer.setVisibility(View.VISIBLE);
|
||||||
|
searchHistoryManager.showSearchHistory();
|
||||||
|
isShowingSearchHistory = true;
|
||||||
|
} else {
|
||||||
|
hideAllOverlays();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the search history container.
|
||||||
|
*/
|
||||||
|
protected void hideSearchHistory() {
|
||||||
|
searchHistoryManager.hideSearchHistoryContainer();
|
||||||
|
isShowingSearchHistory = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides all overlay containers, including search results and history.
|
||||||
|
*/
|
||||||
|
protected void hideAllOverlays() {
|
||||||
|
hideSearchHistory();
|
||||||
|
hideSearchResults();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the search results overlay and clears the filtered results.
|
||||||
|
*/
|
||||||
|
protected void hideSearchResults() {
|
||||||
|
overlayContainer.setVisibility(View.GONE);
|
||||||
|
filteredSearchItems.clear();
|
||||||
|
searchResultsAdapter.notifyDataSetChanged();
|
||||||
|
for (BaseSearchResultItem item : allSearchItems) {
|
||||||
|
item.clearHighlighting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the search results display if the search is active and history is not shown.
|
||||||
|
*/
|
||||||
|
protected void refreshSearchResults() {
|
||||||
|
if (isSearchActive && !isShowingSearchHistory) {
|
||||||
|
searchResultsAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a search item corresponding to the given preference.
|
||||||
|
*
|
||||||
|
* @param preference The preference to find a search item for.
|
||||||
|
* @return The corresponding PreferenceSearchItem, or null if not found.
|
||||||
|
*/
|
||||||
|
public BaseSearchResultItem.PreferenceSearchItem findSearchItemByPreference(Preference preference) {
|
||||||
|
// First, search in filtered results.
|
||||||
|
for (BaseSearchResultItem item : filteredSearchItems) {
|
||||||
|
if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
|
||||||
|
if (prefItem.preference == preference) {
|
||||||
|
return prefItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If not found, search in all items.
|
||||||
|
for (BaseSearchResultItem item : allSearchItems) {
|
||||||
|
if (item instanceof BaseSearchResultItem.PreferenceSearchItem prefItem) {
|
||||||
|
if (prefItem.preference == preference) {
|
||||||
|
return prefItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the background color for search view components based on current theme.
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
|
public static int getSearchViewBackground() {
|
||||||
|
return Utils.adjustColorBrightness(Utils.getDialogBackgroundColor(), Utils.isDarkModeEnabled() ? 1.11f : 0.95f);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a rounded background drawable for the main search view.
|
||||||
|
*/
|
||||||
|
protected static GradientDrawable createBackgroundDrawable() {
|
||||||
|
GradientDrawable background = new GradientDrawable();
|
||||||
|
background.setShape(GradientDrawable.RECTANGLE);
|
||||||
|
background.setCornerRadius(Utils.dipToPixels(28));
|
||||||
|
background.setColor(getSearchViewBackground());
|
||||||
|
return background;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return if a search is currently active.
|
||||||
|
*/
|
||||||
|
public boolean isSearchActive() {
|
||||||
|
return isSearchActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,377 @@
|
|||||||
|
package app.revanced.extension.shared.settings.search;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
import static app.revanced.extension.shared.settings.BaseSettings.SETTINGS_SEARCH_ENTRIES;
|
||||||
|
import static app.revanced.extension.shared.settings.BaseSettings.SETTINGS_SEARCH_HISTORY;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Pair;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.settings.preference.BulletPointPreference;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manager for search history functionality.
|
||||||
|
*/
|
||||||
|
public class SearchHistoryManager {
|
||||||
|
/**
|
||||||
|
* Interface for handling history item selection.
|
||||||
|
*/
|
||||||
|
private static final int MAX_HISTORY_SIZE = 5; // Maximum history items stored.
|
||||||
|
|
||||||
|
private static final int ID_CLEAR_HISTORY_BUTTON = getResourceIdentifierOrThrow(
|
||||||
|
"clear_history_button", "id");
|
||||||
|
private static final int ID_HISTORY_TEXT = getResourceIdentifierOrThrow(
|
||||||
|
"history_text", "id");
|
||||||
|
private static final int ID_DELETE_ICON = getResourceIdentifierOrThrow(
|
||||||
|
"delete_icon", "id");
|
||||||
|
private static final int ID_EMPTY_HISTORY_TITLE = getResourceIdentifierOrThrow(
|
||||||
|
"empty_history_title", "id");
|
||||||
|
private static final int ID_EMPTY_HISTORY_SUMMARY = getResourceIdentifierOrThrow(
|
||||||
|
"empty_history_summary", "id");
|
||||||
|
private static final int ID_SEARCH_HISTORY_HEADER = getResourceIdentifierOrThrow(
|
||||||
|
"search_history_header", "id");
|
||||||
|
private static final int ID_SEARCH_TIPS_SUMMARY = getResourceIdentifierOrThrow(
|
||||||
|
"revanced_settings_search_tips_summary", "id");
|
||||||
|
private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN = getResourceIdentifierOrThrow(
|
||||||
|
"revanced_preference_search_history_screen", "layout");
|
||||||
|
private static final int LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM = getResourceIdentifierOrThrow(
|
||||||
|
"revanced_preference_search_history_item", "layout");
|
||||||
|
private static final int ID_SEARCH_HISTORY_LIST = getResourceIdentifierOrThrow(
|
||||||
|
"search_history_list", "id");
|
||||||
|
|
||||||
|
private final Deque<String> searchHistory;
|
||||||
|
private final Activity activity;
|
||||||
|
private final SearchHistoryAdapter searchHistoryAdapter;
|
||||||
|
private final boolean showSettingsSearchHistory;
|
||||||
|
private final FrameLayout searchHistoryContainer;
|
||||||
|
|
||||||
|
public interface OnSelectHistoryItemListener {
|
||||||
|
void onSelectHistoryItem(String query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor for SearchHistoryManager.
|
||||||
|
*
|
||||||
|
* @param activity The parent activity.
|
||||||
|
* @param overlayContainer The overlay container to hold the search history container.
|
||||||
|
* @param onSelectHistoryItemAction Callback for when a history item is selected.
|
||||||
|
*/
|
||||||
|
SearchHistoryManager(Activity activity, FrameLayout overlayContainer,
|
||||||
|
OnSelectHistoryItemListener onSelectHistoryItemAction) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.showSettingsSearchHistory = SETTINGS_SEARCH_HISTORY.get();
|
||||||
|
this.searchHistory = new LinkedList<>();
|
||||||
|
|
||||||
|
// Initialize search history from settings.
|
||||||
|
if (showSettingsSearchHistory) {
|
||||||
|
String entries = SETTINGS_SEARCH_ENTRIES.get();
|
||||||
|
if (!entries.isBlank()) {
|
||||||
|
searchHistory.addAll(Arrays.asList(entries.split("\n")));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clear old saved history if the feature is disabled.
|
||||||
|
SETTINGS_SEARCH_ENTRIES.resetToDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create search history container.
|
||||||
|
this.searchHistoryContainer = new FrameLayout(activity);
|
||||||
|
searchHistoryContainer.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
// Inflate search history layout.
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(activity);
|
||||||
|
View historyView = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_SCREEN, searchHistoryContainer, false);
|
||||||
|
searchHistoryContainer.addView(historyView, new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT));
|
||||||
|
|
||||||
|
// Add history container to overlay.
|
||||||
|
FrameLayout.LayoutParams overlayParams = new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT);
|
||||||
|
overlayParams.gravity = Gravity.TOP;
|
||||||
|
overlayContainer.addView(searchHistoryContainer, overlayParams);
|
||||||
|
|
||||||
|
// Find the LinearLayout for the history list within the container.
|
||||||
|
LinearLayout searchHistoryListView = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_LIST);
|
||||||
|
if (searchHistoryListView == null) {
|
||||||
|
throw new IllegalStateException("Search history list view not found in container");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up history adapter. Use a copy of the search history.
|
||||||
|
this.searchHistoryAdapter = new SearchHistoryAdapter(activity, searchHistoryListView,
|
||||||
|
new ArrayList<>(searchHistory), onSelectHistoryItemAction);
|
||||||
|
|
||||||
|
// Set up clear history button.
|
||||||
|
TextView clearHistoryButton = searchHistoryContainer.findViewById(ID_CLEAR_HISTORY_BUTTON);
|
||||||
|
clearHistoryButton.setOnClickListener(v -> createAndShowDialog(
|
||||||
|
str("revanced_settings_search_clear_history"),
|
||||||
|
str("revanced_settings_search_clear_history_message"),
|
||||||
|
this::clearAllSearchHistory
|
||||||
|
));
|
||||||
|
|
||||||
|
// Set up search tips summary.
|
||||||
|
CharSequence text = BulletPointPreference.formatIntoBulletPoints(
|
||||||
|
str("revanced_settings_search_tips_summary"));
|
||||||
|
TextView tipsSummary = historyView.findViewById(ID_SEARCH_TIPS_SUMMARY);
|
||||||
|
tipsSummary.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows search history screen - either with history items or empty history message.
|
||||||
|
*/
|
||||||
|
public void showSearchHistory() {
|
||||||
|
if (!showSettingsSearchHistory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all view elements.
|
||||||
|
TextView emptyHistoryTitle = searchHistoryContainer.findViewById(ID_EMPTY_HISTORY_TITLE);
|
||||||
|
TextView emptyHistorySummary = searchHistoryContainer.findViewById(ID_EMPTY_HISTORY_SUMMARY);
|
||||||
|
TextView historyHeader = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_HEADER);
|
||||||
|
LinearLayout historyList = searchHistoryContainer.findViewById(ID_SEARCH_HISTORY_LIST);
|
||||||
|
TextView clearHistoryButton = searchHistoryContainer.findViewById(ID_CLEAR_HISTORY_BUTTON);
|
||||||
|
|
||||||
|
if (searchHistory.isEmpty()) {
|
||||||
|
// Show empty history state.
|
||||||
|
showEmptyHistoryViews(emptyHistoryTitle, emptyHistorySummary);
|
||||||
|
hideHistoryViews(historyHeader, historyList, clearHistoryButton);
|
||||||
|
} else {
|
||||||
|
// Show history list state.
|
||||||
|
hideEmptyHistoryViews(emptyHistoryTitle, emptyHistorySummary);
|
||||||
|
showHistoryViews(historyHeader, historyList, clearHistoryButton);
|
||||||
|
|
||||||
|
// Update adapter with current history.
|
||||||
|
searchHistoryAdapter.clear();
|
||||||
|
searchHistoryAdapter.addAll(searchHistory);
|
||||||
|
searchHistoryAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the search history container.
|
||||||
|
showSearchHistoryContainer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a search query to the history, maintaining the size limit.
|
||||||
|
*/
|
||||||
|
public void saveSearchQuery(String query) {
|
||||||
|
if (!showSettingsSearchHistory) return;
|
||||||
|
|
||||||
|
searchHistory.remove(query); // Remove if already exists to update position.
|
||||||
|
searchHistory.addFirst(query); // Add to the most recent.
|
||||||
|
|
||||||
|
// Remove extra old entries.
|
||||||
|
while (searchHistory.size() > MAX_HISTORY_SIZE) {
|
||||||
|
String last = searchHistory.removeLast();
|
||||||
|
Logger.printDebug(() -> "Removing search history query: " + last);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSearchHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the search history to shared preferences.
|
||||||
|
*/
|
||||||
|
protected void saveSearchHistory() {
|
||||||
|
Logger.printDebug(() -> "Saving search history: " + searchHistory);
|
||||||
|
SETTINGS_SEARCH_ENTRIES.save(String.join("\n", searchHistory));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a search query from the history.
|
||||||
|
*/
|
||||||
|
public void removeSearchQuery(String query) {
|
||||||
|
searchHistory.remove(query);
|
||||||
|
saveSearchHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all search history.
|
||||||
|
*/
|
||||||
|
public void clearAllSearchHistory() {
|
||||||
|
searchHistory.clear();
|
||||||
|
saveSearchHistory();
|
||||||
|
searchHistoryAdapter.clear();
|
||||||
|
searchHistoryAdapter.notifyDataSetChanged();
|
||||||
|
showSearchHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if search history feature is enabled.
|
||||||
|
*/
|
||||||
|
public boolean isSearchHistoryEnabled() {
|
||||||
|
return showSettingsSearchHistory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the search history container and overlay.
|
||||||
|
*/
|
||||||
|
public void showSearchHistoryContainer() {
|
||||||
|
searchHistoryContainer.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides the search history container.
|
||||||
|
*/
|
||||||
|
public void hideSearchHistoryContainer() {
|
||||||
|
searchHistoryContainer.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to show empty history views.
|
||||||
|
*/
|
||||||
|
protected void showEmptyHistoryViews(TextView emptyTitle, TextView emptySummary) {
|
||||||
|
emptyTitle.setVisibility(View.VISIBLE);
|
||||||
|
emptyTitle.setText(str("revanced_settings_search_empty_history_title"));
|
||||||
|
emptySummary.setVisibility(View.VISIBLE);
|
||||||
|
emptySummary.setText(str("revanced_settings_search_empty_history_summary"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to hide empty history views.
|
||||||
|
*/
|
||||||
|
protected void hideEmptyHistoryViews(TextView emptyTitle, TextView emptySummary) {
|
||||||
|
emptyTitle.setVisibility(View.GONE);
|
||||||
|
emptySummary.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to show history list views.
|
||||||
|
*/
|
||||||
|
protected void showHistoryViews(TextView header, LinearLayout list, TextView clearButton) {
|
||||||
|
header.setVisibility(View.VISIBLE);
|
||||||
|
list.setVisibility(View.VISIBLE);
|
||||||
|
clearButton.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to hide history list views.
|
||||||
|
*/
|
||||||
|
protected void hideHistoryViews(TextView header, LinearLayout list, TextView clearButton) {
|
||||||
|
header.setVisibility(View.GONE);
|
||||||
|
list.setVisibility(View.GONE);
|
||||||
|
clearButton.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and shows a dialog with the specified title, message, and confirmation action.
|
||||||
|
*
|
||||||
|
* @param title The title of the dialog.
|
||||||
|
* @param message The message to display in the dialog.
|
||||||
|
* @param confirmAction The action to perform when the dialog is confirmed.
|
||||||
|
*/
|
||||||
|
protected void createAndShowDialog(String title, String message, Runnable confirmAction) {
|
||||||
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
|
activity,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
confirmAction,
|
||||||
|
() -> {},
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
Dialog dialog = dialogPair.first;
|
||||||
|
dialog.setCancelable(true);
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom adapter for search history items.
|
||||||
|
*/
|
||||||
|
protected class SearchHistoryAdapter {
|
||||||
|
protected final Collection<String> history;
|
||||||
|
protected final LayoutInflater inflater;
|
||||||
|
protected final LinearLayout container;
|
||||||
|
protected final OnSelectHistoryItemListener onSelectHistoryItemListener;
|
||||||
|
|
||||||
|
public SearchHistoryAdapter(Context context, LinearLayout container, Collection<String> history,
|
||||||
|
OnSelectHistoryItemListener listener) {
|
||||||
|
this.history = history;
|
||||||
|
this.inflater = LayoutInflater.from(context);
|
||||||
|
this.container = container;
|
||||||
|
this.onSelectHistoryItemListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the container with current history items.
|
||||||
|
*/
|
||||||
|
public void notifyDataSetChanged() {
|
||||||
|
container.removeAllViews();
|
||||||
|
for (String query : history) {
|
||||||
|
View view = inflater.inflate(LAYOUT_REVANCED_PREFERENCE_SEARCH_HISTORY_ITEM, container, false);
|
||||||
|
|
||||||
|
TextView historyText = view.findViewById(ID_HISTORY_TEXT);
|
||||||
|
ImageView deleteIcon = view.findViewById(ID_DELETE_ICON);
|
||||||
|
|
||||||
|
historyText.setText(query);
|
||||||
|
|
||||||
|
// Set click listener for main item (select query).
|
||||||
|
view.setOnClickListener(v -> onSelectHistoryItemListener.onSelectHistoryItem(query));
|
||||||
|
|
||||||
|
// Set click listener for delete icon.
|
||||||
|
deleteIcon.setOnClickListener(v -> createAndShowDialog(
|
||||||
|
query,
|
||||||
|
str("revanced_settings_search_remove_message"),
|
||||||
|
() -> {
|
||||||
|
removeSearchQuery(query);
|
||||||
|
remove(query);
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
container.addView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all views from the container and history list.
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
history.clear();
|
||||||
|
container.removeAllViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds all provided history items to the container.
|
||||||
|
*/
|
||||||
|
public void addAll(Collection<String> items) {
|
||||||
|
history.addAll(items);
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a query from the history and updates the container.
|
||||||
|
*/
|
||||||
|
public void remove(String query) {
|
||||||
|
history.remove(query);
|
||||||
|
if (history.isEmpty()) {
|
||||||
|
// If history is now empty, show the empty history state.
|
||||||
|
showSearchHistory();
|
||||||
|
} else {
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package app.revanced.extension.shared.ui;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.adjustColorBrightness;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
import static app.revanced.extension.shared.Utils.getAppBackgroundColor;
|
||||||
|
import static app.revanced.extension.shared.Utils.isDarkModeEnabled;
|
||||||
|
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.DISABLED_ALPHA;
|
||||||
|
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
|
public class ColorDot {
|
||||||
|
private static final int STROKE_WIDTH = dipToPixels(1.5f); // Stroke width in dp.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a circular drawable with a main fill and a stroke.
|
||||||
|
* Stroke adapts to dark/light theme and transparency, applied only when color is transparent or matches app background.
|
||||||
|
*/
|
||||||
|
public static GradientDrawable createColorDotDrawable(@ColorInt int color) {
|
||||||
|
final boolean isDarkTheme = isDarkModeEnabled();
|
||||||
|
final boolean isTransparent = Color.alpha(color) == 0;
|
||||||
|
final int opaqueColor = color | 0xFF000000;
|
||||||
|
final int appBackground = getAppBackgroundColor();
|
||||||
|
final int strokeColor;
|
||||||
|
final int strokeWidth;
|
||||||
|
|
||||||
|
// Determine stroke color.
|
||||||
|
if (isTransparent || (opaqueColor == appBackground)) {
|
||||||
|
final int baseColor = isTransparent ? appBackground : opaqueColor;
|
||||||
|
strokeColor = adjustColorBrightness(baseColor, isDarkTheme ? 1.2f : 0.8f);
|
||||||
|
strokeWidth = STROKE_WIDTH;
|
||||||
|
} else {
|
||||||
|
strokeColor = 0;
|
||||||
|
strokeWidth = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create circular drawable with conditional stroke.
|
||||||
|
GradientDrawable circle = new GradientDrawable();
|
||||||
|
circle.setShape(GradientDrawable.OVAL);
|
||||||
|
circle.setColor(color);
|
||||||
|
circle.setStroke(strokeWidth, strokeColor);
|
||||||
|
|
||||||
|
return circle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the color dot drawable to the target view.
|
||||||
|
*/
|
||||||
|
public static void applyColorDot(View targetView, @ColorInt int color, boolean enabled) {
|
||||||
|
if (targetView == null) return;
|
||||||
|
targetView.setBackground(createColorDotDrawable(color));
|
||||||
|
targetView.setAlpha(enabled ? 1.0f : DISABLED_ALPHA);
|
||||||
|
if (!isDarkModeEnabled()) {
|
||||||
|
targetView.setClipToOutline(true);
|
||||||
|
targetView.setElevation(dipToPixels(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,472 @@
|
|||||||
|
package app.revanced.extension.shared.ui;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.ShapeDrawable;
|
||||||
|
import android.graphics.drawable.shapes.RoundRectShape;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.util.Pair;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility class for creating a customizable dialog with a title, message or EditText, and up to three buttons (OK, Cancel, Neutral).
|
||||||
|
* The dialog supports themed colors, rounded corners, and dynamic button layout based on screen width. It is dismissible by default.
|
||||||
|
*/
|
||||||
|
public class CustomDialog {
|
||||||
|
private final Context context;
|
||||||
|
private final Dialog dialog;
|
||||||
|
private final LinearLayout mainLayout;
|
||||||
|
private final int dip4, dip8, dip16, dip24, dip36;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a custom dialog with a styled layout, including a title, message, buttons, and an optional EditText.
|
||||||
|
* The dialog's appearance adapts to the app's dark mode setting, with rounded corners and customizable button actions.
|
||||||
|
* Buttons adjust dynamically to their text content and are arranged in a single row if they fit within 80% of the
|
||||||
|
* screen width, with the Neutral button aligned to the left and OK/Cancel buttons centered on the right.
|
||||||
|
* If buttons do not fit, each is placed on a separate row, all aligned to the right.
|
||||||
|
*
|
||||||
|
* @param context Context used to create the dialog.
|
||||||
|
* @param title Title text of the dialog.
|
||||||
|
* @param message Message text of the dialog (supports Spanned for HTML), or null if replaced by EditText.
|
||||||
|
* @param editText EditText to include in the dialog, or null if no EditText is needed.
|
||||||
|
* @param okButtonText OK button text, or null to use the default "OK" string.
|
||||||
|
* @param onOkClick Action to perform when the OK button is clicked.
|
||||||
|
* @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed.
|
||||||
|
* @param neutralButtonText Neutral button text, or null if no Neutral button is needed.
|
||||||
|
* @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed.
|
||||||
|
* @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked.
|
||||||
|
* @return The Dialog and its main LinearLayout container.
|
||||||
|
*/
|
||||||
|
public static Pair<Dialog, LinearLayout> create(Context context, String title, CharSequence message,
|
||||||
|
@Nullable EditText editText, String okButtonText,
|
||||||
|
Runnable onOkClick, Runnable onCancelClick,
|
||||||
|
@Nullable String neutralButtonText,
|
||||||
|
@Nullable Runnable onNeutralClick,
|
||||||
|
boolean dismissDialogOnNeutralClick) {
|
||||||
|
Logger.printDebug(() -> "Creating custom dialog with title: " + title);
|
||||||
|
CustomDialog customDialog = new CustomDialog(context, title, message, editText,
|
||||||
|
okButtonText, onOkClick, onCancelClick,
|
||||||
|
neutralButtonText, onNeutralClick, dismissDialogOnNeutralClick);
|
||||||
|
return new Pair<>(customDialog.dialog, customDialog.mainLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes a custom dialog with the specified parameters.
|
||||||
|
*
|
||||||
|
* @param context Context used to create the dialog.
|
||||||
|
* @param title Title text of the dialog.
|
||||||
|
* @param message Message text of the dialog, or null if replaced by EditText.
|
||||||
|
* @param editText EditText to include in the dialog, or null if no EditText is needed.
|
||||||
|
* @param okButtonText OK button text, or null to use the default "OK" string.
|
||||||
|
* @param onOkClick Action to perform when the OK button is clicked.
|
||||||
|
* @param onCancelClick Action to perform when the Cancel button is clicked, or null if no Cancel button is needed.
|
||||||
|
* @param neutralButtonText Neutral button text, or null if no Neutral button is needed.
|
||||||
|
* @param onNeutralClick Action to perform when the Neutral button is clicked, or null if no Neutral button is needed.
|
||||||
|
* @param dismissDialogOnNeutralClick If the dialog should be dismissed when the Neutral button is clicked.
|
||||||
|
*/
|
||||||
|
private CustomDialog(Context context, String title, CharSequence message, @Nullable EditText editText,
|
||||||
|
String okButtonText, Runnable onOkClick, Runnable onCancelClick,
|
||||||
|
@Nullable String neutralButtonText, @Nullable Runnable onNeutralClick,
|
||||||
|
boolean dismissDialogOnNeutralClick) {
|
||||||
|
this.context = context;
|
||||||
|
this.dialog = new Dialog(context);
|
||||||
|
this.dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
|
||||||
|
|
||||||
|
// Preset size constants.
|
||||||
|
dip4 = dipToPixels(4);
|
||||||
|
dip8 = dipToPixels(8);
|
||||||
|
dip16 = dipToPixels(16);
|
||||||
|
dip24 = dipToPixels(24);
|
||||||
|
dip36 = dipToPixels(36);
|
||||||
|
|
||||||
|
// Create main layout.
|
||||||
|
mainLayout = createMainLayout();
|
||||||
|
addTitle(title);
|
||||||
|
addContent(message, editText);
|
||||||
|
addButtons(okButtonText, onOkClick, onCancelClick, neutralButtonText, onNeutralClick, dismissDialogOnNeutralClick);
|
||||||
|
|
||||||
|
// Set dialog content and window attributes.
|
||||||
|
dialog.setContentView(mainLayout);
|
||||||
|
Window window = dialog.getWindow();
|
||||||
|
if (window != null) {
|
||||||
|
Utils.setDialogWindowParameters(window, Gravity.CENTER, 0, 90, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the main layout for the dialog with vertical orientation and rounded corners.
|
||||||
|
*
|
||||||
|
* @return The configured LinearLayout for the dialog.
|
||||||
|
*/
|
||||||
|
private LinearLayout createMainLayout() {
|
||||||
|
LinearLayout layout = new LinearLayout(context);
|
||||||
|
layout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
layout.setPadding(dip24, dip16, dip24, dip24);
|
||||||
|
|
||||||
|
// Set rounded rectangle background.
|
||||||
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||||
|
Utils.createCornerRadii(28), null, null));
|
||||||
|
// Dialog background.
|
||||||
|
background.getPaint().setColor(Utils.getDialogBackgroundColor());
|
||||||
|
layout.setBackground(background);
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a title to the dialog if provided.
|
||||||
|
*
|
||||||
|
* @param title The title text to display.
|
||||||
|
*/
|
||||||
|
private void addTitle(String title) {
|
||||||
|
if (TextUtils.isEmpty(title)) return;
|
||||||
|
|
||||||
|
TextView titleView = new TextView(context);
|
||||||
|
titleView.setText(title);
|
||||||
|
titleView.setTypeface(Typeface.DEFAULT_BOLD);
|
||||||
|
titleView.setTextSize(18);
|
||||||
|
titleView.setTextColor(Utils.getAppForegroundColor());
|
||||||
|
titleView.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
params.setMargins(0, 0, 0, dip16);
|
||||||
|
titleView.setLayoutParams(params);
|
||||||
|
|
||||||
|
mainLayout.addView(titleView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a message or EditText to the dialog within a ScrollView.
|
||||||
|
*
|
||||||
|
* @param message The message text to display (supports Spanned for HTML), or null if replaced by EditText.
|
||||||
|
* @param editText The EditText to include, or null if no EditText is needed.
|
||||||
|
*/
|
||||||
|
private void addContent(CharSequence message, @Nullable EditText editText) {
|
||||||
|
// Create content container (message/EditText) inside a ScrollView only if message or editText is provided.
|
||||||
|
if (message == null && editText == null) return;
|
||||||
|
|
||||||
|
ScrollView scrollView = new ScrollView(context);
|
||||||
|
// Disable the vertical scrollbar.
|
||||||
|
scrollView.setVerticalScrollBarEnabled(false);
|
||||||
|
scrollView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||||
|
|
||||||
|
LinearLayout contentContainer = new LinearLayout(context);
|
||||||
|
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
scrollView.addView(contentContainer);
|
||||||
|
|
||||||
|
// EditText (if provided).
|
||||||
|
if (editText != null) {
|
||||||
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||||
|
Utils.createCornerRadii(10), null, null));
|
||||||
|
background.getPaint().setColor(Utils.getEditTextBackground());
|
||||||
|
scrollView.setPadding(dip8, dip8, dip8, dip8);
|
||||||
|
scrollView.setBackground(background);
|
||||||
|
scrollView.setClipToOutline(true);
|
||||||
|
|
||||||
|
// Remove EditText from its current parent, if any.
|
||||||
|
ViewGroup parent = (ViewGroup) editText.getParent();
|
||||||
|
if (parent != null) parent.removeView(editText);
|
||||||
|
// Style the EditText to match the dialog theme.
|
||||||
|
editText.setTextColor(Utils.getAppForegroundColor());
|
||||||
|
editText.setBackgroundColor(Color.TRANSPARENT);
|
||||||
|
editText.setPadding(0, 0, 0, 0);
|
||||||
|
contentContainer.addView(editText, new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
// Message (if not replaced by EditText).
|
||||||
|
} else {
|
||||||
|
TextView messageView = new TextView(context);
|
||||||
|
// Supports Spanned (HTML).
|
||||||
|
messageView.setText(message);
|
||||||
|
messageView.setTextSize(16);
|
||||||
|
messageView.setTextColor(Utils.getAppForegroundColor());
|
||||||
|
// Enable HTML link clicking if the message contains links.
|
||||||
|
if (message instanceof Spanned) {
|
||||||
|
messageView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
}
|
||||||
|
contentContainer.addView(messageView, new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weight to take available space.
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
0,
|
||||||
|
1.0f);
|
||||||
|
scrollView.setLayoutParams(params);
|
||||||
|
// Add ScrollView to main layout only if content exist.
|
||||||
|
mainLayout.addView(scrollView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds buttons to the dialog, arranging them dynamically based on their widths.
|
||||||
|
*
|
||||||
|
* @param okButtonText OK button text, or null to use the default "OK" string.
|
||||||
|
* @param onOkClick Action for the OK button click.
|
||||||
|
* @param onCancelClick Action for the Cancel button click, or null if no Cancel button.
|
||||||
|
* @param neutralButtonText Neutral button text, or null if no Neutral button.
|
||||||
|
* @param onNeutralClick Action for the Neutral button click, or null if no Neutral button.
|
||||||
|
* @param dismissDialogOnNeutralClick If the dialog should dismiss on Neutral button click.
|
||||||
|
*/
|
||||||
|
private void addButtons(String okButtonText, Runnable onOkClick, Runnable onCancelClick,
|
||||||
|
@Nullable String neutralButtonText, @Nullable Runnable onNeutralClick,
|
||||||
|
boolean dismissDialogOnNeutralClick) {
|
||||||
|
// Button container.
|
||||||
|
LinearLayout buttonContainer = new LinearLayout(context);
|
||||||
|
buttonContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
LinearLayout.LayoutParams buttonContainerParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
buttonContainerParams.setMargins(0, dip16, 0, 0);
|
||||||
|
buttonContainer.setLayoutParams(buttonContainerParams);
|
||||||
|
|
||||||
|
List<Button> buttons = new ArrayList<>();
|
||||||
|
List<Integer> buttonWidths = new ArrayList<>();
|
||||||
|
|
||||||
|
// Create buttons in order: Neutral, Cancel, OK.
|
||||||
|
if (neutralButtonText != null && onNeutralClick != null) {
|
||||||
|
Button neutralButton = createButton(neutralButtonText, onNeutralClick, false, dismissDialogOnNeutralClick);
|
||||||
|
buttons.add(neutralButton);
|
||||||
|
buttonWidths.add(measureButtonWidth(neutralButton));
|
||||||
|
}
|
||||||
|
if (onCancelClick != null) {
|
||||||
|
Button cancelButton = createButton(context.getString(android.R.string.cancel), onCancelClick, false, true);
|
||||||
|
buttons.add(cancelButton);
|
||||||
|
buttonWidths.add(measureButtonWidth(cancelButton));
|
||||||
|
}
|
||||||
|
if (onOkClick != null) {
|
||||||
|
Button okButton = createButton(
|
||||||
|
okButtonText != null ? okButtonText : context.getString(android.R.string.ok),
|
||||||
|
onOkClick, true, true);
|
||||||
|
buttons.add(okButton);
|
||||||
|
buttonWidths.add(measureButtonWidth(okButton));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle button layout.
|
||||||
|
layoutButtons(buttonContainer, buttons, buttonWidths);
|
||||||
|
mainLayout.addView(buttonContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a styled button with customizable text, click behavior, and appearance.
|
||||||
|
*
|
||||||
|
* @param text The button text to display.
|
||||||
|
* @param onClick The action to perform on button click.
|
||||||
|
* @param isOkButton If this is the OK button, which uses distinct styling.
|
||||||
|
* @param dismissDialog If the dialog should dismiss when the button is clicked.
|
||||||
|
* @return The created Button.
|
||||||
|
*/
|
||||||
|
private Button createButton(String text, Runnable onClick, boolean isOkButton, boolean dismissDialog) {
|
||||||
|
Button button = new Button(context, null, 0);
|
||||||
|
button.setText(text);
|
||||||
|
button.setTextSize(14);
|
||||||
|
button.setAllCaps(false);
|
||||||
|
button.setSingleLine(true);
|
||||||
|
button.setEllipsize(TextUtils.TruncateAt.END);
|
||||||
|
button.setGravity(Gravity.CENTER);
|
||||||
|
// Set internal padding.
|
||||||
|
button.setPadding(dip16, 0, dip16, 0);
|
||||||
|
|
||||||
|
// Background color for OK button (inversion).
|
||||||
|
// Background color for Cancel or Neutral buttons.
|
||||||
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||||
|
Utils.createCornerRadii(20), null, null));
|
||||||
|
background.getPaint().setColor(isOkButton
|
||||||
|
? Utils.getOkButtonBackgroundColor()
|
||||||
|
: Utils.getCancelOrNeutralButtonBackgroundColor());
|
||||||
|
button.setBackground(background);
|
||||||
|
|
||||||
|
button.setTextColor(Utils.isDarkModeEnabled()
|
||||||
|
? (isOkButton ? Color.BLACK : Color.WHITE)
|
||||||
|
: (isOkButton ? Color.WHITE : Color.BLACK));
|
||||||
|
|
||||||
|
button.setOnClickListener(v -> {
|
||||||
|
if (onClick != null) onClick.run();
|
||||||
|
if (dismissDialog) dialog.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Measures the width of a button.
|
||||||
|
*/
|
||||||
|
private int measureButtonWidth(Button button) {
|
||||||
|
button.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
|
||||||
|
return button.getMeasuredWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arranges buttons in the dialog, either in a single row or multiple rows based on their total width.
|
||||||
|
*
|
||||||
|
* @param buttonContainer The container for the buttons.
|
||||||
|
* @param buttons The list of buttons to arrange.
|
||||||
|
* @param buttonWidths The measured widths of the buttons.
|
||||||
|
*/
|
||||||
|
private void layoutButtons(LinearLayout buttonContainer, List<Button> buttons, List<Integer> buttonWidths) {
|
||||||
|
if (buttons.isEmpty()) return;
|
||||||
|
|
||||||
|
// Check if buttons fit in one row.
|
||||||
|
int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
|
||||||
|
int totalWidth = 0;
|
||||||
|
for (Integer width : buttonWidths) {
|
||||||
|
totalWidth += width;
|
||||||
|
}
|
||||||
|
if (buttonWidths.size() > 1) {
|
||||||
|
// Add margins for gaps.
|
||||||
|
totalWidth += (buttonWidths.size() - 1) * dip8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single button: stretch to full width.
|
||||||
|
if (buttons.size() == 1) {
|
||||||
|
layoutSingleButton(buttonContainer, buttons.get(0));
|
||||||
|
} else if (totalWidth <= screenWidth * 0.8) {
|
||||||
|
// Single row: Neutral, Cancel, OK.
|
||||||
|
layoutButtonsInRow(buttonContainer, buttons, buttonWidths);
|
||||||
|
} else {
|
||||||
|
// Multiple rows: OK, Cancel, Neutral.
|
||||||
|
layoutButtonsInColumns(buttonContainer, buttons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arranges a single button, stretching it to full width.
|
||||||
|
*
|
||||||
|
* @param buttonContainer The container for the button.
|
||||||
|
* @param button The button to arrange.
|
||||||
|
*/
|
||||||
|
private void layoutSingleButton(LinearLayout buttonContainer, Button button) {
|
||||||
|
LinearLayout singleContainer = new LinearLayout(context);
|
||||||
|
singleContainer.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
singleContainer.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
ViewGroup parent = (ViewGroup) button.getParent();
|
||||||
|
if (parent != null) parent.removeView(button);
|
||||||
|
|
||||||
|
button.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
dip36));
|
||||||
|
singleContainer.addView(button);
|
||||||
|
buttonContainer.addView(singleContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arranges buttons in a single horizontal row with proportional widths.
|
||||||
|
*
|
||||||
|
* @param buttonContainer The container for the buttons.
|
||||||
|
* @param buttons The list of buttons to arrange.
|
||||||
|
* @param buttonWidths The measured widths of the buttons.
|
||||||
|
*/
|
||||||
|
private void layoutButtonsInRow(LinearLayout buttonContainer, List<Button> buttons, List<Integer> buttonWidths) {
|
||||||
|
LinearLayout rowContainer = new LinearLayout(context);
|
||||||
|
rowContainer.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
rowContainer.setGravity(Gravity.CENTER);
|
||||||
|
rowContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT));
|
||||||
|
|
||||||
|
// Add all buttons with proportional weights and specific margins.
|
||||||
|
for (int i = 0; i < buttons.size(); i++) {
|
||||||
|
Button button = getButton(buttons, buttonWidths, i);
|
||||||
|
rowContainer.addView(button);
|
||||||
|
}
|
||||||
|
|
||||||
|
buttonContainer.addView(rowContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Button getButton(List<Button> buttons, List<Integer> buttonWidths, int i) {
|
||||||
|
Button button = buttons.get(i);
|
||||||
|
ViewGroup parent = (ViewGroup) button.getParent();
|
||||||
|
if (parent != null) parent.removeView(button);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
0, dip36, buttonWidths.get(i));
|
||||||
|
|
||||||
|
// Set margins based on button type and combination.
|
||||||
|
if (buttons.size() == 2) {
|
||||||
|
// Neutral + OK or Cancel + OK.
|
||||||
|
params.setMargins(i == 0 ? 0 : dip4, 0, i == 0 ? dip4 : 0, 0);
|
||||||
|
} else if (buttons.size() == 3) {
|
||||||
|
// Neutral.
|
||||||
|
// Cancel.
|
||||||
|
// OK.
|
||||||
|
params.setMargins(i == 0 ? 0 : dip4, 0, i == 2 ? 0 : dip4, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.setLayoutParams(params);
|
||||||
|
return button;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Arranges buttons in separate rows, ordered OK, Cancel, Neutral.
|
||||||
|
*
|
||||||
|
* @param buttonContainer The container for the buttons.
|
||||||
|
* @param buttons The list of buttons to arrange.
|
||||||
|
*/
|
||||||
|
private void layoutButtonsInColumns(LinearLayout buttonContainer, List<Button> buttons) {
|
||||||
|
// Reorder: OK, Cancel, Neutral.
|
||||||
|
List<Button> reorderedButtons = new ArrayList<>();
|
||||||
|
if (buttons.size() == 3) {
|
||||||
|
reorderedButtons.add(buttons.get(2)); // OK
|
||||||
|
reorderedButtons.add(buttons.get(1)); // Cancel
|
||||||
|
reorderedButtons.add(buttons.get(0)); // Neutral
|
||||||
|
} else if (buttons.size() == 2) {
|
||||||
|
reorderedButtons.add(buttons.get(1)); // OK or Cancel
|
||||||
|
reorderedButtons.add(buttons.get(0)); // Neutral or Cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < reorderedButtons.size(); i++) {
|
||||||
|
Button button = reorderedButtons.get(i);
|
||||||
|
LinearLayout singleContainer = new LinearLayout(context);
|
||||||
|
singleContainer.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
singleContainer.setGravity(Gravity.CENTER);
|
||||||
|
singleContainer.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
dip36));
|
||||||
|
|
||||||
|
ViewGroup parent = (ViewGroup) button.getParent();
|
||||||
|
if (parent != null) parent.removeView(button);
|
||||||
|
|
||||||
|
button.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
dip36));
|
||||||
|
singleContainer.addView(button);
|
||||||
|
buttonContainer.addView(singleContainer);
|
||||||
|
|
||||||
|
// Add a spacer between the buttons (except the last one).
|
||||||
|
if (i < reorderedButtons.size() - 1) {
|
||||||
|
View spacer = new View(context);
|
||||||
|
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
dip8);
|
||||||
|
spacer.setLayoutParams(spacerParams);
|
||||||
|
buttonContainer.addView(spacer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,463 @@
|
|||||||
|
package app.revanced.extension.shared.ui;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
|
||||||
|
import android.animation.Animator;
|
||||||
|
import android.animation.AnimatorListenerAdapter;
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.VelocityTracker;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewConfiguration;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.view.animation.DecelerateInterpolator;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.graphics.drawable.ShapeDrawable;
|
||||||
|
import android.graphics.drawable.shapes.RoundRectShape;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.Scroller;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A utility class for creating a bottom sheet dialog that slides up from the bottom of the screen.
|
||||||
|
* The dialog supports drag-to-dismiss functionality, animations, and nested scrolling for scrollable content.
|
||||||
|
*/
|
||||||
|
public class SheetBottomDialog {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link SlideDialog} that slides up from the bottom of the screen with a specified content view.
|
||||||
|
* The dialog supports drag-to-dismiss functionality, allowing the user to drag it downward to close it,
|
||||||
|
* with proper handling of nested scrolling for scrollable content (e.g., {@link ListView}).
|
||||||
|
* It includes side margins, a top spacer for drag interaction, and can be dismissed by touching outside.
|
||||||
|
*
|
||||||
|
* @param context The context used to create the dialog.
|
||||||
|
* @param contentView The {@link View} to be displayed inside the dialog, such as a {@link LinearLayout}
|
||||||
|
* containing a {@link ListView}, buttons, or other UI elements.
|
||||||
|
* @param animationDuration The duration of the slide-in and slide-out animations in milliseconds.
|
||||||
|
* @return A configured {@link SlideDialog} instance ready to be shown.
|
||||||
|
* @throws IllegalArgumentException If contentView is null.
|
||||||
|
*/
|
||||||
|
public static SlideDialog createSlideDialog(@NonNull Context context, @NonNull View contentView, int animationDuration) {
|
||||||
|
SlideDialog dialog = new SlideDialog(context);
|
||||||
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
dialog.setCanceledOnTouchOutside(true);
|
||||||
|
dialog.setCancelable(true);
|
||||||
|
|
||||||
|
// Create wrapper layout for side margins.
|
||||||
|
LinearLayout wrapperLayout = new LinearLayout(context);
|
||||||
|
wrapperLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
// Create drag container.
|
||||||
|
DraggableLinearLayout dragContainer = new DraggableLinearLayout(context, animationDuration);
|
||||||
|
dragContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
dragContainer.setDialog(dialog);
|
||||||
|
|
||||||
|
// Add top spacer.
|
||||||
|
View spacer = new View(context);
|
||||||
|
final int dip40 = dipToPixels(40);
|
||||||
|
LinearLayout.LayoutParams spacerParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, dip40);
|
||||||
|
spacer.setLayoutParams(spacerParams);
|
||||||
|
spacer.setClickable(true);
|
||||||
|
dragContainer.addView(spacer);
|
||||||
|
|
||||||
|
// Add content view.
|
||||||
|
ViewGroup parent = (ViewGroup) contentView.getParent();
|
||||||
|
if (parent != null) parent.removeView(contentView);
|
||||||
|
dragContainer.addView(contentView);
|
||||||
|
|
||||||
|
// Add drag container to wrapper layout.
|
||||||
|
wrapperLayout.addView(dragContainer);
|
||||||
|
|
||||||
|
dialog.setContentView(wrapperLayout);
|
||||||
|
|
||||||
|
// Configure dialog window.
|
||||||
|
Window window = dialog.getWindow();
|
||||||
|
if (window != null) {
|
||||||
|
Utils.setDialogWindowParameters(window, Gravity.BOTTOM, 0, 100, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up animation on drag container.
|
||||||
|
dialog.setAnimView(dragContainer);
|
||||||
|
dialog.setAnimationDuration(animationDuration);
|
||||||
|
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a {@link DraggableLinearLayout} with a rounded background and a centered handle bar,
|
||||||
|
* styled for use as the main layout in a {@link SlideDialog}. The layout has vertical orientation,
|
||||||
|
* includes padding, and supports drag-to-dismiss functionality with proper handling of nested scrolling
|
||||||
|
* for scrollable content (e.g., {@link ListView}) or clickable elements (e.g., buttons, {@link android.widget.SeekBar}).
|
||||||
|
*
|
||||||
|
* @param context The context used to create the layout.
|
||||||
|
* @param backgroundColor The background color for the layout as an {@link Integer}, or null to use
|
||||||
|
* the default dialog background color.
|
||||||
|
* @return A configured {@link DraggableLinearLayout} with a handle bar and styled background.
|
||||||
|
*/
|
||||||
|
public static DraggableLinearLayout createMainLayout(@NonNull Context context, @Nullable Integer backgroundColor) {
|
||||||
|
// Preset size constants.
|
||||||
|
final int dip4 = dipToPixels(4); // Handle bar height.
|
||||||
|
final int dip8 = dipToPixels(8); // Dialog padding.
|
||||||
|
final int dip40 = dipToPixels(40); // Handle bar width.
|
||||||
|
|
||||||
|
DraggableLinearLayout mainLayout = new DraggableLinearLayout(context);
|
||||||
|
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
layoutParams.setMargins(dip8, 0, dip8, dip8);
|
||||||
|
mainLayout.setLayoutParams(layoutParams);
|
||||||
|
|
||||||
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||||
|
Utils.createCornerRadii(12), null, null));
|
||||||
|
int color = (backgroundColor != null) ? backgroundColor : Utils.getDialogBackgroundColor();
|
||||||
|
background.getPaint().setColor(color);
|
||||||
|
mainLayout.setBackground(background);
|
||||||
|
|
||||||
|
// Add handle bar.
|
||||||
|
LinearLayout handleContainer = new LinearLayout(context);
|
||||||
|
LinearLayout.LayoutParams containerParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
containerParams.setMargins(0, dip8, 0, 0);
|
||||||
|
handleContainer.setLayoutParams(containerParams);
|
||||||
|
handleContainer.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
|
||||||
|
View handleBar = new View(context);
|
||||||
|
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
|
||||||
|
Utils.createCornerRadii(4), null, null));
|
||||||
|
handleBackground.getPaint().setColor(Utils.adjustColorBrightness(color, 0.9f, 1.25f));
|
||||||
|
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(dip40, dip4);
|
||||||
|
handleBar.setLayoutParams(handleParams);
|
||||||
|
handleBar.setBackground(handleBackground);
|
||||||
|
|
||||||
|
handleContainer.addView(handleBar);
|
||||||
|
mainLayout.addView(handleContainer);
|
||||||
|
|
||||||
|
return mainLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom {@link LinearLayout} that provides drag-to-dismiss functionality for a {@link SlideDialog}.
|
||||||
|
* This layout intercepts touch events to allow dragging the dialog downward to dismiss it when the
|
||||||
|
* content cannot scroll upward. It ensures compatibility with scrollable content (e.g., {@link ListView},
|
||||||
|
* {@link ScrollView}) and clickable elements (e.g., buttons, {@link android.widget.SeekBar}) by prioritizing
|
||||||
|
* their touch events to prevent conflicts.
|
||||||
|
*
|
||||||
|
* <p>Dragging is enabled only after the dialog's slide-in animation completes. The dialog is dismissed
|
||||||
|
* if dragged beyond 50% of its height or with a downward fling velocity exceeding 800 px/s.</p>
|
||||||
|
*/
|
||||||
|
public static class DraggableLinearLayout extends LinearLayout {
|
||||||
|
private static final int MIN_FLING_VELOCITY = 800; // px/s
|
||||||
|
private static final float DISMISS_HEIGHT_FRACTION = 0.5f; // 50% of height.
|
||||||
|
|
||||||
|
private float initialTouchRawY; // Raw Y on ACTION_DOWN.
|
||||||
|
private float dragOffset; // Current drag translation.
|
||||||
|
private boolean isDragging;
|
||||||
|
private boolean isDragEnabled;
|
||||||
|
|
||||||
|
private final int animationDuration;
|
||||||
|
private final Scroller scroller;
|
||||||
|
private final VelocityTracker velocityTracker;
|
||||||
|
private final Runnable settleRunnable;
|
||||||
|
|
||||||
|
private SlideDialog dialog;
|
||||||
|
private float dismissThreshold;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link DraggableLinearLayout} with the specified context.
|
||||||
|
*/
|
||||||
|
public DraggableLinearLayout(@NonNull Context context) {
|
||||||
|
this(context, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link DraggableLinearLayout} with the specified context and animation duration.
|
||||||
|
*
|
||||||
|
* @param context The context used to initialize the layout.
|
||||||
|
* @param animDuration The duration of the drag animation in milliseconds.
|
||||||
|
*/
|
||||||
|
public DraggableLinearLayout(@NonNull Context context, int animDuration) {
|
||||||
|
super(context);
|
||||||
|
scroller = new Scroller(context, new DecelerateInterpolator());
|
||||||
|
velocityTracker = VelocityTracker.obtain();
|
||||||
|
animationDuration = animDuration;
|
||||||
|
settleRunnable = this::runSettleAnimation;
|
||||||
|
|
||||||
|
setClickable(true);
|
||||||
|
|
||||||
|
// Enable drag only after slide-in animation finishes.
|
||||||
|
isDragEnabled = false;
|
||||||
|
postDelayed(() -> isDragEnabled = true, animationDuration + 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the {@link SlideDialog} associated with this layout for dismissal.
|
||||||
|
*/
|
||||||
|
public void setDialog(@NonNull SlideDialog dialog) {
|
||||||
|
this.dialog = dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the dismissal threshold when the layout's size changes.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
dismissThreshold = h * DISMISS_HEIGHT_FRACTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Intercepts touch events to initiate dragging when the content cannot scroll upward and the
|
||||||
|
* touch movement exceeds the system's touch slop.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||||
|
if (!isDragEnabled) return false;
|
||||||
|
|
||||||
|
switch (ev.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
initialTouchRawY = ev.getRawY();
|
||||||
|
isDragging = false;
|
||||||
|
scroller.forceFinished(true);
|
||||||
|
removeCallbacks(settleRunnable);
|
||||||
|
velocityTracker.clear();
|
||||||
|
velocityTracker.addMovement(ev);
|
||||||
|
dragOffset = getTranslationY();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
float dy = ev.getRawY() - initialTouchRawY;
|
||||||
|
if (dy > ViewConfiguration.get(getContext()).getScaledTouchSlop()
|
||||||
|
&& !canChildScrollUp()) {
|
||||||
|
isDragging = true;
|
||||||
|
return true; // Intercept touches for drag.
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles touch events to perform dragging or trigger dismissal/return animations based on
|
||||||
|
* drag distance or fling velocity.
|
||||||
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent ev) {
|
||||||
|
if (!isDragEnabled) return super.onTouchEvent(ev);
|
||||||
|
velocityTracker.addMovement(ev);
|
||||||
|
|
||||||
|
switch (ev.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
if (isDragging) {
|
||||||
|
float deltaY = ev.getRawY() - initialTouchRawY;
|
||||||
|
dragOffset = Math.max(0, deltaY); // Prevent upward drag.
|
||||||
|
setTranslationY(dragOffset); // 1:1 following finger.
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
velocityTracker.computeCurrentVelocity(1000);
|
||||||
|
float velocityY = velocityTracker.getYVelocity();
|
||||||
|
|
||||||
|
if (dragOffset > dismissThreshold || velocityY > MIN_FLING_VELOCITY) {
|
||||||
|
startDismissAnimation();
|
||||||
|
} else {
|
||||||
|
startReturnAnimation();
|
||||||
|
}
|
||||||
|
isDragging = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Consume the touch event to prevent focus changes on child views.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an animation to dismiss the dialog by sliding it downward.
|
||||||
|
*/
|
||||||
|
private void startDismissAnimation() {
|
||||||
|
scroller.startScroll(0, (int) dragOffset,
|
||||||
|
0, getHeight() - (int) dragOffset, animationDuration);
|
||||||
|
post(settleRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts an animation to return the dialog to its original position.
|
||||||
|
*/
|
||||||
|
private void startReturnAnimation() {
|
||||||
|
scroller.startScroll(0, (int) dragOffset,
|
||||||
|
0, -(int) dragOffset, animationDuration);
|
||||||
|
post(settleRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the settle animation, updating the layout's translation until the animation completes.
|
||||||
|
* Dismisses the dialog if the drag offset reaches the view's height.
|
||||||
|
*/
|
||||||
|
private void runSettleAnimation() {
|
||||||
|
if (scroller.computeScrollOffset()) {
|
||||||
|
dragOffset = scroller.getCurrY();
|
||||||
|
setTranslationY(dragOffset);
|
||||||
|
|
||||||
|
if (dragOffset >= getHeight() && dialog != null) {
|
||||||
|
dialog.dismiss();
|
||||||
|
scroller.forceFinished(true);
|
||||||
|
} else {
|
||||||
|
post(settleRunnable);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dragOffset = getTranslationY();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if any child view can scroll upward, preventing drag if scrolling is possible.
|
||||||
|
*
|
||||||
|
* @return True if a child can scroll upward, false otherwise.
|
||||||
|
*/
|
||||||
|
private boolean canChildScrollUp() {
|
||||||
|
View target = findScrollableChild(this);
|
||||||
|
return target != null && target.canScrollVertically(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively searches for a scrollable child view within the given view group.
|
||||||
|
*
|
||||||
|
* @param group The view group to search.
|
||||||
|
* @return The scrollable child view, or null if none found.
|
||||||
|
*/
|
||||||
|
private View findScrollableChild(ViewGroup group) {
|
||||||
|
for (int i = 0; i < group.getChildCount(); i++) {
|
||||||
|
View child = group.getChildAt(i);
|
||||||
|
if (child.canScrollVertically(-1)) return child;
|
||||||
|
if (child instanceof ViewGroup) {
|
||||||
|
View scroll = findScrollableChild((ViewGroup) child);
|
||||||
|
if (scroll != null) return scroll;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom dialog that slides up from the bottom of the screen with animation. It supports
|
||||||
|
* drag-to-dismiss functionality and ensures smooth dismissal animations without overlapping
|
||||||
|
* dismiss calls. The dialog animates a specified view during show and dismiss operations.
|
||||||
|
*/
|
||||||
|
public static class SlideDialog extends Dialog {
|
||||||
|
private View animView;
|
||||||
|
private boolean isDismissing = false;
|
||||||
|
private int duration;
|
||||||
|
private final int screenHeight;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new {@link SlideDialog} with the specified context.
|
||||||
|
*/
|
||||||
|
public SlideDialog(@NonNull Context context) {
|
||||||
|
super(context);
|
||||||
|
screenHeight = context.getResources().getDisplayMetrics().heightPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the view to animate during show and dismiss operations.
|
||||||
|
*/
|
||||||
|
public void setAnimView(@NonNull View view) {
|
||||||
|
this.animView = view;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the duration of the slide-in and slide-out animations.
|
||||||
|
*/
|
||||||
|
public void setAnimationDuration(int duration) {
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the dialog with a slide-up animation for the animated view, if set.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void show() {
|
||||||
|
super.show();
|
||||||
|
if (animView == null) return;
|
||||||
|
|
||||||
|
animView.setTranslationY(screenHeight);
|
||||||
|
animView.animate()
|
||||||
|
.translationY(0)
|
||||||
|
.setDuration(duration)
|
||||||
|
.setListener(null)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels the dialog, triggering a dismissal animation.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void cancel() {
|
||||||
|
dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismisses the dialog with a slide-down animation for the animated view, if set.
|
||||||
|
* Ensures that dismissal is not triggered multiple times concurrently.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void dismiss() {
|
||||||
|
if (isDismissing) return;
|
||||||
|
isDismissing = true;
|
||||||
|
|
||||||
|
Window window = getWindow();
|
||||||
|
if (window == null) {
|
||||||
|
super.dismiss();
|
||||||
|
isDismissing = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowManager.LayoutParams params = window.getAttributes();
|
||||||
|
float startDim = params != null ? params.dimAmount : 0f;
|
||||||
|
|
||||||
|
// Animate dimming effect.
|
||||||
|
ValueAnimator dimAnimator = ValueAnimator.ofFloat(startDim, 0f);
|
||||||
|
dimAnimator.setDuration(duration);
|
||||||
|
dimAnimator.addUpdateListener(animation -> {
|
||||||
|
if (params != null) {
|
||||||
|
params.dimAmount = (float) animation.getAnimatedValue();
|
||||||
|
window.setAttributes(params);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (animView == null) {
|
||||||
|
dimAnimator.addListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
SlideDialog.super.dismiss();
|
||||||
|
isDismissing = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
dimAnimator.start();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dimAnimator.start();
|
||||||
|
animView.animate()
|
||||||
|
.translationY(screenHeight)
|
||||||
|
.setDuration(duration)
|
||||||
|
.setListener(new AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
SlideDialog.super.dismiss();
|
||||||
|
isDismissing = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,29 +7,29 @@ import android.preference.PreferenceFragment;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.tiktok.settings.preference.ReVancedPreferenceFragment;
|
import app.revanced.extension.tiktok.settings.preference.TikTokPreferenceFragment;
|
||||||
|
|
||||||
import com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization.AdPersonalizationActivity;
|
import com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization.AdPersonalizationActivity;
|
||||||
|
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks AdPersonalizationActivity.
|
* Hooks AdPersonalizationActivity to inject a custom {@link TikTokPreferenceFragment}.
|
||||||
* <p>
|
|
||||||
* This class is responsible for injecting our own fragment by replacing the AdPersonalizationActivity.
|
|
||||||
*
|
|
||||||
* @noinspection unused
|
|
||||||
*/
|
*/
|
||||||
public class AdPersonalizationActivityHook {
|
@SuppressWarnings({"deprecation", "NewApi", "unused"})
|
||||||
|
public class TikTokActivityHook {
|
||||||
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
|
public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) {
|
||||||
try {
|
try {
|
||||||
Class<?> entryClazz = Class.forName(entryClazzName);
|
Class<?> entryClazz = Class.forName(entryClazzName);
|
||||||
Class<?> entryInfoClazz = Class.forName(entryInfoClazzName);
|
Class<?> entryInfoClazz = Class.forName(entryInfoClazzName);
|
||||||
Constructor<?> entryConstructor = entryClazz.getConstructor(entryInfoClazz);
|
Constructor<?> entryConstructor = entryClazz.getConstructor(entryInfoClazz);
|
||||||
Constructor<?> entryInfoConstructor = entryInfoClazz.getDeclaredConstructors()[0];
|
Constructor<?> entryInfoConstructor = entryInfoClazz.getDeclaredConstructors()[0];
|
||||||
Object buttonInfo = entryInfoConstructor.newInstance("ReVanced settings", null, (View.OnClickListener) view -> startSettingsActivity(), "revanced");
|
Object buttonInfo = entryInfoConstructor.newInstance(
|
||||||
|
"ReVanced settings", null, (View.OnClickListener) view -> startSettingsActivity(), "revanced");
|
||||||
return entryConstructor.newInstance(buttonInfo);
|
return entryConstructor.newInstance(buttonInfo);
|
||||||
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException |
|
} catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException |
|
||||||
InstantiationException e) {
|
InstantiationException e) {
|
||||||
@@ -62,7 +62,7 @@ public class AdPersonalizationActivityHook {
|
|||||||
linearLayout.addView(fragment);
|
linearLayout.addView(fragment);
|
||||||
base.setContentView(linearLayout);
|
base.setContentView(linearLayout);
|
||||||
|
|
||||||
PreferenceFragment preferenceFragment = new ReVancedPreferenceFragment();
|
PreferenceFragment preferenceFragment = new TikTokPreferenceFragment();
|
||||||
base.getFragmentManager().beginTransaction().replace(fragmentId, preferenceFragment).commit();
|
base.getFragmentManager().beginTransaction().replace(fragmentId, preferenceFragment).commit();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -121,7 +121,7 @@ public class DownloadPathPreference extends DialogPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private int findIndexOf(String str) {
|
private int findIndexOf(String str) {
|
||||||
for (int i = 0; i < entryValues.length; i++) {
|
for (int i = 0, length = entryValues.length; i < length; i++) {
|
||||||
if (str.equals(entryValues[i])) return i;
|
if (str.equals(entryValues[i])) return i;
|
||||||
}
|
}
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference {
|
|||||||
/**
|
/**
|
||||||
* Because resources cannot be added to TikTok,
|
* Because resources cannot be added to TikTok,
|
||||||
* these strings are copied from the shared strings.xml file.
|
* these strings are copied from the shared strings.xml file.
|
||||||
*
|
* <p>
|
||||||
* Changes here must also be made in strings.xml
|
* Changes here must also be made in strings.xml
|
||||||
*/
|
*/
|
||||||
private final Map<String, String> aboutStrings = Map.of(
|
private final Map<String, String> aboutStrings = Map.of(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import app.revanced.extension.tiktok.settings.preference.categories.SimSpoofPref
|
|||||||
* Preference fragment for ReVanced settings
|
* Preference fragment for ReVanced settings
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
public class TikTokPreferenceFragment extends AbstractPreferenceFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void syncSettingWithPreference(@NonNull Preference pref,
|
protected void syncSettingWithPreference(@NonNull Preference pref,
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
package app.revanced.extension.twitch.settings;
|
package app.revanced.extension.twitch.settings;
|
||||||
|
|
||||||
|
import static app.revanced.extension.twitch.Utils.getStringId;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.twitch.settings.preference.ReVancedPreferenceFragment;
|
import app.revanced.extension.twitch.settings.preference.TwitchPreferenceFragment;
|
||||||
|
|
||||||
import tv.twitch.android.feature.settings.menu.SettingsMenuGroup;
|
import tv.twitch.android.feature.settings.menu.SettingsMenuGroup;
|
||||||
import tv.twitch.android.settings.SettingsActivity;
|
import tv.twitch.android.settings.SettingsActivity;
|
||||||
|
|
||||||
@@ -13,17 +18,15 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks AppCompatActivity.
|
* Hooks AppCompatActivity to inject a custom {@link TwitchPreferenceFragment}.
|
||||||
* <p>
|
|
||||||
* This class is responsible for injecting our own fragment by replacing the AppCompatActivity.
|
|
||||||
* @noinspection unused
|
|
||||||
*/
|
*/
|
||||||
public class AppCompatActivityHook {
|
@SuppressWarnings({"deprecation", "NewApi", "unused"})
|
||||||
|
public class TwitchActivityHook {
|
||||||
private static final int REVANCED_SETTINGS_MENU_ITEM_ID = 0x7;
|
private static final int REVANCED_SETTINGS_MENU_ITEM_ID = 0x7;
|
||||||
private static final String EXTRA_REVANCED_SETTINGS = "app.revanced.twitch.settings";
|
private static final String EXTRA_REVANCED_SETTINGS = "app.revanced.twitch.settings";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Launches SettingsActivity and show ReVanced settings
|
* Launches SettingsActivity and show ReVanced settings.
|
||||||
*/
|
*/
|
||||||
public static void startSettingsActivity() {
|
public static void startSettingsActivity() {
|
||||||
Logger.printDebug(() -> "Launching ReVanced settings");
|
Logger.printDebug(() -> "Launching ReVanced settings");
|
||||||
@@ -41,27 +44,27 @@ public class AppCompatActivityHook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for easy access in smali
|
* Helper for easy access in smali.
|
||||||
* @return Returns string resource id
|
* @return Returns string resource id.
|
||||||
*/
|
*/
|
||||||
public static int getReVancedSettingsString() {
|
public static int getReVancedSettingsString() {
|
||||||
return app.revanced.extension.twitch.Utils.getStringId("revanced_settings");
|
return getStringId("revanced_settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intercepts settings menu group list creation in SettingsMenuPresenter$Event.MenuGroupsUpdated
|
* Intercepts settings menu group list creation in SettingsMenuPresenter$Event.MenuGroupsUpdated.
|
||||||
* @return Returns a modified list of menu groups
|
* @return Returns a modified list of menu groups.
|
||||||
*/
|
*/
|
||||||
public static List<SettingsMenuGroup> handleSettingMenuCreation(List<SettingsMenuGroup> settingGroups, Object revancedEntry) {
|
public static List<SettingsMenuGroup> handleSettingMenuCreation(List<SettingsMenuGroup> settingGroups, Object revancedEntry) {
|
||||||
List<SettingsMenuGroup> groups = new ArrayList<>(settingGroups);
|
List<SettingsMenuGroup> groups = new ArrayList<>(settingGroups);
|
||||||
|
|
||||||
if (groups.isEmpty()) {
|
if (groups.isEmpty()) {
|
||||||
// Create new menu group if none exist yet
|
// Create new menu group if none exist yet.
|
||||||
List<Object> items = new ArrayList<>();
|
List<Object> items = new ArrayList<>();
|
||||||
items.add(revancedEntry);
|
items.add(revancedEntry);
|
||||||
groups.add(new SettingsMenuGroup(items));
|
groups.add(new SettingsMenuGroup(items));
|
||||||
} else {
|
} else {
|
||||||
// Add to last menu group
|
// Add to last menu group.
|
||||||
int groupIdx = groups.size() - 1;
|
int groupIdx = groups.size() - 1;
|
||||||
List<Object> items = new ArrayList<>(groups.remove(groupIdx).getSettingsMenuItems());
|
List<Object> items = new ArrayList<>(groups.remove(groupIdx).getSettingsMenuItems());
|
||||||
items.add(revancedEntry);
|
items.add(revancedEntry);
|
||||||
@@ -73,8 +76,8 @@ public class AppCompatActivityHook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intercepts settings menu group onclick events
|
* Intercepts settings menu group onclick events.
|
||||||
* @return Returns true if handled, otherwise false
|
* @return Returns true if handled, otherwise false.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
public static boolean handleSettingMenuOnClick(Enum item) {
|
public static boolean handleSettingMenuOnClick(Enum item) {
|
||||||
@@ -88,20 +91,20 @@ public class AppCompatActivityHook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intercepts fragment loading in SettingsActivity.onCreate
|
* Intercepts fragment loading in SettingsActivity.onCreate.
|
||||||
* @return Returns true if the revanced settings have been requested by the user, otherwise false
|
* @return Returns true if the ReVanced settings have been requested by the user, otherwise false.
|
||||||
*/
|
*/
|
||||||
public static boolean handleSettingsCreation(androidx.appcompat.app.AppCompatActivity base) {
|
public static boolean handleSettingsCreation(AppCompatActivity base) {
|
||||||
if (!base.getIntent().getBooleanExtra(EXTRA_REVANCED_SETTINGS, false)) {
|
if (!base.getIntent().getBooleanExtra(EXTRA_REVANCED_SETTINGS, false)) {
|
||||||
Logger.printDebug(() -> "Revanced settings not requested");
|
Logger.printDebug(() -> "Revanced settings not requested");
|
||||||
return false; // User wants to enter another settings fragment
|
return false; // User wants to enter another settings fragment.
|
||||||
}
|
}
|
||||||
Logger.printDebug(() -> "ReVanced settings requested");
|
Logger.printDebug(() -> "ReVanced settings requested");
|
||||||
|
|
||||||
ReVancedPreferenceFragment fragment = new ReVancedPreferenceFragment();
|
TwitchPreferenceFragment fragment = new TwitchPreferenceFragment();
|
||||||
ActionBar supportActionBar = base.getSupportActionBar();
|
ActionBar supportActionBar = base.getSupportActionBar();
|
||||||
if (supportActionBar != null)
|
if (supportActionBar != null)
|
||||||
supportActionBar.setTitle(app.revanced.extension.twitch.Utils.getStringId("revanced_settings"));
|
supportActionBar.setTitle(getStringId("revanced_settings"));
|
||||||
|
|
||||||
base.getFragmentManager()
|
base.getFragmentManager()
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
@@ -7,6 +7,7 @@ import android.util.AttributeSet;
|
|||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
@SuppressWarnings({"deprecation", "unused"})
|
||||||
public class CustomPreferenceCategory extends PreferenceCategory {
|
public class CustomPreferenceCategory extends PreferenceCategory {
|
||||||
public CustomPreferenceCategory(Context context, AttributeSet attrs) {
|
public CustomPreferenceCategory(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragm
|
|||||||
import app.revanced.extension.twitch.settings.Settings;
|
import app.revanced.extension.twitch.settings.Settings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preference fragment for ReVanced settings
|
* Preference fragment for ReVanced settings.
|
||||||
*/
|
*/
|
||||||
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
public class TwitchPreferenceFragment extends AbstractPreferenceFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize() {
|
protected void initialize() {
|
||||||
@@ -19,6 +19,7 @@ import java.net.HttpURLConnection;
|
|||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
@@ -65,6 +66,17 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
public boolean isAvailable() {
|
public boolean isAvailable() {
|
||||||
return usingDeArrowAnywhere();
|
return usingDeArrowAnywhere();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return List.of(
|
||||||
|
ALT_THUMBNAIL_HOME,
|
||||||
|
ALT_THUMBNAIL_SUBSCRIPTIONS,
|
||||||
|
ALT_THUMBNAIL_LIBRARY,
|
||||||
|
ALT_THUMBNAIL_PLAYER,
|
||||||
|
ALT_THUMBNAIL_SEARCH
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class StillImagesAvailability implements Setting.Availability {
|
public static final class StillImagesAvailability implements Setting.Availability {
|
||||||
@@ -80,6 +92,17 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
public boolean isAvailable() {
|
public boolean isAvailable() {
|
||||||
return usingStillImagesAnywhere();
|
return usingStillImagesAnywhere();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return List.of(
|
||||||
|
ALT_THUMBNAIL_HOME,
|
||||||
|
ALT_THUMBNAIL_SUBSCRIPTIONS,
|
||||||
|
ALT_THUMBNAIL_LIBRARY,
|
||||||
|
ALT_THUMBNAIL_PLAYER,
|
||||||
|
ALT_THUMBNAIL_SEARCH
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum ThumbnailOption {
|
public enum ThumbnailOption {
|
||||||
@@ -451,29 +474,29 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final boolean useFastQuality = Settings.ALT_THUMBNAIL_STILLS_FAST.get();
|
final boolean useFastQuality = Settings.ALT_THUMBNAIL_STILLS_FAST.get();
|
||||||
switch (quality) {
|
return switch (quality) {
|
||||||
case SDDEFAULT:
|
// SD alt images have somewhat worse quality with washed out color and poor contrast.
|
||||||
// SD alt images have somewhat worse quality with washed out color and poor contrast.
|
// But the 720 images look much better and don't suffer from these issues.
|
||||||
// But the 720 images look much better and don't suffer from these issues.
|
// For unknown reasons, the 720 thumbnails are used only for the home feed,
|
||||||
// For unknown reasons, the 720 thumbnails are used only for the home feed,
|
// while SD is used for the search and subscription feed
|
||||||
// while SD is used for the search and subscription feed
|
// (even though search and subscriptions use the exact same layout as the home feed).
|
||||||
// (even though search and subscriptions use the exact same layout as the home feed).
|
// Of note, this image quality issue only appears with the alt thumbnail images,
|
||||||
// Of note, this image quality issue only appears with the alt thumbnail images,
|
// and the regular thumbnails have identical color/contrast quality for all sizes.
|
||||||
// and the regular thumbnails have identical color/contrast quality for all sizes.
|
// Fix this by falling thru and upgrading SD to 720.
|
||||||
// Fix this by falling thru and upgrading SD to 720.
|
case SDDEFAULT, HQ720 -> { // SD is max resolution for fast alt images.
|
||||||
case HQ720:
|
|
||||||
if (useFastQuality) {
|
if (useFastQuality) {
|
||||||
return SDDEFAULT; // SD is max resolution for fast alt images.
|
yield SDDEFAULT;
|
||||||
}
|
}
|
||||||
return HQ720;
|
yield HQ720;
|
||||||
case MAXRESDEFAULT:
|
}
|
||||||
|
case MAXRESDEFAULT -> {
|
||||||
if (useFastQuality) {
|
if (useFastQuality) {
|
||||||
return SDDEFAULT;
|
yield SDDEFAULT;
|
||||||
}
|
}
|
||||||
return MAXRESDEFAULT;
|
yield MAXRESDEFAULT;
|
||||||
default:
|
}
|
||||||
return quality;
|
default -> quality;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
final String originalName;
|
final String originalName;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class ChangeStartPagePatch {
|
public final class ChangeStartPagePatch {
|
||||||
|
|
||||||
@@ -87,6 +89,11 @@ public final class ChangeStartPagePatch {
|
|||||||
public boolean isAvailable() {
|
public boolean isAvailable() {
|
||||||
return Settings.CHANGE_START_PAGE.get() != StartPage.DEFAULT;
|
return Settings.CHANGE_START_PAGE.get() != StartPage.DEFAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return List.of(Settings.CHANGE_START_PAGE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import java.net.UnknownHostException;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@@ -68,7 +69,7 @@ public class CheckWatchHistoryDomainNameResolutionPatch {
|
|||||||
Utils.runOnMainThread(() -> {
|
Utils.runOnMainThread(() -> {
|
||||||
try {
|
try {
|
||||||
// Create the custom dialog.
|
// Create the custom dialog.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
str("revanced_check_watch_history_domain_name_dialog_title"), // Title.
|
str("revanced_check_watch_history_domain_name_dialog_title"), // Title.
|
||||||
Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML).
|
Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML).
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ public final class DownloadsPatch {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*
|
* <p>
|
||||||
* Called from the in app download hook,
|
* Called from the in app download hook,
|
||||||
* for both the player action button (below the video)
|
* for both the player action button (below the video)
|
||||||
* and the 'Download video' flyout option for feed videos.
|
* and the 'Download video' flyout option for feed videos.
|
||||||
*
|
* <p>
|
||||||
* Appears to always be called from the main thread.
|
* Appears to always be called from the main thread.
|
||||||
*/
|
*/
|
||||||
public static boolean inAppDownloadButtonOnClick(String videoId) {
|
public static boolean inAppDownloadButtonOnClick(String videoId) {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
@@ -37,11 +39,11 @@ public final class HidePlayerOverlayButtonsPatch {
|
|||||||
private static final boolean HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS_ENABLED
|
private static final boolean HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS_ENABLED
|
||||||
= Settings.HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS.get();
|
= Settings.HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS.get();
|
||||||
|
|
||||||
private static final int PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID =
|
private static final int PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID = getResourceIdentifierOrThrow(
|
||||||
Utils.getResourceIdentifier("player_control_previous_button_touch_area", "id");
|
"player_control_previous_button_touch_area", "id");
|
||||||
|
|
||||||
private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID =
|
private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID = getResourceIdentifierOrThrow(
|
||||||
Utils.getResourceIdentifier("player_control_next_button_touch_area", "id");
|
"player_control_next_button_touch_area", "id");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ import android.widget.TextView;
|
|||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "SpellCheckingInspection"})
|
@SuppressWarnings("SpellCheckingInspection")
|
||||||
public final class MiniplayerPatch {
|
public final class MiniplayerPatch {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -173,6 +175,14 @@ public final class MiniplayerPatch {
|
|||||||
public boolean isAvailable() {
|
public boolean isAvailable() {
|
||||||
return Settings.MINIPLAYER_TYPE.get().isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get();
|
return Settings.MINIPLAYER_TYPE.get().isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return List.of(
|
||||||
|
Settings.MINIPLAYER_TYPE,
|
||||||
|
Settings.MINIPLAYER_DRAG_AND_DROP
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final class MiniplayerHideOverlayButtonsAvailability implements Setting.Availability {
|
public static final class MiniplayerHideOverlayButtonsAvailability implements Setting.Availability {
|
||||||
@@ -185,11 +195,59 @@ public final class MiniplayerPatch {
|
|||||||
&& !Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() && !Settings.MINIPLAYER_DRAG_AND_DROP.get())
|
&& !Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() && !Settings.MINIPLAYER_DRAG_AND_DROP.get())
|
||||||
|| (IS_19_29_OR_GREATER && type == MODERN_3);
|
|| (IS_19_29_OR_GREATER && type == MODERN_3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return List.of(
|
||||||
|
Settings.MINIPLAYER_TYPE,
|
||||||
|
Settings.MINIPLAYER_DOUBLE_TAP_ACTION,
|
||||||
|
Settings.MINIPLAYER_DRAG_AND_DROP
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class MiniplayerAnyModernAvailability implements Setting.Availability {
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
MiniplayerType type = Settings.MINIPLAYER_TYPE.get();
|
||||||
|
return type == MODERN_1 || type == MODERN_2 || type == MODERN_3 || type == MODERN_4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return List.of(Settings.MINIPLAYER_TYPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class MiniplayerHideSubtextsAvailability implements Setting.Availability {
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
MiniplayerType type = Settings.MINIPLAYER_TYPE.get();
|
||||||
|
return type == MODERN_3 || type == MODERN_4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return List.of(Settings.MINIPLAYER_TYPE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class MiniplayerHideRewindOrOverlayOpacityAvailability implements Setting.Availability {
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
MiniplayerType type = Settings.MINIPLAYER_TYPE.get();
|
||||||
|
return type == MODERN_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return List.of(Settings.MINIPLAYER_TYPE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*
|
* <p>
|
||||||
* Enables a handler that immediately closes the miniplayer when the video is minimized,
|
* Enables a handler that immediately closes the miniplayer when the video is minimized,
|
||||||
* effectively disabling the miniplayer.
|
* effectively disabling the miniplayer.
|
||||||
*/
|
*/
|
||||||
@@ -378,4 +436,4 @@ public final class MiniplayerPatch {
|
|||||||
Logger.printException(() -> "playerOverlayGroupCreated failure", ex);
|
Logger.printException(() -> "playerOverlayGroupCreated failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package app.revanced.extension.youtube.patches;
|
|||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SeekbarThumbnailsPatch {
|
public class SeekbarThumbnailsPatch {
|
||||||
|
|
||||||
@@ -11,6 +13,11 @@ public class SeekbarThumbnailsPatch {
|
|||||||
public boolean isAvailable() {
|
public boolean isAvailable() {
|
||||||
return VersionCheckPatch.IS_19_17_OR_GREATER || !Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get();
|
return VersionCheckPatch.IS_19_17_OR_GREATER || !Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return List.of(Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final boolean SEEKBAR_THUMBNAILS_HIGH_QUALITY_ENABLED
|
private static final boolean SEEKBAR_THUMBNAILS_HIGH_QUALITY_ENABLED
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import android.view.View;
|
|||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -125,7 +126,7 @@ public final class AnnouncementsPatch {
|
|||||||
|
|
||||||
Utils.runOnMainThread(() -> {
|
Utils.runOnMainThread(() -> {
|
||||||
// Create the custom dialog and show the announcement.
|
// Create the custom dialog and show the announcement.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
finalTitle, // Title.
|
finalTitle, // Title.
|
||||||
finalMessage, // Message.
|
finalMessage, // Message.
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package app.revanced.extension.youtube.patches.components;
|
package app.revanced.extension.youtube.patches.components;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class PlayerFlyoutMenuItemsFilter extends Filter {
|
public class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||||
|
|
||||||
@@ -17,6 +20,11 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
|||||||
// without a restart the setting will show as available when it's not.
|
// without a restart the setting will show as available when it's not.
|
||||||
return AVAILABLE_ON_LAUNCH && !SpoofVideoStreamsPatch.spoofingToClientWithNoMultiAudioStreams();
|
return AVAILABLE_ON_LAUNCH && !SpoofVideoStreamsPatch.spoofingToClientWithNoMultiAudioStreams();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Setting<?>> getParentSettings() {
|
||||||
|
return List.of(BaseSettings.SPOOF_VIDEO_STREAMS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final ByteArrayFilterGroupList flyoutFilterGroupList = new ByteArrayFilterGroupList();
|
private final ByteArrayFilterGroupList flyoutFilterGroupList = new ByteArrayFilterGroupList();
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ package app.revanced.extension.youtube.patches.playback.speed;
|
|||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
import static app.revanced.extension.youtube.videoplayer.PlayerControlButton.fadeInDuration;
|
||||||
|
import static app.revanced.extension.youtube.videoplayer.PlayerControlButton.getDialogBackgroundColor;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.ColorFilter;
|
import android.graphics.ColorFilter;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
@@ -20,19 +20,9 @@ import android.graphics.drawable.shapes.RoundRectShape;
|
|||||||
import android.icu.text.NumberFormat;
|
import android.icu.text.NumberFormat;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.Gravity;
|
import android.view.Gravity;
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.Window;
|
import android.widget.*;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.TranslateAnimation;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.GridLayout;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.SeekBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
@@ -40,6 +30,7 @@ import java.util.function.Function;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.ui.SheetBottomDialog;
|
||||||
import app.revanced.extension.youtube.patches.VideoInformation;
|
import app.revanced.extension.youtube.patches.VideoInformation;
|
||||||
import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilter;
|
import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilter;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
@@ -97,7 +88,7 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
/**
|
/**
|
||||||
* Weak reference to the currently open dialog.
|
* Weak reference to the currently open dialog.
|
||||||
*/
|
*/
|
||||||
private static WeakReference<Dialog> currentDialog = new WeakReference<>(null);
|
private static WeakReference<SheetBottomDialog.SlideDialog> currentDialog;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// Use same 2 digit format as built in speed picker,
|
// Use same 2 digit format as built in speed picker,
|
||||||
@@ -268,368 +259,239 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
*/
|
*/
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
public static void showModernCustomPlaybackSpeedDialog(Context context) {
|
public static void showModernCustomPlaybackSpeedDialog(Context context) {
|
||||||
// Create a dialog without a theme for custom appearance.
|
try {
|
||||||
Dialog dialog = new Dialog(context);
|
// Create main layout.
|
||||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
|
SheetBottomDialog.DraggableLinearLayout mainLayout =
|
||||||
|
SheetBottomDialog.createMainLayout(context, getDialogBackgroundColor());
|
||||||
|
|
||||||
// Store the dialog reference.
|
// Preset size constants.
|
||||||
currentDialog = new WeakReference<>(dialog);
|
final int dip4 = dipToPixels(4);
|
||||||
|
final int dip8 = dipToPixels(8);
|
||||||
|
final int dip12 = dipToPixels(12);
|
||||||
|
final int dip20 = dipToPixels(20);
|
||||||
|
final int dip32 = dipToPixels(32);
|
||||||
|
final int dip60 = dipToPixels(60);
|
||||||
|
|
||||||
// Enable dismissing the dialog when tapping outside.
|
// Display current playback speed.
|
||||||
dialog.setCanceledOnTouchOutside(true);
|
TextView currentSpeedText = new TextView(context);
|
||||||
|
float currentSpeed = VideoInformation.getPlaybackSpeed();
|
||||||
|
// Initially show with only 0 minimum digits, so 1.0 shows as 1x.
|
||||||
|
currentSpeedText.setText(formatSpeedStringX(currentSpeed));
|
||||||
|
currentSpeedText.setTextColor(Utils.getAppForegroundColor());
|
||||||
|
currentSpeedText.setTextSize(16);
|
||||||
|
currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD);
|
||||||
|
currentSpeedText.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
textParams.setMargins(0, dip20, 0, 0);
|
||||||
|
currentSpeedText.setLayoutParams(textParams);
|
||||||
|
// Add current speed text view to main layout.
|
||||||
|
mainLayout.addView(currentSpeedText);
|
||||||
|
|
||||||
// Create main vertical LinearLayout for dialog content.
|
// Create horizontal layout for slider and +/- buttons.
|
||||||
LinearLayout mainLayout = new LinearLayout(context);
|
LinearLayout sliderLayout = new LinearLayout(context);
|
||||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
sliderLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
sliderLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
|
||||||
// Preset size constants.
|
// Create +/- buttons.
|
||||||
final int dip4 = dipToPixels(4); // Height for handle bar.
|
Button minusButton = createStyledButton(context, false, dip8, dip8);
|
||||||
final int dip5 = dipToPixels(5);
|
Button plusButton = createStyledButton(context, true, dip8, dip8);
|
||||||
final int dip6 = dipToPixels(6); // Padding for mainLayout from bottom.
|
|
||||||
final int dip8 = dipToPixels(8); // Padding for mainLayout from left and right.
|
|
||||||
final int dip20 = dipToPixels(20);
|
|
||||||
final int dip32 = dipToPixels(32); // Height for in-rows speed buttons.
|
|
||||||
final int dip36 = dipToPixels(36); // Height for minus and plus buttons.
|
|
||||||
final int dip40 = dipToPixels(40); // Width for handle bar.
|
|
||||||
final int dip60 = dipToPixels(60); // Height for speed button container.
|
|
||||||
|
|
||||||
mainLayout.setPadding(dip5, dip8, dip5, dip8);
|
// Create slider for speed adjustment.
|
||||||
|
SeekBar speedSlider = new SeekBar(context);
|
||||||
|
speedSlider.setFocusable(true);
|
||||||
|
speedSlider.setFocusableInTouchMode(true);
|
||||||
|
speedSlider.setMax(speedToProgressValue(customPlaybackSpeedsMax));
|
||||||
|
speedSlider.setProgress(speedToProgressValue(currentSpeed));
|
||||||
|
speedSlider.getProgressDrawable().setColorFilter(
|
||||||
|
Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar.
|
||||||
|
speedSlider.getThumb().setColorFilter(
|
||||||
|
Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme slider thumb.
|
||||||
|
LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams(
|
||||||
|
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
speedSlider.setLayoutParams(sliderParams);
|
||||||
|
|
||||||
// Set rounded rectangle background for the main layout.
|
// Add -/+ and slider views to slider layout.
|
||||||
RoundRectShape roundRectShape = new RoundRectShape(
|
sliderLayout.addView(minusButton);
|
||||||
Utils.createCornerRadii(12), null, null);
|
sliderLayout.addView(speedSlider);
|
||||||
ShapeDrawable background = new ShapeDrawable(roundRectShape);
|
sliderLayout.addView(plusButton);
|
||||||
background.getPaint().setColor(Utils.getDialogBackgroundColor());
|
|
||||||
mainLayout.setBackground(background);
|
|
||||||
|
|
||||||
// Add handle bar at the top.
|
// Add slider layout to main layout.
|
||||||
View handleBar = new View(context);
|
mainLayout.addView(sliderLayout);
|
||||||
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Utils.createCornerRadii(4), null, null));
|
|
||||||
handleBackground.getPaint().setColor(getAdjustedBackgroundColor(true));
|
|
||||||
handleBar.setBackground(handleBackground);
|
|
||||||
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(
|
|
||||||
dip40, // handle bar width.
|
|
||||||
dip4 // handle bar height.
|
|
||||||
);
|
|
||||||
handleParams.gravity = Gravity.CENTER_HORIZONTAL; // Center horizontally.
|
|
||||||
handleParams.setMargins(0, 0, 0, dip20); // 20dp bottom margins.
|
|
||||||
handleBar.setLayoutParams(handleParams);
|
|
||||||
// Add handle bar view to main layout.
|
|
||||||
mainLayout.addView(handleBar);
|
|
||||||
|
|
||||||
// Display current playback speed.
|
Function<Float, Void> userSelectedSpeed = newSpeed -> {
|
||||||
TextView currentSpeedText = new TextView(context);
|
final float roundedSpeed = roundSpeedToNearestIncrement(newSpeed);
|
||||||
float currentSpeed = VideoInformation.getPlaybackSpeed();
|
if (VideoInformation.getPlaybackSpeed() == roundedSpeed) {
|
||||||
// Initially show with only 0 minimum digits, so 1.0 shows as 1x
|
// Nothing has changed. New speed rounds to the current speed.
|
||||||
currentSpeedText.setText(formatSpeedStringX(currentSpeed));
|
return null;
|
||||||
currentSpeedText.setTextColor(Utils.getAppForegroundColor());
|
}
|
||||||
currentSpeedText.setTextSize(16);
|
|
||||||
currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD);
|
|
||||||
currentSpeedText.setGravity(Gravity.CENTER);
|
|
||||||
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
||||||
textParams.setMargins(0, 0, 0, 0);
|
|
||||||
currentSpeedText.setLayoutParams(textParams);
|
|
||||||
// Add current speed text view to main layout.
|
|
||||||
mainLayout.addView(currentSpeedText);
|
|
||||||
|
|
||||||
// Create horizontal layout for slider and +/- buttons.
|
currentSpeedText.setText(formatSpeedStringX(roundedSpeed)); // Update display.
|
||||||
LinearLayout sliderLayout = new LinearLayout(context);
|
speedSlider.setProgress(speedToProgressValue(roundedSpeed)); // Update slider.
|
||||||
sliderLayout.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
sliderLayout.setGravity(Gravity.CENTER_VERTICAL);
|
|
||||||
sliderLayout.setPadding(dip5, dip5, dip5, dip5); // 5dp padding.
|
|
||||||
|
|
||||||
// Create minus button.
|
RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
|
||||||
Button minusButton = new Button(context, null, 0); // Disable default theme style.
|
VideoInformation.overridePlaybackSpeed(roundedSpeed);
|
||||||
minusButton.setText(""); // No text on button.
|
|
||||||
ShapeDrawable minusBackground = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Utils.createCornerRadii(20), null, null));
|
|
||||||
minusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
|
|
||||||
minusButton.setBackground(minusBackground);
|
|
||||||
OutlineSymbolDrawable minusDrawable = new OutlineSymbolDrawable(false); // Minus symbol.
|
|
||||||
minusButton.setForeground(minusDrawable);
|
|
||||||
LinearLayout.LayoutParams minusParams = new LinearLayout.LayoutParams(dip36, dip36);
|
|
||||||
minusParams.setMargins(0, 0, dip5, 0); // 5dp to slider.
|
|
||||||
minusButton.setLayoutParams(minusParams);
|
|
||||||
|
|
||||||
// Create slider for speed adjustment.
|
|
||||||
SeekBar speedSlider = new SeekBar(context);
|
|
||||||
speedSlider.setMax(speedToProgressValue(customPlaybackSpeedsMax));
|
|
||||||
speedSlider.setProgress(speedToProgressValue(currentSpeed));
|
|
||||||
speedSlider.getProgressDrawable().setColorFilter(
|
|
||||||
Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar.
|
|
||||||
speedSlider.getThumb().setColorFilter(
|
|
||||||
Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme slider thumb.
|
|
||||||
LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams(
|
|
||||||
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
|
|
||||||
sliderParams.setMargins(dip5, 0, dip5, 0); // 5dp to -/+ buttons.
|
|
||||||
speedSlider.setLayoutParams(sliderParams);
|
|
||||||
|
|
||||||
// Create plus button.
|
|
||||||
Button plusButton = new Button(context, null, 0); // Disable default theme style.
|
|
||||||
plusButton.setText(""); // No text on button.
|
|
||||||
ShapeDrawable plusBackground = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Utils.createCornerRadii(20), null, null));
|
|
||||||
plusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
|
|
||||||
plusButton.setBackground(plusBackground);
|
|
||||||
OutlineSymbolDrawable plusDrawable = new OutlineSymbolDrawable(true); // Plus symbol.
|
|
||||||
plusButton.setForeground(plusDrawable);
|
|
||||||
LinearLayout.LayoutParams plusParams = new LinearLayout.LayoutParams(dip36, dip36);
|
|
||||||
plusParams.setMargins(dip5, 0, 0, 0); // 5dp to slider.
|
|
||||||
plusButton.setLayoutParams(plusParams);
|
|
||||||
|
|
||||||
// Add -/+ and slider views to slider layout.
|
|
||||||
sliderLayout.addView(minusButton);
|
|
||||||
sliderLayout.addView(speedSlider);
|
|
||||||
sliderLayout.addView(plusButton);
|
|
||||||
|
|
||||||
LinearLayout.LayoutParams sliderLayoutParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
||||||
sliderLayoutParams.setMargins(0, 0, 0, dip5); // 5dp bottom margin.
|
|
||||||
sliderLayout.setLayoutParams(sliderLayoutParams);
|
|
||||||
|
|
||||||
// Add slider layout to main layout.
|
|
||||||
mainLayout.addView(sliderLayout);
|
|
||||||
|
|
||||||
Function<Float, Void> userSelectedSpeed = newSpeed -> {
|
|
||||||
final float roundedSpeed = roundSpeedToNearestIncrement(newSpeed);
|
|
||||||
if (VideoInformation.getPlaybackSpeed() == roundedSpeed) {
|
|
||||||
// Nothing has changed. New speed rounds to the current speed.
|
|
||||||
return null;
|
return null;
|
||||||
}
|
};
|
||||||
|
|
||||||
currentSpeedText.setText(formatSpeedStringX(roundedSpeed)); // Update display.
|
// Set listener for slider to update playback speed.
|
||||||
speedSlider.setProgress(speedToProgressValue(roundedSpeed)); // Update slider.
|
speedSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
|
@Override
|
||||||
RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||||
VideoInformation.overridePlaybackSpeed(roundedSpeed);
|
if (fromUser) {
|
||||||
return null;
|
// Convert from progress value to video playback speed.
|
||||||
};
|
userSelectedSpeed.apply(customPlaybackSpeedsMin + (progress / PROGRESS_BAR_VALUE_SCALE));
|
||||||
|
}
|
||||||
// Set listener for slider to update playback speed.
|
|
||||||
speedSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
|
||||||
@Override
|
|
||||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
|
||||||
if (fromUser) {
|
|
||||||
// Convert from progress value to video playback speed.
|
|
||||||
userSelectedSpeed.apply(customPlaybackSpeedsMin + (progress / PROGRESS_BAR_VALUE_SCALE));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
minusButton.setOnClickListener(v -> userSelectedSpeed.apply(
|
||||||
|
(float) (VideoInformation.getPlaybackSpeed() - SPEED_ADJUSTMENT_CHANGE)));
|
||||||
|
plusButton.setOnClickListener(v -> userSelectedSpeed.apply(
|
||||||
|
(float) (VideoInformation.getPlaybackSpeed() + SPEED_ADJUSTMENT_CHANGE)));
|
||||||
|
|
||||||
|
// Create GridLayout for preset speed buttons.
|
||||||
|
GridLayout gridLayout = new GridLayout(context);
|
||||||
|
gridLayout.setColumnCount(5); // 5 columns for speed buttons.
|
||||||
|
gridLayout.setAlignmentMode(GridLayout.ALIGN_BOUNDS);
|
||||||
|
gridLayout.setRowCount((int) Math.ceil(customPlaybackSpeeds.length / 5.0));
|
||||||
|
LinearLayout.LayoutParams gridParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
gridParams.setMargins(dip4, dip12, dip4, dip12); // Speed buttons container.
|
||||||
|
gridLayout.setLayoutParams(gridParams);
|
||||||
|
|
||||||
|
// For button use 1 digit minimum.
|
||||||
|
speedFormatter.setMinimumFractionDigits(1);
|
||||||
|
|
||||||
|
// Add buttons for each preset playback speed.
|
||||||
|
for (float speed : customPlaybackSpeeds) {
|
||||||
|
// Container for button and optional label.
|
||||||
|
FrameLayout buttonContainer = new FrameLayout(context);
|
||||||
|
|
||||||
|
// Set layout parameters for each grid cell.
|
||||||
|
GridLayout.LayoutParams containerParams = new GridLayout.LayoutParams();
|
||||||
|
containerParams.width = 0; // Equal width for columns.
|
||||||
|
containerParams.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1, 1f);
|
||||||
|
containerParams.setMargins(dip4, 0, dip4, 0); // Button margins.
|
||||||
|
containerParams.height = dip60; // Fixed height for button and label.
|
||||||
|
buttonContainer.setLayoutParams(containerParams);
|
||||||
|
|
||||||
|
// Create speed button.
|
||||||
|
Button speedButton = new Button(context, null, 0);
|
||||||
|
speedButton.setText(speedFormatter.format(speed));
|
||||||
|
speedButton.setTextColor(Utils.getAppForegroundColor());
|
||||||
|
speedButton.setTextSize(12);
|
||||||
|
speedButton.setAllCaps(false);
|
||||||
|
speedButton.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
ShapeDrawable buttonBackground = new ShapeDrawable(new RoundRectShape(
|
||||||
|
Utils.createCornerRadii(20), null, null));
|
||||||
|
buttonBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
|
||||||
|
speedButton.setBackground(buttonBackground);
|
||||||
|
speedButton.setPadding(dip4, dip4, dip4, dip4);
|
||||||
|
|
||||||
|
// Center button vertically and stretch horizontally in container.
|
||||||
|
FrameLayout.LayoutParams buttonParams = new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT, dip32, Gravity.CENTER);
|
||||||
|
speedButton.setLayoutParams(buttonParams);
|
||||||
|
|
||||||
|
// Add speed buttons view to buttons container layout.
|
||||||
|
buttonContainer.addView(speedButton);
|
||||||
|
|
||||||
|
// Add "Normal" label for 1.0x speed.
|
||||||
|
if (speed == 1.0f) {
|
||||||
|
TextView normalLabel = new TextView(context);
|
||||||
|
// Use same 'Normal' string as stock YouTube.
|
||||||
|
normalLabel.setText(str("normal_playback_rate_label"));
|
||||||
|
normalLabel.setTextColor(Utils.getAppForegroundColor());
|
||||||
|
normalLabel.setTextSize(10);
|
||||||
|
normalLabel.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams labelParams = new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
|
||||||
|
labelParams.bottomMargin = 0; // Position label below button.
|
||||||
|
normalLabel.setLayoutParams(labelParams);
|
||||||
|
|
||||||
|
buttonContainer.addView(normalLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
speedButton.setOnClickListener(v -> userSelectedSpeed.apply(speed));
|
||||||
|
|
||||||
|
gridLayout.addView(buttonContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
// Restore 2 digit minimum.
|
||||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
speedFormatter.setMinimumFractionDigits(2);
|
||||||
|
|
||||||
@Override
|
// Add in-rows speed buttons layout to main layout.
|
||||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
mainLayout.addView(gridLayout);
|
||||||
});
|
|
||||||
|
|
||||||
minusButton.setOnClickListener(v -> userSelectedSpeed.apply(
|
// Create dialog.
|
||||||
(float) (VideoInformation.getPlaybackSpeed() - SPEED_ADJUSTMENT_CHANGE)));
|
SheetBottomDialog.SlideDialog dialog = SheetBottomDialog.createSlideDialog(context, mainLayout, fadeInDuration);
|
||||||
plusButton.setOnClickListener(v -> userSelectedSpeed.apply(
|
currentDialog = new WeakReference<>(dialog);
|
||||||
(float) (VideoInformation.getPlaybackSpeed() + SPEED_ADJUSTMENT_CHANGE)));
|
|
||||||
|
|
||||||
// Create GridLayout for preset speed buttons.
|
// Create observer for PlayerType changes.
|
||||||
GridLayout gridLayout = new GridLayout(context);
|
Function1<PlayerType, Unit> playerTypeObserver = new Function1<>() {
|
||||||
gridLayout.setColumnCount(5); // 5 columns for speed buttons.
|
@Override
|
||||||
gridLayout.setAlignmentMode(GridLayout.ALIGN_BOUNDS);
|
public Unit invoke(PlayerType type) {
|
||||||
gridLayout.setRowCount((int) Math.ceil(customPlaybackSpeeds.length / 5.0));
|
SheetBottomDialog.SlideDialog current = currentDialog.get();
|
||||||
LinearLayout.LayoutParams gridParams = new LinearLayout.LayoutParams(
|
if (current == null || !current.isShowing()) {
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
// Should never happen.
|
||||||
gridParams.setMargins(0, 0, 0, 0); // No margins around GridLayout.
|
PlayerType.getOnChange().removeObserver(this);
|
||||||
gridLayout.setLayoutParams(gridParams);
|
Logger.printException(() -> "Removing player type listener as dialog is null or closed");
|
||||||
|
} else if (type == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) {
|
||||||
|
current.dismiss();
|
||||||
|
Logger.printDebug(() -> "Playback speed dialog dismissed due to PiP mode");
|
||||||
|
}
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// For button use 1 digit minimum.
|
// Add observer to dismiss dialog when entering PiP mode.
|
||||||
speedFormatter.setMinimumFractionDigits(1);
|
PlayerType.getOnChange().addObserver(playerTypeObserver);
|
||||||
|
|
||||||
// Add buttons for each preset playback speed.
|
// Remove observer when dialog is dismissed.
|
||||||
for (float speed : customPlaybackSpeeds) {
|
dialog.setOnDismissListener(d -> {
|
||||||
// Container for button and optional label.
|
PlayerType.getOnChange().removeObserver(playerTypeObserver);
|
||||||
FrameLayout buttonContainer = new FrameLayout(context);
|
Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss");
|
||||||
|
});
|
||||||
|
|
||||||
// Set layout parameters for each grid cell.
|
dialog.show(); // Show the dialog.
|
||||||
GridLayout.LayoutParams containerParams = new GridLayout.LayoutParams();
|
|
||||||
containerParams.width = 0; // Equal width for columns.
|
|
||||||
containerParams.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1, 1f);
|
|
||||||
containerParams.setMargins(dip5, 0, dip5, 0); // Button margins.
|
|
||||||
containerParams.height = dip60; // Fixed height for button and label.
|
|
||||||
buttonContainer.setLayoutParams(containerParams);
|
|
||||||
|
|
||||||
// Create speed button.
|
} catch (Exception ex) {
|
||||||
Button speedButton = new Button(context, null, 0);
|
Logger.printException(() -> "showModernCustomPlaybackSpeedDialog failure", ex);
|
||||||
speedButton.setText(speedFormatter.format(speed));
|
|
||||||
speedButton.setTextColor(Utils.getAppForegroundColor());
|
|
||||||
speedButton.setTextSize(12);
|
|
||||||
speedButton.setAllCaps(false);
|
|
||||||
speedButton.setGravity(Gravity.CENTER);
|
|
||||||
|
|
||||||
ShapeDrawable buttonBackground = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Utils.createCornerRadii(20), null, null));
|
|
||||||
buttonBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
|
|
||||||
speedButton.setBackground(buttonBackground);
|
|
||||||
speedButton.setPadding(dip5, dip5, dip5, dip5);
|
|
||||||
|
|
||||||
// Center button vertically and stretch horizontally in container.
|
|
||||||
FrameLayout.LayoutParams buttonParams = new FrameLayout.LayoutParams(
|
|
||||||
FrameLayout.LayoutParams.MATCH_PARENT, dip32, Gravity.CENTER);
|
|
||||||
speedButton.setLayoutParams(buttonParams);
|
|
||||||
|
|
||||||
// Add speed buttons view to buttons container layout.
|
|
||||||
buttonContainer.addView(speedButton);
|
|
||||||
|
|
||||||
// Add "Normal" label for 1.0x speed.
|
|
||||||
if (speed == 1.0f) {
|
|
||||||
TextView normalLabel = new TextView(context);
|
|
||||||
// Use same 'Normal' string as stock YouTube.
|
|
||||||
normalLabel.setText(str("normal_playback_rate_label"));
|
|
||||||
normalLabel.setTextColor(Utils.getAppForegroundColor());
|
|
||||||
normalLabel.setTextSize(10);
|
|
||||||
normalLabel.setGravity(Gravity.CENTER);
|
|
||||||
|
|
||||||
FrameLayout.LayoutParams labelParams = new FrameLayout.LayoutParams(
|
|
||||||
FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
||||||
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
|
|
||||||
labelParams.bottomMargin = 0; // Position label below button.
|
|
||||||
normalLabel.setLayoutParams(labelParams);
|
|
||||||
|
|
||||||
buttonContainer.addView(normalLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
speedButton.setOnClickListener(v -> userSelectedSpeed.apply(speed));
|
|
||||||
|
|
||||||
gridLayout.addView(buttonContainer);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Restore 2 digit minimum.
|
/**
|
||||||
speedFormatter.setMinimumFractionDigits(2);
|
* Creates a styled button with a plus or minus symbol.
|
||||||
|
*
|
||||||
// Add in-rows speed buttons layout to main layout.
|
* @param context The Android context used to create the button.
|
||||||
mainLayout.addView(gridLayout);
|
* @param isPlus True to display a plus symbol, false to display a minus symbol.
|
||||||
|
* @param marginStart The start margin in pixels (left for LTR, right for RTL).
|
||||||
// Wrap mainLayout in another LinearLayout for side margins.
|
* @param marginEnd The end margin in pixels (right for LTR, left for RTL).
|
||||||
LinearLayout wrapperLayout = new LinearLayout(context);
|
* @return A configured {@link Button} with the specified styling and layout parameters.
|
||||||
wrapperLayout.setOrientation(LinearLayout.VERTICAL);
|
*/
|
||||||
wrapperLayout.setPadding(dip8, 0, dip8, 0); // 8dp side margins.
|
private static Button createStyledButton(Context context, boolean isPlus, int marginStart, int marginEnd) {
|
||||||
wrapperLayout.addView(mainLayout);
|
Button button = new Button(context, null, 0); // Disable default theme style.
|
||||||
dialog.setContentView(wrapperLayout);
|
button.setText(""); // No text on button.
|
||||||
|
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||||
// Configure dialog window to appear at the bottom.
|
Utils.createCornerRadii(20), null, null));
|
||||||
Window window = dialog.getWindow();
|
background.getPaint().setColor(getAdjustedBackgroundColor(false));
|
||||||
if (window != null) {
|
button.setBackground(background);
|
||||||
WindowManager.LayoutParams params = window.getAttributes();
|
button.setForeground(new OutlineSymbolDrawable(isPlus)); // Plus or minus symbol.
|
||||||
params.gravity = Gravity.BOTTOM; // Position at bottom of screen.
|
final int dip36 = dipToPixels(36);
|
||||||
params.y = dip6; // 6dp margin from bottom.
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(dip36, dip36);
|
||||||
// In landscape, use the smaller dimension (height) as portrait width.
|
params.setMargins(marginStart, 0, marginEnd, 0); // Set margins.
|
||||||
int portraitWidth = context.getResources().getDisplayMetrics().widthPixels;
|
button.setLayoutParams(params);
|
||||||
if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
return button;
|
||||||
portraitWidth = Math.min(
|
|
||||||
portraitWidth,
|
|
||||||
context.getResources().getDisplayMetrics().heightPixels);
|
|
||||||
}
|
|
||||||
params.width = portraitWidth; // Use portrait width.
|
|
||||||
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
|
||||||
window.setAttributes(params);
|
|
||||||
window.setBackgroundDrawable(null); // Remove default dialog background.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply slide-in animation when showing the dialog.
|
|
||||||
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
|
|
||||||
Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
|
|
||||||
slideInABottomAnimation.setDuration(fadeDurationFast);
|
|
||||||
mainLayout.startAnimation(slideInABottomAnimation);
|
|
||||||
|
|
||||||
// Set touch listener on mainLayout to enable drag-to-dismiss.
|
|
||||||
//noinspection ClickableViewAccessibility
|
|
||||||
mainLayout.setOnTouchListener(new View.OnTouchListener() {
|
|
||||||
/** Threshold for dismissing the dialog. */
|
|
||||||
final float dismissThreshold = dipToPixels(100); // Distance to drag to dismiss.
|
|
||||||
/** Store initial Y position of touch. */
|
|
||||||
float touchY;
|
|
||||||
/** Track current translation. */
|
|
||||||
float translationY;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTouch(View v, MotionEvent event) {
|
|
||||||
switch (event.getAction()) {
|
|
||||||
case MotionEvent.ACTION_DOWN:
|
|
||||||
// Capture initial Y position of touch.
|
|
||||||
touchY = event.getRawY();
|
|
||||||
translationY = mainLayout.getTranslationY();
|
|
||||||
return true;
|
|
||||||
case MotionEvent.ACTION_MOVE:
|
|
||||||
// Calculate drag distance and apply translation downwards only.
|
|
||||||
final float deltaY = event.getRawY() - touchY;
|
|
||||||
// Only allow downward drag (positive deltaY).
|
|
||||||
if (deltaY >= 0) {
|
|
||||||
mainLayout.setTranslationY(translationY + deltaY);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
// Check if dialog should be dismissed based on drag distance.
|
|
||||||
if (mainLayout.getTranslationY() > dismissThreshold) {
|
|
||||||
// Animate dialog off-screen and dismiss.
|
|
||||||
//noinspection ExtractMethodRecommender
|
|
||||||
final float remainingDistance = context.getResources().getDisplayMetrics().heightPixels
|
|
||||||
- mainLayout.getTop();
|
|
||||||
TranslateAnimation slideOut = new TranslateAnimation(
|
|
||||||
0, 0, mainLayout.getTranslationY(), remainingDistance);
|
|
||||||
slideOut.setDuration(fadeDurationFast);
|
|
||||||
slideOut.setAnimationListener(new Animation.AnimationListener() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animation animation) {}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animation animation) {
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationRepeat(Animation animation) {}
|
|
||||||
});
|
|
||||||
mainLayout.startAnimation(slideOut);
|
|
||||||
} else {
|
|
||||||
// Animate back to original position if not dragged far enough.
|
|
||||||
TranslateAnimation slideBack = new TranslateAnimation(
|
|
||||||
0, 0, mainLayout.getTranslationY(), 0);
|
|
||||||
slideBack.setDuration(fadeDurationFast);
|
|
||||||
mainLayout.startAnimation(slideBack);
|
|
||||||
mainLayout.setTranslationY(0);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create observer for PlayerType changes.
|
|
||||||
Function1<PlayerType, Unit> playerTypeObserver = new Function1<>() {
|
|
||||||
@Override
|
|
||||||
public Unit invoke(PlayerType type) {
|
|
||||||
Dialog current = currentDialog.get();
|
|
||||||
if (current == null || !current.isShowing()) {
|
|
||||||
// Should never happen.
|
|
||||||
PlayerType.getOnChange().removeObserver(this);
|
|
||||||
Logger.printException(() -> "Removing player type listener as dialog is null or closed");
|
|
||||||
} else if (type == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) {
|
|
||||||
current.dismiss();
|
|
||||||
Logger.printDebug(() -> "Playback speed dialog dismissed due to PiP mode");
|
|
||||||
}
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add observer to dismiss dialog when entering PiP mode.
|
|
||||||
PlayerType.getOnChange().addObserver(playerTypeObserver);
|
|
||||||
|
|
||||||
// Remove observer when dialog is dismissed.
|
|
||||||
dialog.setOnDismissListener(d -> {
|
|
||||||
PlayerType.getOnChange().removeObserver(playerTypeObserver);
|
|
||||||
Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss");
|
|
||||||
});
|
|
||||||
|
|
||||||
dialog.show(); // Display the dialog.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -674,10 +536,9 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
* for light themes to ensure visual contrast.
|
* for light themes to ensure visual contrast.
|
||||||
*/
|
*/
|
||||||
public static int getAdjustedBackgroundColor(boolean isHandleBar) {
|
public static int getAdjustedBackgroundColor(boolean isHandleBar) {
|
||||||
final int baseColor = Utils.getDialogBackgroundColor();
|
|
||||||
final float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
|
final float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
|
||||||
final float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
|
final float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
|
||||||
return Utils.adjustColorBrightness(baseColor, lightThemeFactor, darkThemeFactor);
|
return Utils.adjustColorBrightness(getDialogBackgroundColor(), lightThemeFactor, darkThemeFactor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -151,13 +151,10 @@ public final class SeekbarColorPatch {
|
|||||||
String seekbarStyle = get9BitStyleIdentifier(customSeekbarColor);
|
String seekbarStyle = get9BitStyleIdentifier(customSeekbarColor);
|
||||||
Logger.printDebug(() -> "Using splash seekbar style: " + seekbarStyle);
|
Logger.printDebug(() -> "Using splash seekbar style: " + seekbarStyle);
|
||||||
|
|
||||||
final int styleIdentifierDefault = Utils.getResourceIdentifier(
|
final int styleIdentifierDefault = Utils.getResourceIdentifierOrThrow(
|
||||||
seekbarStyle,
|
seekbarStyle,
|
||||||
"style"
|
"style"
|
||||||
);
|
);
|
||||||
if (styleIdentifierDefault == 0) {
|
|
||||||
throw new RuntimeException("Seekbar style not found: " + seekbarStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
Resources.Theme theme = Utils.getContext().getResources().newTheme();
|
Resources.Theme theme = Utils.getContext().getResources().newTheme();
|
||||||
theme.applyStyle(styleIdentifierDefault, true);
|
theme.applyStyle(styleIdentifierDefault, true);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package app.revanced.extension.youtube.returnyoutubedislike.ui;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import app.revanced.extension.youtube.settings.preference.UrlLinkPreference;
|
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows tapping the RYD about preference to open the website.
|
* Allows tapping the RYD about preference to open the website.
|
||||||
|
|||||||
@@ -1,414 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.settings;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.drawable.GradientDrawable;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.inputmethod.EditorInfo;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.AutoCompleteTextView;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.SearchView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toolbar;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.settings.StringSetting;
|
|
||||||
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Controller for managing the search view in ReVanced settings.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"deprecated", "DiscouragedApi"})
|
|
||||||
public class SearchViewController {
|
|
||||||
private static final int MAX_HISTORY_SIZE = 5;
|
|
||||||
|
|
||||||
private final SearchView searchView;
|
|
||||||
private final FrameLayout searchContainer;
|
|
||||||
private final Toolbar toolbar;
|
|
||||||
private final Activity activity;
|
|
||||||
private boolean isSearchActive;
|
|
||||||
private final CharSequence originalTitle;
|
|
||||||
private final Deque<String> searchHistory;
|
|
||||||
private final AutoCompleteTextView autoCompleteTextView;
|
|
||||||
private final boolean showSettingsSearchHistory;
|
|
||||||
private int currentOrientation;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a background drawable for the SearchView with rounded corners.
|
|
||||||
*/
|
|
||||||
private static GradientDrawable createBackgroundDrawable(Context context) {
|
|
||||||
GradientDrawable background = new GradientDrawable();
|
|
||||||
background.setShape(GradientDrawable.RECTANGLE);
|
|
||||||
background.setCornerRadius(28 * context.getResources().getDisplayMetrics().density); // 28dp corner radius.
|
|
||||||
background.setColor(getSearchViewBackground());
|
|
||||||
return background;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a background drawable for suggestion items with rounded corners.
|
|
||||||
*/
|
|
||||||
private static GradientDrawable createSuggestionBackgroundDrawable(Context context) {
|
|
||||||
GradientDrawable background = new GradientDrawable();
|
|
||||||
background.setShape(GradientDrawable.RECTANGLE);
|
|
||||||
background.setColor(getSearchViewBackground());
|
|
||||||
return background;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
public static int getSearchViewBackground() {
|
|
||||||
return Utils.isDarkModeEnabled()
|
|
||||||
? Utils.adjustColorBrightness(Utils.getDialogBackgroundColor(), 1.11f)
|
|
||||||
: Utils.adjustColorBrightness(Utils.getThemeLightColor(), 0.95f);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds search view components to the activity.
|
|
||||||
*/
|
|
||||||
public static SearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
|
|
||||||
return new SearchViewController(activity, toolbar, fragment);
|
|
||||||
}
|
|
||||||
|
|
||||||
private SearchViewController(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.toolbar = toolbar;
|
|
||||||
this.originalTitle = toolbar.getTitle();
|
|
||||||
this.showSettingsSearchHistory = Settings.SETTINGS_SEARCH_HISTORY.get();
|
|
||||||
this.searchHistory = new LinkedList<>();
|
|
||||||
this.currentOrientation = activity.getResources().getConfiguration().orientation;
|
|
||||||
StringSetting searchEntries = Settings.SETTINGS_SEARCH_ENTRIES;
|
|
||||||
if (showSettingsSearchHistory) {
|
|
||||||
String entries = searchEntries.get();
|
|
||||||
if (!entries.isBlank()) {
|
|
||||||
searchHistory.addAll(Arrays.asList(entries.split("\n")));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Clear old saved history if the user turns off the feature.
|
|
||||||
searchEntries.resetToDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve SearchView and container from XML.
|
|
||||||
searchView = activity.findViewById(getResourceIdentifier(
|
|
||||||
"revanced_search_view", "id"));
|
|
||||||
searchContainer = activity.findViewById(getResourceIdentifier(
|
|
||||||
"revanced_search_view_container", "id"));
|
|
||||||
|
|
||||||
// Initialize AutoCompleteTextView.
|
|
||||||
autoCompleteTextView = searchView.findViewById(
|
|
||||||
searchView.getContext().getResources().getIdentifier(
|
|
||||||
"android:id/search_src_text", null, null));
|
|
||||||
|
|
||||||
// Disable fullscreen keyboard mode.
|
|
||||||
autoCompleteTextView.setImeOptions(autoCompleteTextView.getImeOptions() | EditorInfo.IME_FLAG_NO_EXTRACT_UI);
|
|
||||||
|
|
||||||
// Set background and query hint.
|
|
||||||
searchView.setBackground(createBackgroundDrawable(toolbar.getContext()));
|
|
||||||
searchView.setQueryHint(str("revanced_settings_search_hint"));
|
|
||||||
|
|
||||||
// Configure RTL support based on app language.
|
|
||||||
AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get();
|
|
||||||
if (Utils.isRightToLeftLocale(appLanguage.getLocale())) {
|
|
||||||
searchView.setTextDirection(View.TEXT_DIRECTION_RTL);
|
|
||||||
searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up search history suggestions.
|
|
||||||
if (showSettingsSearchHistory) {
|
|
||||||
setupSearchHistory();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up query text listener.
|
|
||||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextSubmit(String query) {
|
|
||||||
try {
|
|
||||||
String queryTrimmed = query.trim();
|
|
||||||
if (!queryTrimmed.isEmpty()) {
|
|
||||||
saveSearchQuery(queryTrimmed);
|
|
||||||
}
|
|
||||||
// Hide suggestions on submit.
|
|
||||||
if (showSettingsSearchHistory && autoCompleteTextView != null) {
|
|
||||||
autoCompleteTextView.dismissDropDown();
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onQueryTextSubmit failure", ex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onQueryTextChange(String newText) {
|
|
||||||
try {
|
|
||||||
Logger.printDebug(() -> "Search query: " + newText);
|
|
||||||
fragment.filterPreferences(newText);
|
|
||||||
// Prevent suggestions from showing during text input.
|
|
||||||
if (showSettingsSearchHistory && autoCompleteTextView != null) {
|
|
||||||
if (!newText.isEmpty()) {
|
|
||||||
autoCompleteTextView.dismissDropDown();
|
|
||||||
autoCompleteTextView.setThreshold(Integer.MAX_VALUE); // Disable autocomplete suggestions.
|
|
||||||
} else {
|
|
||||||
autoCompleteTextView.setThreshold(1); // Re-enable for empty input.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onQueryTextChange failure", ex);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set menu and search icon.
|
|
||||||
final int actionSearchId = getResourceIdentifier("action_search", "id");
|
|
||||||
toolbar.inflateMenu(getResourceIdentifier("revanced_search_menu", "menu"));
|
|
||||||
MenuItem searchItem = toolbar.getMenu().findItem(actionSearchId);
|
|
||||||
|
|
||||||
// Set menu item click listener.
|
|
||||||
toolbar.setOnMenuItemClickListener(item -> {
|
|
||||||
try {
|
|
||||||
if (item.getItemId() == actionSearchId) {
|
|
||||||
if (!isSearchActive) {
|
|
||||||
openSearch();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "menu click failure", ex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set navigation click listener.
|
|
||||||
toolbar.setNavigationOnClickListener(view -> {
|
|
||||||
try {
|
|
||||||
if (isSearchActive) {
|
|
||||||
closeSearch();
|
|
||||||
} else {
|
|
||||||
activity.finish();
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "navigation click failure", ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up the search history suggestions for the SearchView with custom adapter.
|
|
||||||
*/
|
|
||||||
private void setupSearchHistory() {
|
|
||||||
if (autoCompleteTextView != null) {
|
|
||||||
SearchHistoryAdapter adapter = new SearchHistoryAdapter(activity, new ArrayList<>(searchHistory));
|
|
||||||
autoCompleteTextView.setAdapter(adapter);
|
|
||||||
autoCompleteTextView.setThreshold(1); // Initial threshold for empty input.
|
|
||||||
autoCompleteTextView.setLongClickable(true);
|
|
||||||
|
|
||||||
// Show suggestions only when search bar is active and query is empty.
|
|
||||||
autoCompleteTextView.setOnFocusChangeListener((v, hasFocus) -> {
|
|
||||||
if (hasFocus && isSearchActive && autoCompleteTextView.getText().length() == 0) {
|
|
||||||
autoCompleteTextView.showDropDown();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves a search query to the search history.
|
|
||||||
* @param query The search query to save.
|
|
||||||
*/
|
|
||||||
private void saveSearchQuery(String query) {
|
|
||||||
if (!showSettingsSearchHistory) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
searchHistory.remove(query); // Remove if already exists to update position.
|
|
||||||
searchHistory.addFirst(query); // Add to the most recent.
|
|
||||||
|
|
||||||
// Remove extra old entries.
|
|
||||||
while (searchHistory.size() > MAX_HISTORY_SIZE) {
|
|
||||||
String last = searchHistory.removeLast();
|
|
||||||
Logger.printDebug(() -> "Removing search history query: " + last);
|
|
||||||
}
|
|
||||||
|
|
||||||
saveSearchHistory();
|
|
||||||
|
|
||||||
updateSearchHistoryAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes a search query from the search history.
|
|
||||||
* @param query The search query to remove.
|
|
||||||
*/
|
|
||||||
private void removeSearchQuery(String query) {
|
|
||||||
searchHistory.remove(query);
|
|
||||||
|
|
||||||
saveSearchHistory();
|
|
||||||
|
|
||||||
updateSearchHistoryAdapter();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the search history to the shared preferences.
|
|
||||||
*/
|
|
||||||
private void saveSearchHistory() {
|
|
||||||
Logger.printDebug(() -> "Saving search history: " + searchHistory);
|
|
||||||
|
|
||||||
Settings.SETTINGS_SEARCH_ENTRIES.save(
|
|
||||||
String.join("\n", searchHistory)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the search history adapter with the latest history.
|
|
||||||
*/
|
|
||||||
private void updateSearchHistoryAdapter() {
|
|
||||||
if (autoCompleteTextView == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SearchHistoryAdapter adapter = (SearchHistoryAdapter) autoCompleteTextView.getAdapter();
|
|
||||||
if (adapter != null) {
|
|
||||||
adapter.clear();
|
|
||||||
adapter.addAll(searchHistory);
|
|
||||||
adapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void handleOrientationChange(int newOrientation) {
|
|
||||||
if (newOrientation != currentOrientation) {
|
|
||||||
currentOrientation = newOrientation;
|
|
||||||
if (autoCompleteTextView != null) {
|
|
||||||
autoCompleteTextView.dismissDropDown();
|
|
||||||
Logger.printDebug(() -> "Orientation changed, search history dismissed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opens the search view and shows the keyboard.
|
|
||||||
*/
|
|
||||||
private void openSearch() {
|
|
||||||
isSearchActive = true;
|
|
||||||
toolbar.getMenu().findItem(getResourceIdentifier(
|
|
||||||
"action_search", "id")).setVisible(false);
|
|
||||||
toolbar.setTitle("");
|
|
||||||
searchContainer.setVisibility(View.VISIBLE);
|
|
||||||
searchView.requestFocus();
|
|
||||||
|
|
||||||
// Show keyboard.
|
|
||||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
imm.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT);
|
|
||||||
|
|
||||||
// Show suggestions with a slight delay.
|
|
||||||
if (showSettingsSearchHistory && autoCompleteTextView != null && autoCompleteTextView.getText().length() == 0) {
|
|
||||||
searchView.postDelayed(() -> {
|
|
||||||
if (isSearchActive && autoCompleteTextView.getText().length() == 0) {
|
|
||||||
autoCompleteTextView.showDropDown();
|
|
||||||
}
|
|
||||||
}, 100); // 100ms delay to ensure focus is stable.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes the search view and hides the keyboard.
|
|
||||||
*/
|
|
||||||
public void closeSearch() {
|
|
||||||
isSearchActive = false;
|
|
||||||
toolbar.getMenu().findItem(getResourceIdentifier(
|
|
||||||
"action_search", "id")).setVisible(true);
|
|
||||||
toolbar.setTitle(originalTitle);
|
|
||||||
searchContainer.setVisibility(View.GONE);
|
|
||||||
searchView.setQuery("", false);
|
|
||||||
|
|
||||||
// Hide keyboard.
|
|
||||||
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
imm.hideSoftInputFromWindow(searchView.getWindowToken(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean handleBackPress() {
|
|
||||||
if (LicenseActivityHook.searchViewController != null
|
|
||||||
&& LicenseActivityHook.searchViewController.isSearchActive()) {
|
|
||||||
LicenseActivityHook.searchViewController.closeSearch();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSearchActive() {
|
|
||||||
return isSearchActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Custom ArrayAdapter for search history.
|
|
||||||
*/
|
|
||||||
private class SearchHistoryAdapter extends ArrayAdapter<String> {
|
|
||||||
public SearchHistoryAdapter(Context context, List<String> history) {
|
|
||||||
super(context, 0, history);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
|
|
||||||
if (convertView == null) {
|
|
||||||
convertView = LinearLayout.inflate(getContext(), getResourceIdentifier(
|
|
||||||
"revanced_search_suggestion_item", "layout"), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply rounded corners programmatically.
|
|
||||||
convertView.setBackground(createSuggestionBackgroundDrawable(getContext()));
|
|
||||||
String query = getItem(position);
|
|
||||||
|
|
||||||
// Set query text.
|
|
||||||
TextView textView = convertView.findViewById(getResourceIdentifier(
|
|
||||||
"suggestion_text", "id"));
|
|
||||||
if (textView != null) {
|
|
||||||
textView.setText(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set click listener for inserting query into SearchView.
|
|
||||||
convertView.setOnClickListener(v -> {
|
|
||||||
searchView.setQuery(query, true); // Insert selected query and submit.
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set long click listener for deletion confirmation.
|
|
||||||
convertView.setOnLongClickListener(v -> {
|
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
|
||||||
activity,
|
|
||||||
query, // Title.
|
|
||||||
str("revanced_settings_search_remove_message"), // Message.
|
|
||||||
null, // No EditText.
|
|
||||||
null, // OK button text.
|
|
||||||
() -> removeSearchQuery(query), // OK button action.
|
|
||||||
() -> {}, // Cancel button action (dismiss only).
|
|
||||||
null, // No Neutral button text.
|
|
||||||
() -> {}, // Neutral button action (dismiss only).
|
|
||||||
true // Dismiss dialog when onNeutralClick.
|
|
||||||
);
|
|
||||||
|
|
||||||
Dialog dialog = dialogPair.first;
|
|
||||||
dialog.setCancelable(true); // Allow dismissal via back button.
|
|
||||||
dialog.show(); // Show the dialog.
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
return convertView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package app.revanced.extension.youtube.settings;
|
|||||||
|
|
||||||
import static java.lang.Boolean.FALSE;
|
import static java.lang.Boolean.FALSE;
|
||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
import static app.revanced.extension.shared.settings.Setting.Availability;
|
|
||||||
import static app.revanced.extension.shared.settings.Setting.migrateOldSettingToNew;
|
import static app.revanced.extension.shared.settings.Setting.migrateOldSettingToNew;
|
||||||
import static app.revanced.extension.shared.settings.Setting.parent;
|
import static app.revanced.extension.shared.settings.Setting.parent;
|
||||||
import static app.revanced.extension.shared.settings.Setting.parentsAll;
|
import static app.revanced.extension.shared.settings.Setting.parentsAll;
|
||||||
@@ -15,10 +14,6 @@ import static app.revanced.extension.youtube.patches.ExitFullscreenPatch.Fullscr
|
|||||||
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerHorizontalDragAvailability;
|
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerHorizontalDragAvailability;
|
||||||
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType;
|
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType;
|
||||||
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MINIMAL;
|
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MINIMAL;
|
||||||
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1;
|
|
||||||
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_2;
|
|
||||||
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3;
|
|
||||||
import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_4;
|
|
||||||
import static app.revanced.extension.youtube.patches.OpenShortsInRegularPlayerPatch.ShortsPlayerType;
|
import static app.revanced.extension.youtube.patches.OpenShortsInRegularPlayerPatch.ShortsPlayerType;
|
||||||
import static app.revanced.extension.youtube.patches.SeekbarThumbnailsPatch.SeekbarThumbnailsHighQualityAvailability;
|
import static app.revanced.extension.youtube.patches.SeekbarThumbnailsPatch.SeekbarThumbnailsHighQualityAvailability;
|
||||||
import static app.revanced.extension.youtube.patches.components.PlayerFlyoutMenuItemsFilter.HideAudioFlyoutMenuAvailability;
|
import static app.revanced.extension.youtube.patches.components.PlayerFlyoutMenuItemsFilter.HideAudioFlyoutMenuAvailability;
|
||||||
@@ -179,16 +174,15 @@ public class Settings extends BaseSettings {
|
|||||||
|
|
||||||
// Miniplayer
|
// Miniplayer
|
||||||
public static final EnumSetting<MiniplayerType> MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.DEFAULT, true);
|
public static final EnumSetting<MiniplayerType> MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.DEFAULT, true);
|
||||||
private static final Availability MINIPLAYER_ANY_MODERN = MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3, MODERN_4);
|
public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, new MiniplayerPatch.MiniplayerAnyModernAvailability());
|
||||||
public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, MINIPLAYER_ANY_MODERN);
|
public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, new MiniplayerPatch.MiniplayerAnyModernAvailability());
|
||||||
public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, MINIPLAYER_ANY_MODERN);
|
|
||||||
public static final BooleanSetting MINIPLAYER_HORIZONTAL_DRAG = new BooleanSetting("revanced_miniplayer_horizontal_drag", FALSE, true, new MiniplayerHorizontalDragAvailability());
|
public static final BooleanSetting MINIPLAYER_HORIZONTAL_DRAG = new BooleanSetting("revanced_miniplayer_horizontal_drag", FALSE, true, new MiniplayerHorizontalDragAvailability());
|
||||||
public static final BooleanSetting MINIPLAYER_HIDE_OVERLAY_BUTTONS = new BooleanSetting("revanced_miniplayer_hide_overlay_buttons", FALSE, true, new MiniplayerPatch.MiniplayerHideOverlayButtonsAvailability());
|
public static final BooleanSetting MINIPLAYER_HIDE_OVERLAY_BUTTONS = new BooleanSetting("revanced_miniplayer_hide_overlay_buttons", FALSE, true, new MiniplayerPatch.MiniplayerHideOverlayButtonsAvailability());
|
||||||
public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3));
|
public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, new MiniplayerPatch.MiniplayerHideSubtextsAvailability());
|
||||||
public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", TRUE, true, MINIPLAYER_TYPE.availability(MODERN_1));
|
public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", TRUE, true, new MiniplayerPatch.MiniplayerHideRewindOrOverlayOpacityAvailability());
|
||||||
public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, MINIPLAYER_ANY_MODERN);
|
public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, new MiniplayerPatch.MiniplayerAnyModernAvailability());
|
||||||
public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_ANY_MODERN);
|
public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, new MiniplayerPatch.MiniplayerAnyModernAvailability());
|
||||||
public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1));
|
public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, new MiniplayerPatch.MiniplayerHideRewindOrOverlayOpacityAvailability());
|
||||||
|
|
||||||
// External downloader
|
// External downloader
|
||||||
public static final BooleanSetting EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_external_downloader", FALSE);
|
public static final BooleanSetting EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_external_downloader", FALSE);
|
||||||
@@ -254,8 +248,6 @@ public class Settings extends BaseSettings {
|
|||||||
|
|
||||||
// General layout
|
// General layout
|
||||||
public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("revanced_restore_old_settings_menus", FALSE, true);
|
public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("revanced_restore_old_settings_menus", FALSE, true);
|
||||||
public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
|
|
||||||
public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "", true);
|
|
||||||
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
|
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
|
||||||
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
|
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
|
||||||
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
|
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
|
||||||
@@ -381,9 +373,9 @@ public class Settings extends BaseSettings {
|
|||||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
||||||
public static final IntegerSetting SWIPE_OVERLAY_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true,
|
public static final IntegerSetting SWIPE_OVERLAY_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true,
|
||||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
||||||
public static final StringSetting SWIPE_OVERLAY_BRIGHTNESS_COLOR = new StringSetting("revanced_swipe_overlay_progress_brightness_color", "#FFFFFF", true,
|
public static final StringSetting SWIPE_OVERLAY_BRIGHTNESS_COLOR = new StringSetting("revanced_swipe_overlay_progress_brightness_color", "#BFFFFFFF", true,
|
||||||
parent(SWIPE_BRIGHTNESS));
|
parent(SWIPE_BRIGHTNESS));
|
||||||
public static final StringSetting SWIPE_OVERLAY_VOLUME_COLOR = new StringSetting("revanced_swipe_overlay_progress_volume_color", "#FFFFFF", true,
|
public static final StringSetting SWIPE_OVERLAY_VOLUME_COLOR = new StringSetting("revanced_swipe_overlay_progress_volume_color", "#BFFFFFFF", true,
|
||||||
parent(SWIPE_VOLUME));
|
parent(SWIPE_VOLUME));
|
||||||
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true,
|
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true,
|
||||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
||||||
@@ -405,7 +397,7 @@ public class Settings extends BaseSettings {
|
|||||||
// SponsorBlock
|
// SponsorBlock
|
||||||
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
|
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
|
||||||
/** Do not use id setting directly. Instead use {@link SponsorBlockSettings}. */
|
/** Do not use id setting directly. Instead use {@link SponsorBlockSettings}. */
|
||||||
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "");
|
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "", parent(SB_ENABLED));
|
||||||
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
|
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
|
||||||
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
|
||||||
public static final BooleanSetting SB_CREATE_NEW_SEGMENT = new BooleanSetting("sb_create_new_segment", FALSE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_CREATE_NEW_SEGMENT = new BooleanSetting("sb_create_new_segment", FALSE, parent(SB_ENABLED));
|
||||||
@@ -421,46 +413,36 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting SB_TRACK_SKIP_COUNT = new BooleanSetting("sb_track_skip_count", TRUE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_TRACK_SKIP_COUNT = new BooleanSetting("sb_track_skip_count", TRUE, parent(SB_ENABLED));
|
||||||
public static final FloatSetting SB_SEGMENT_MIN_DURATION = new FloatSetting("sb_min_segment_duration", 0F, parent(SB_ENABLED));
|
public static final FloatSetting SB_SEGMENT_MIN_DURATION = new FloatSetting("sb_min_segment_duration", 0F, parent(SB_ENABLED));
|
||||||
public static final BooleanSetting SB_VIDEO_LENGTH_WITHOUT_SEGMENTS = new BooleanSetting("sb_video_length_without_segments", FALSE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_VIDEO_LENGTH_WITHOUT_SEGMENTS = new BooleanSetting("sb_video_length_without_segments", FALSE, parent(SB_ENABLED));
|
||||||
public static final StringSetting SB_API_URL = new StringSetting("sb_api_url", "https://sponsor.ajay.app");
|
public static final StringSetting SB_API_URL = new StringSetting("sb_api_url", "https://sponsor.ajay.app", parent(SB_ENABLED));
|
||||||
public static final BooleanSetting SB_USER_IS_VIP = new BooleanSetting("sb_user_is_vip", FALSE);
|
public static final BooleanSetting SB_USER_IS_VIP = new BooleanSetting("sb_user_is_vip", FALSE);
|
||||||
public static final IntegerSetting SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS = new IntegerSetting("sb_local_time_saved_number_segments", 0);
|
public static final IntegerSetting SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS = new IntegerSetting("sb_local_time_saved_number_segments", 0, parent(SB_ENABLED));
|
||||||
public static final LongSetting SB_LOCAL_TIME_SAVED_MILLISECONDS = new LongSetting("sb_local_time_saved_milliseconds", 0L);
|
public static final LongSetting SB_LOCAL_TIME_SAVED_MILLISECONDS = new LongSetting("sb_local_time_saved_milliseconds", 0L, parent(SB_ENABLED));
|
||||||
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false);
|
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false);
|
||||||
public static final BooleanSetting SB_HIDE_EXPORT_WARNING = new BooleanSetting("sb_hide_export_warning", FALSE, false, false);
|
public static final BooleanSetting SB_HIDE_EXPORT_WARNING = new BooleanSetting("sb_hide_export_warning", FALSE, false, false);
|
||||||
public static final BooleanSetting SB_SEEN_GUIDELINES = new BooleanSetting("sb_seen_guidelines", FALSE, false, false);
|
public static final BooleanSetting SB_SEEN_GUIDELINES = new BooleanSetting("sb_seen_guidelines", FALSE, false, false);
|
||||||
public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue, parent(SB_ENABLED));
|
||||||
public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400");
|
public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#CC00D400");
|
||||||
public static final FloatSetting SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f);
|
public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
|
||||||
public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", MANUAL_SKIP.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#CCFFFF00");
|
||||||
public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00");
|
public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
|
||||||
public static final FloatSetting SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f);
|
public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CCCC00FF");
|
||||||
public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", MANUAL_SKIP.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
|
||||||
public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF");
|
public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#CCFF1684");
|
||||||
public static final FloatSetting SB_CATEGORY_INTERACTION_OPACITY = new FloatSetting("sb_interaction_opacity", 0.8f);
|
public static final StringSetting SB_CATEGORY_HOOK = new StringSetting("sb_hook", IGNORE.reVancedKeyValue, parent(SB_ENABLED));
|
||||||
public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_HOOK_COLOR = new StringSetting("sb_hook_color", "#CC395699");
|
||||||
public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684");
|
public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
|
||||||
public static final FloatSetting SB_CATEGORY_HIGHLIGHT_OPACITY = new FloatSetting("sb_highlight_opacity", 0.8f);
|
public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#CC00FFFF");
|
||||||
public static final StringSetting SB_CATEGORY_HOOK = new StringSetting("sb_hook", IGNORE.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
|
||||||
public static final StringSetting SB_CATEGORY_HOOK_COLOR = new StringSetting("sb_hook_color", "#395699");
|
public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#CC0202ED");
|
||||||
public static final FloatSetting SB_CATEGORY_HOOK_OPACITY = new FloatSetting("sb_hook_opacity", 0.8f);
|
public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", IGNORE.reVancedKeyValue, parent(SB_ENABLED));
|
||||||
public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", MANUAL_SKIP.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#CC008FD6");
|
||||||
public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF");
|
public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", IGNORE.reVancedKeyValue, parent(SB_ENABLED));
|
||||||
public static final FloatSetting SB_CATEGORY_INTRO_OPACITY = new FloatSetting("sb_intro_opacity", 0.8f);
|
public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#CC7300FF");
|
||||||
public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", MANUAL_SKIP.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue, parent(SB_ENABLED));
|
||||||
public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED");
|
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#CCFF9900");
|
||||||
public static final FloatSetting SB_CATEGORY_OUTRO_OPACITY = new FloatSetting("sb_outro_opacity", 0.8f);
|
// Dummy setting. Category is not exposed in the UI nor does it ever change.
|
||||||
public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", IGNORE.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue, false, false);
|
||||||
public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6");
|
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFFFF", false, false);
|
||||||
public static final FloatSetting SB_CATEGORY_PREVIEW_OPACITY = new FloatSetting("sb_preview_opacity", 0.8f);
|
|
||||||
public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", IGNORE.reVancedKeyValue);
|
|
||||||
public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF");
|
|
||||||
public static final FloatSetting SB_CATEGORY_FILLER_OPACITY = new FloatSetting("sb_filler_opacity", 0.8f);
|
|
||||||
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue);
|
|
||||||
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900");
|
|
||||||
public static final FloatSetting SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY = new FloatSetting("sb_music_offtopic_opacity", 0.8f);
|
|
||||||
public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue);
|
|
||||||
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF");
|
|
||||||
public static final FloatSetting SB_CATEGORY_UNSUBMITTED_OPACITY = new FloatSetting("sb_unsubmitted_opacity", 1.0f);
|
|
||||||
|
|
||||||
// Deprecated migrations
|
// Deprecated migrations
|
||||||
private static final BooleanSetting DEPRECATED_HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE, true);
|
private static final BooleanSetting DEPRECATED_HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE, true);
|
||||||
@@ -471,6 +453,17 @@ public class Settings extends BaseSettings {
|
|||||||
private static final BooleanSetting DEPRECATED_RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE);
|
private static final BooleanSetting DEPRECATED_RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE);
|
||||||
private static final BooleanSetting DEPRECATED_AUTO_CAPTIONS = new BooleanSetting("revanced_auto_captions", FALSE);
|
private static final BooleanSetting DEPRECATED_AUTO_CAPTIONS = new BooleanSetting("revanced_auto_captions", FALSE);
|
||||||
|
|
||||||
|
public static final FloatSetting DEPRECATED_SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f, false, false);
|
||||||
|
public static final FloatSetting DEPRECATED_SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f, false, false);
|
||||||
|
public static final FloatSetting DEPRECATED_SB_CATEGORY_INTERACTION_OPACITY = new FloatSetting("sb_interaction_opacity", 0.8f, false, false);
|
||||||
|
public static final FloatSetting DEPRECATED_SB_CATEGORY_HIGHLIGHT_OPACITY = new FloatSetting("sb_highlight_opacity", 0.8f, false, false);
|
||||||
|
public static final FloatSetting DEPRECATED_SB_CATEGORY_HOOK_OPACITY = new FloatSetting("sb_hook_opacity", 0.8f, false, false);
|
||||||
|
public static final FloatSetting DEPRECATED_SB_CATEGORY_INTRO_OPACITY = new FloatSetting("sb_intro_opacity", 0.8f, false, false);
|
||||||
|
public static final FloatSetting DEPRECATED_SB_CATEGORY_OUTRO_OPACITY = new FloatSetting("sb_outro_opacity", 0.8f, false, false);
|
||||||
|
public static final FloatSetting DEPRECATED_SB_CATEGORY_PREVIEW_OPACITY = new FloatSetting("sb_preview_opacity", 0.8f, false, false);
|
||||||
|
public static final FloatSetting DEPRECATED_SB_CATEGORY_FILLER_OPACITY = new FloatSetting("sb_filler_opacity", 0.8f, false, false);
|
||||||
|
public static final FloatSetting DEPRECATED_SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY = new FloatSetting("sb_music_offtopic_opacity", 0.8f, false, false);
|
||||||
|
|
||||||
static {
|
static {
|
||||||
// region Migration
|
// region Migration
|
||||||
|
|
||||||
@@ -535,6 +528,18 @@ public class Settings extends BaseSettings {
|
|||||||
Setting.migrateFromOldPreferences(revancedPrefs, RYD_ESTIMATED_LIKE, "ryd_estimated_like");
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_ESTIMATED_LIKE, "ryd_estimated_like");
|
||||||
Setting.migrateFromOldPreferences(revancedPrefs, RYD_TOAST_ON_CONNECTION_ERROR, "ryd_toast_on_connection_error");
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_TOAST_ON_CONNECTION_ERROR, "ryd_toast_on_connection_error");
|
||||||
|
|
||||||
|
// Migrate old saved data. Must be done here before the settings can be used by any other code.
|
||||||
|
applyOldSbOpacityToColor(SB_CATEGORY_SPONSOR_COLOR, DEPRECATED_SB_CATEGORY_SPONSOR_OPACITY);
|
||||||
|
applyOldSbOpacityToColor(SB_CATEGORY_SELF_PROMO_COLOR, DEPRECATED_SB_CATEGORY_SELF_PROMO_OPACITY);
|
||||||
|
applyOldSbOpacityToColor(SB_CATEGORY_INTERACTION_COLOR, DEPRECATED_SB_CATEGORY_INTERACTION_OPACITY);
|
||||||
|
applyOldSbOpacityToColor(SB_CATEGORY_HIGHLIGHT_COLOR, DEPRECATED_SB_CATEGORY_HIGHLIGHT_OPACITY);
|
||||||
|
applyOldSbOpacityToColor(SB_CATEGORY_HOOK_COLOR, DEPRECATED_SB_CATEGORY_HOOK_OPACITY);
|
||||||
|
applyOldSbOpacityToColor(SB_CATEGORY_INTRO_COLOR, DEPRECATED_SB_CATEGORY_INTRO_OPACITY);
|
||||||
|
applyOldSbOpacityToColor(SB_CATEGORY_OUTRO_COLOR, DEPRECATED_SB_CATEGORY_OUTRO_OPACITY);
|
||||||
|
applyOldSbOpacityToColor(SB_CATEGORY_PREVIEW_COLOR, DEPRECATED_SB_CATEGORY_PREVIEW_OPACITY);
|
||||||
|
applyOldSbOpacityToColor(SB_CATEGORY_FILLER_COLOR, DEPRECATED_SB_CATEGORY_FILLER_OPACITY);
|
||||||
|
applyOldSbOpacityToColor(SB_CATEGORY_MUSIC_OFFTOPIC_COLOR, DEPRECATED_SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY);
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region SB import/export callbacks
|
// region SB import/export callbacks
|
||||||
@@ -543,4 +548,13 @@ public class Settings extends BaseSettings {
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void applyOldSbOpacityToColor(StringSetting colorSetting, FloatSetting opacitySetting) {
|
||||||
|
String colorString = colorSetting.get();
|
||||||
|
if (colorString.length() >= 8) {
|
||||||
|
return; // Color is already #ARGB
|
||||||
|
}
|
||||||
|
|
||||||
|
colorSetting.save(SponsorBlockSettings.migrateOldColorString(colorString, opacitySetting.get()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,26 +2,23 @@ package app.revanced.extension.youtube.settings;
|
|||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
|
||||||
import app.revanced.extension.shared.settings.BaseActivityHook;
|
import app.revanced.extension.shared.settings.BaseActivityHook;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.youtube.patches.VersionCheckPatch;
|
import app.revanced.extension.youtube.patches.VersionCheckPatch;
|
||||||
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
|
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
|
||||||
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
|
import app.revanced.extension.youtube.settings.preference.YouTubePreferenceFragment;
|
||||||
|
import app.revanced.extension.youtube.settings.search.YouTubeSearchViewController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks LicenseActivity to inject a custom ReVancedPreferenceFragment with a toolbar and search functionality.
|
* Hooks LicenseActivity to inject a custom {@link YouTubePreferenceFragment} with a toolbar and search functionality.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public class LicenseActivityHook extends BaseActivityHook {
|
public class YouTubeActivityHook extends BaseActivityHook {
|
||||||
|
|
||||||
private static int currentThemeValueOrdinal = -1; // Must initially be a non-valid enum ordinal value.
|
private static int currentThemeValueOrdinal = -1; // Must initially be a non-valid enum ordinal value.
|
||||||
|
|
||||||
@@ -29,16 +26,14 @@ public class LicenseActivityHook extends BaseActivityHook {
|
|||||||
* Controller for managing search view components in the toolbar.
|
* Controller for managing search view components in the toolbar.
|
||||||
*/
|
*/
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
public static SearchViewController searchViewController;
|
public static YouTubeSearchViewController searchViewController;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point
|
* Injection point.
|
||||||
* <p>
|
|
||||||
* Creates an instance of LicenseActivityHook for use in static initialization.
|
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static LicenseActivityHook createInstance() {
|
public static void initialize(Activity parentActivity) {
|
||||||
return new LicenseActivityHook();
|
BaseActivityHook.initialize(new YouTubeActivityHook(), parentActivity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,7 +44,7 @@ public class LicenseActivityHook extends BaseActivityHook {
|
|||||||
final var theme = Utils.isDarkModeEnabled()
|
final var theme = Utils.isDarkModeEnabled()
|
||||||
? "Theme.YouTube.Settings.Dark"
|
? "Theme.YouTube.Settings.Dark"
|
||||||
: "Theme.YouTube.Settings";
|
: "Theme.YouTube.Settings";
|
||||||
activity.setTheme(Utils.getResourceIdentifier(theme, "style"));
|
activity.setTheme(Utils.getResourceIdentifierOrThrow(theme, "style"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,7 +52,7 @@ public class LicenseActivityHook extends BaseActivityHook {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected int getContentViewResourceId() {
|
protected int getContentViewResourceId() {
|
||||||
return Utils.getResourceIdentifier("revanced_settings_with_toolbar", "layout");
|
return LAYOUT_REVANCED_SETTINGS_WITH_TOOLBAR;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +71,7 @@ public class LicenseActivityHook extends BaseActivityHook {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected Drawable getNavigationIcon() {
|
protected Drawable getNavigationIcon() {
|
||||||
return ReVancedPreferenceFragment.getBackButtonDrawable();
|
return YouTubePreferenceFragment.getBackButtonDrawable();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -88,7 +83,7 @@ public class LicenseActivityHook extends BaseActivityHook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds search view components to the toolbar for ReVancedPreferenceFragment.
|
* Adds search view components to the toolbar for {@link YouTubePreferenceFragment}.
|
||||||
*
|
*
|
||||||
* @param activity The activity hosting the toolbar.
|
* @param activity The activity hosting the toolbar.
|
||||||
* @param toolbar The configured toolbar.
|
* @param toolbar The configured toolbar.
|
||||||
@@ -96,32 +91,18 @@ public class LicenseActivityHook extends BaseActivityHook {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {
|
protected void onPostToolbarSetup(Activity activity, Toolbar toolbar, PreferenceFragment fragment) {
|
||||||
if (fragment instanceof ReVancedPreferenceFragment) {
|
if (fragment instanceof YouTubePreferenceFragment) {
|
||||||
searchViewController = SearchViewController.addSearchViewComponents(
|
searchViewController = YouTubeSearchViewController.addSearchViewComponents(
|
||||||
activity, toolbar, (ReVancedPreferenceFragment) fragment);
|
activity, toolbar, (YouTubePreferenceFragment) fragment);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new ReVancedPreferenceFragment for the activity.
|
* Creates a new {@link YouTubePreferenceFragment} for the activity.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected PreferenceFragment createPreferenceFragment() {
|
protected PreferenceFragment createPreferenceFragment() {
|
||||||
return new ReVancedPreferenceFragment();
|
return new YouTubePreferenceFragment();
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Injection point.
|
|
||||||
* Overrides the ReVanced settings language.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public static Context getAttachBaseContext(Context original) {
|
|
||||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
|
||||||
if (language == AppLanguage.DEFAULT) {
|
|
||||||
return original;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Utils.getContext();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -164,12 +145,14 @@ public class LicenseActivityHook extends BaseActivityHook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles configuration changes, such as orientation, to update the search view.
|
* Injection point.
|
||||||
|
* <p>
|
||||||
|
* Overrides {@link Activity#finish()} of the injection Activity.
|
||||||
|
*
|
||||||
|
* @return if the original activity finish method should be allowed to run.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static void handleConfigurationChanged(Activity activity, Configuration newConfig) {
|
public static boolean handleFinish() {
|
||||||
if (searchViewController != null) {
|
return YouTubeSearchViewController.handleFinish(searchViewController);
|
||||||
searchViewController.handleOrientationChange(newConfig.orientation);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package app.revanced.extension.youtube.settings.preference;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows tapping the DeArrow about preference to open the DeArrow website.
|
* Allows tapping the DeArrow about preference to open the DeArrow website.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import java.util.function.Function;
|
|||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,7 +211,7 @@ public class ExternalDownloaderPreference extends CustomDialogListPreference {
|
|||||||
final boolean usingCustomDownloader = Downloader.findByPackageName(packageName) == null;
|
final boolean usingCustomDownloader = Downloader.findByPackageName(packageName) == null;
|
||||||
adapter = new CustomDialogListPreference.ListPreferenceArrayAdapter(
|
adapter = new CustomDialogListPreference.ListPreferenceArrayAdapter(
|
||||||
context,
|
context,
|
||||||
Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"),
|
LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED,
|
||||||
getEntries(),
|
getEntries(),
|
||||||
getEntryValues(),
|
getEntryValues(),
|
||||||
usingCustomDownloader
|
usingCustomDownloader
|
||||||
@@ -302,7 +303,7 @@ public class ExternalDownloaderPreference extends CustomDialogListPreference {
|
|||||||
contentLayout.addView(editText);
|
contentLayout.addView(editText);
|
||||||
|
|
||||||
// Create the custom dialog.
|
// Create the custom dialog.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
getTitle() != null ? getTitle().toString() : "",
|
getTitle() != null ? getTitle().toString() : "",
|
||||||
null,
|
null,
|
||||||
@@ -312,7 +313,7 @@ public class ExternalDownloaderPreference extends CustomDialogListPreference {
|
|||||||
String newValue = editText.getText().toString().trim();
|
String newValue = editText.getText().toString().trim();
|
||||||
if (newValue.isEmpty()) {
|
if (newValue.isEmpty()) {
|
||||||
// Show dialog if EditText is empty.
|
// Show dialog if EditText is empty.
|
||||||
Utils.createCustomDialog(
|
CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
str("revanced_external_downloader_name_title"),
|
str("revanced_external_downloader_name_title"),
|
||||||
str("revanced_external_downloader_empty_warning"),
|
str("revanced_external_downloader_empty_warning"),
|
||||||
@@ -415,7 +416,7 @@ public class ExternalDownloaderPreference extends CustomDialogListPreference {
|
|||||||
? str("revanced_external_downloader_not_installed_warning", downloader.name)
|
? str("revanced_external_downloader_not_installed_warning", downloader.name)
|
||||||
: str("revanced_external_downloader_package_not_found_warning", packageName);
|
: str("revanced_external_downloader_package_not_found_warning", packageName);
|
||||||
|
|
||||||
Utils.createCustomDialog(
|
CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
str("revanced_external_downloader_not_found_title"),
|
str("revanced_external_downloader_not_found_title"),
|
||||||
message,
|
message,
|
||||||
|
|||||||
@@ -1,483 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.preference.ListPreference;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.PreferenceCategory;
|
|
||||||
import android.preference.PreferenceGroup;
|
|
||||||
import android.preference.PreferenceScreen;
|
|
||||||
import android.preference.SwitchPreference;
|
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.style.BackgroundColorSpan;
|
|
||||||
import android.widget.Toolbar;
|
|
||||||
|
|
||||||
import androidx.annotation.CallSuper;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.ArrayDeque;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Deque;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
|
|
||||||
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
|
||||||
import app.revanced.extension.youtube.settings.LicenseActivityHook;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preference fragment for ReVanced settings.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings({"deprecation", "NewApi"})
|
|
||||||
public class ReVancedPreferenceFragment extends ToolbarPreferenceFragment {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The main PreferenceScreen used to display the current set of preferences.
|
|
||||||
* This screen is manipulated during initialization and filtering to show or hide preferences.
|
|
||||||
*/
|
|
||||||
private PreferenceScreen preferenceScreen;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A copy of the original PreferenceScreen created during initialization.
|
|
||||||
* Used to restore the preference structure to its initial state after filtering or other modifications.
|
|
||||||
*/
|
|
||||||
private PreferenceScreen originalPreferenceScreen;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used for searching preferences. A Collection of all preferences including nested preferences.
|
|
||||||
* Root preferences are excluded (no need to search what's on the root screen),
|
|
||||||
* but their sub preferences are included.
|
|
||||||
*/
|
|
||||||
private final List<AbstractPreferenceSearchData<?>> allPreferences = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initializes the preference fragment, copying the original screen to allow full restoration.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void initialize() {
|
|
||||||
super.initialize();
|
|
||||||
|
|
||||||
try {
|
|
||||||
preferenceScreen = getPreferenceScreen();
|
|
||||||
Utils.sortPreferenceGroups(preferenceScreen);
|
|
||||||
|
|
||||||
// Store the original structure for restoration after filtering.
|
|
||||||
originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getContext());
|
|
||||||
for (int i = 0, count = preferenceScreen.getPreferenceCount(); i < count; i++) {
|
|
||||||
originalPreferenceScreen.addPreference(preferenceScreen.getPreference(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
setPreferenceScreenToolbar(preferenceScreen);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "initialize failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the fragment starts, ensuring all preferences are collected after initialization.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
try {
|
|
||||||
if (allPreferences.isEmpty()) {
|
|
||||||
// Must collect preferences on start and not in initialize since
|
|
||||||
// legacy SB settings are not loaded yet.
|
|
||||||
Logger.printDebug(() -> "Collecting preferences to search");
|
|
||||||
|
|
||||||
// Do not show root menu preferences in search results.
|
|
||||||
// Instead search for everything that's not shown when search is not active.
|
|
||||||
collectPreferences(preferenceScreen, 1, 0);
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onStart failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets toolbar for all nested preference screens.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void customizeToolbar(Toolbar toolbar) {
|
|
||||||
LicenseActivityHook.setToolbarLayoutParams(toolbar);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform actions after toolbar setup.
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {
|
|
||||||
if (LicenseActivityHook.searchViewController != null
|
|
||||||
&& LicenseActivityHook.searchViewController.isSearchActive()) {
|
|
||||||
toolbar.post(() -> LicenseActivityHook.searchViewController.closeSearch());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively collects all preferences from the screen or group.
|
|
||||||
*
|
|
||||||
* @param includeDepth Menu depth to start including preferences.
|
|
||||||
* A value of 0 adds all preferences.
|
|
||||||
*/
|
|
||||||
private void collectPreferences(PreferenceGroup group, int includeDepth, int currentDepth) {
|
|
||||||
for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
|
|
||||||
Preference preference = group.getPreference(i);
|
|
||||||
if (includeDepth <= currentDepth && !(preference instanceof PreferenceCategory)
|
|
||||||
&& !(preference instanceof SponsorBlockPreferenceGroup)) {
|
|
||||||
|
|
||||||
AbstractPreferenceSearchData<?> data;
|
|
||||||
if (preference instanceof SwitchPreference switchPref) {
|
|
||||||
data = new SwitchPreferenceSearchData(switchPref);
|
|
||||||
} else if (preference instanceof ListPreference listPref) {
|
|
||||||
data = new ListPreferenceSearchData(listPref);
|
|
||||||
} else {
|
|
||||||
data = new PreferenceSearchData(preference);
|
|
||||||
}
|
|
||||||
|
|
||||||
allPreferences.add(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preference instanceof PreferenceGroup subGroup) {
|
|
||||||
collectPreferences(subGroup, includeDepth, currentDepth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filters the preferences using the given query string and applies highlighting.
|
|
||||||
*/
|
|
||||||
public void filterPreferences(String query) {
|
|
||||||
preferenceScreen.removeAll();
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(query)) {
|
|
||||||
// Restore original preferences and their titles/summaries/entries.
|
|
||||||
for (int i = 0, count = originalPreferenceScreen.getPreferenceCount(); i < count; i++) {
|
|
||||||
preferenceScreen.addPreference(originalPreferenceScreen.getPreference(i));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (AbstractPreferenceSearchData<?> data : allPreferences) {
|
|
||||||
data.clearHighlighting();
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Navigation path -> Category
|
|
||||||
Map<String, PreferenceCategory> categoryMap = new HashMap<>();
|
|
||||||
String queryLower = Utils.removePunctuationToLowercase(query);
|
|
||||||
|
|
||||||
Pattern queryPattern = Pattern.compile(Pattern.quote(Utils.removePunctuationToLowercase(query)),
|
|
||||||
Pattern.CASE_INSENSITIVE);
|
|
||||||
|
|
||||||
for (AbstractPreferenceSearchData<?> data : allPreferences) {
|
|
||||||
if (data.matchesSearchQuery(queryLower)) {
|
|
||||||
data.applyHighlighting(queryLower, queryPattern);
|
|
||||||
|
|
||||||
String navigationPath = data.navigationPath;
|
|
||||||
PreferenceCategory group = categoryMap.computeIfAbsent(navigationPath, key -> {
|
|
||||||
PreferenceCategory newGroup = new PreferenceCategory(preferenceScreen.getContext());
|
|
||||||
newGroup.setTitle(navigationPath);
|
|
||||||
preferenceScreen.addPreference(newGroup);
|
|
||||||
return newGroup;
|
|
||||||
});
|
|
||||||
group.addPreference(data.preference);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show 'No results found' if search results are empty.
|
|
||||||
if (categoryMap.isEmpty()) {
|
|
||||||
Preference noResultsPreference = new Preference(preferenceScreen.getContext());
|
|
||||||
noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
|
|
||||||
noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
|
|
||||||
noResultsPreference.setSelectable(false);
|
|
||||||
// Set icon for the placeholder preference.
|
|
||||||
noResultsPreference.setLayoutResource(getResourceIdentifier(
|
|
||||||
"revanced_preference_with_icon_no_search_result", "layout"));
|
|
||||||
noResultsPreference.setIcon(getResourceIdentifier("revanced_settings_search_icon", "drawable"));
|
|
||||||
preferenceScreen.addPreference(noResultsPreference);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
class AbstractPreferenceSearchData<T extends Preference> {
|
|
||||||
/**
|
|
||||||
* @return The navigation path for the given preference, such as "Player > Action buttons".
|
|
||||||
*/
|
|
||||||
private static String getPreferenceNavigationString(Preference preference) {
|
|
||||||
Deque<CharSequence> pathElements = new ArrayDeque<>();
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
preference = preference.getParent();
|
|
||||||
|
|
||||||
if (preference == null) {
|
|
||||||
if (pathElements.isEmpty()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
Locale locale = BaseSettings.REVANCED_LANGUAGE.get().getLocale();
|
|
||||||
return Utils.getTextDirectionString(locale) + String.join(" > ", pathElements);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(preference instanceof NoTitlePreferenceCategory)
|
|
||||||
&& !(preference instanceof SponsorBlockPreferenceGroup)) {
|
|
||||||
CharSequence title = preference.getTitle();
|
|
||||||
if (title != null && title.length() > 0) {
|
|
||||||
pathElements.addFirst(title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Highlights the search query in the given text by applying color span.
|
|
||||||
* @param text The original text to process.
|
|
||||||
* @param queryPattern The search query to highlight.
|
|
||||||
* @return The text with highlighted query matches as a SpannableStringBuilder.
|
|
||||||
*/
|
|
||||||
static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
|
|
||||||
if (TextUtils.isEmpty(text)) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int adjustedColor = Utils.adjustColorBrightness(Utils.getAppBackgroundColor(),
|
|
||||||
0.95f, 1.20f);
|
|
||||||
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
|
|
||||||
|
|
||||||
SpannableStringBuilder spannable = new SpannableStringBuilder(text);
|
|
||||||
Matcher matcher = queryPattern.matcher(text);
|
|
||||||
|
|
||||||
while (matcher.find()) {
|
|
||||||
spannable.setSpan(
|
|
||||||
highlightSpan,
|
|
||||||
matcher.start(),
|
|
||||||
matcher.end(),
|
|
||||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return spannable;
|
|
||||||
}
|
|
||||||
|
|
||||||
final T preference;
|
|
||||||
final String key;
|
|
||||||
final String navigationPath;
|
|
||||||
boolean highlightingApplied;
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
CharSequence originalTitle;
|
|
||||||
@Nullable
|
|
||||||
String searchTitle;
|
|
||||||
|
|
||||||
AbstractPreferenceSearchData(T pref) {
|
|
||||||
preference = pref;
|
|
||||||
key = Utils.removePunctuationToLowercase(pref.getKey());
|
|
||||||
navigationPath = getPreferenceNavigationString(pref);
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
void updateSearchDataIfNeeded() {
|
|
||||||
if (highlightingApplied) {
|
|
||||||
// Must clear, otherwise old highlighting is still applied.
|
|
||||||
clearHighlighting();
|
|
||||||
}
|
|
||||||
|
|
||||||
CharSequence title = preference.getTitle();
|
|
||||||
if (originalTitle != title) { // Check using reference equality.
|
|
||||||
originalTitle = title;
|
|
||||||
searchTitle = Utils.removePunctuationToLowercase(title);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
boolean matchesSearchQuery(String query) {
|
|
||||||
updateSearchDataIfNeeded();
|
|
||||||
|
|
||||||
return key.contains(query)
|
|
||||||
|| searchTitle != null && searchTitle.contains(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
void applyHighlighting(String query, Pattern queryPattern) {
|
|
||||||
preference.setTitle(highlightSearchQuery(originalTitle, queryPattern));
|
|
||||||
highlightingApplied = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
void clearHighlighting() {
|
|
||||||
if (highlightingApplied) {
|
|
||||||
preference.setTitle(originalTitle);
|
|
||||||
highlightingApplied = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Regular preference type that only uses the base preference summary.
|
|
||||||
* Should only be used if a more specific data class does not exist.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
class PreferenceSearchData extends AbstractPreferenceSearchData<Preference> {
|
|
||||||
@Nullable
|
|
||||||
CharSequence originalSummary;
|
|
||||||
@Nullable
|
|
||||||
String searchSummary;
|
|
||||||
|
|
||||||
PreferenceSearchData(Preference pref) {
|
|
||||||
super(pref);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateSearchDataIfNeeded() {
|
|
||||||
super.updateSearchDataIfNeeded();
|
|
||||||
|
|
||||||
CharSequence summary = preference.getSummary();
|
|
||||||
if (originalSummary != summary) {
|
|
||||||
originalSummary = summary;
|
|
||||||
searchSummary = Utils.removePunctuationToLowercase(summary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean matchesSearchQuery(String query) {
|
|
||||||
return super.matchesSearchQuery(query)
|
|
||||||
|| searchSummary != null && searchSummary.contains(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void applyHighlighting(String query, Pattern queryPattern) {
|
|
||||||
super.applyHighlighting(query, queryPattern);
|
|
||||||
|
|
||||||
preference.setSummary(highlightSearchQuery(originalSummary, queryPattern));
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
void clearHighlighting() {
|
|
||||||
if (highlightingApplied) {
|
|
||||||
preference.setSummary(originalSummary);
|
|
||||||
}
|
|
||||||
|
|
||||||
super.clearHighlighting();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Switch preference type that uses summaryOn and summaryOff.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
class SwitchPreferenceSearchData extends AbstractPreferenceSearchData<SwitchPreference> {
|
|
||||||
@Nullable
|
|
||||||
CharSequence originalSummaryOn, originalSummaryOff;
|
|
||||||
@Nullable
|
|
||||||
String searchSummaryOn, searchSummaryOff;
|
|
||||||
|
|
||||||
SwitchPreferenceSearchData(SwitchPreference pref) {
|
|
||||||
super(pref);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateSearchDataIfNeeded() {
|
|
||||||
super.updateSearchDataIfNeeded();
|
|
||||||
|
|
||||||
CharSequence summaryOn = preference.getSummaryOn();
|
|
||||||
if (originalSummaryOn != summaryOn) {
|
|
||||||
originalSummaryOn = summaryOn;
|
|
||||||
searchSummaryOn = Utils.removePunctuationToLowercase(summaryOn);
|
|
||||||
}
|
|
||||||
|
|
||||||
CharSequence summaryOff = preference.getSummaryOff();
|
|
||||||
if (originalSummaryOff != summaryOff) {
|
|
||||||
originalSummaryOff = summaryOff;
|
|
||||||
searchSummaryOff = Utils.removePunctuationToLowercase(summaryOff);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean matchesSearchQuery(String query) {
|
|
||||||
return super.matchesSearchQuery(query)
|
|
||||||
|| searchSummaryOn != null && searchSummaryOn.contains(query)
|
|
||||||
|| searchSummaryOff != null && searchSummaryOff.contains(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void applyHighlighting(String query, Pattern queryPattern) {
|
|
||||||
super.applyHighlighting(query, queryPattern);
|
|
||||||
|
|
||||||
preference.setSummaryOn(highlightSearchQuery(originalSummaryOn, queryPattern));
|
|
||||||
preference.setSummaryOff(highlightSearchQuery(originalSummaryOff, queryPattern));
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
void clearHighlighting() {
|
|
||||||
if (highlightingApplied) {
|
|
||||||
preference.setSummaryOn(originalSummaryOn);
|
|
||||||
preference.setSummaryOff(originalSummaryOff);
|
|
||||||
}
|
|
||||||
|
|
||||||
super.clearHighlighting();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List preference type that uses entries.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
class ListPreferenceSearchData extends AbstractPreferenceSearchData<ListPreference> {
|
|
||||||
@Nullable
|
|
||||||
CharSequence[] originalEntries;
|
|
||||||
@Nullable
|
|
||||||
String searchEntries;
|
|
||||||
|
|
||||||
ListPreferenceSearchData(ListPreference pref) {
|
|
||||||
super(pref);
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateSearchDataIfNeeded() {
|
|
||||||
super.updateSearchDataIfNeeded();
|
|
||||||
|
|
||||||
CharSequence[] entries = preference.getEntries();
|
|
||||||
if (originalEntries != entries) {
|
|
||||||
originalEntries = entries;
|
|
||||||
searchEntries = Utils.removePunctuationToLowercase(String.join(" ", entries));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean matchesSearchQuery(String query) {
|
|
||||||
return super.matchesSearchQuery(query)
|
|
||||||
|| searchEntries != null && searchEntries.contains(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
void applyHighlighting(String query, Pattern queryPattern) {
|
|
||||||
super.applyHighlighting(query, queryPattern);
|
|
||||||
|
|
||||||
if (originalEntries != null) {
|
|
||||||
final int length = originalEntries.length;
|
|
||||||
CharSequence[] highlightedEntries = new CharSequence[length];
|
|
||||||
|
|
||||||
for (int i = 0; i < length; i++) {
|
|
||||||
highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern);
|
|
||||||
|
|
||||||
// Cannot highlight the summary text, because ListPreference uses
|
|
||||||
// the toString() of the summary CharSequence which strips away all formatting.
|
|
||||||
}
|
|
||||||
|
|
||||||
preference.setEntries(highlightedEntries);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
void clearHighlighting() {
|
|
||||||
if (highlightingApplied) {
|
|
||||||
preference.setEntries(originalEntries);
|
|
||||||
}
|
|
||||||
|
|
||||||
super.clearHighlighting();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
|
import app.revanced.extension.shared.settings.preference.BulletPointPreference;
|
||||||
import app.revanced.extension.shared.spoof.ClientType;
|
import app.revanced.extension.shared.spoof.ClientType;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@@ -99,6 +100,7 @@ public class SpoofStreamingDataSideEffectsPreference extends Preference {
|
|||||||
+ '\n' + str("revanced_spoof_video_streams_about_kids_videos");
|
+ '\n' + str("revanced_spoof_video_streams_about_kids_videos");
|
||||||
}
|
}
|
||||||
|
|
||||||
setSummary(summary);
|
// Use better formatting for bullet points.
|
||||||
|
setSummary(BulletPointPreference.formatIntoBulletPoints(summary));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ToolbarPreferenceFragment;
|
||||||
|
import app.revanced.extension.youtube.settings.YouTubeActivityHook;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference fragment for ReVanced settings.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class YouTubePreferenceFragment extends ToolbarPreferenceFragment {
|
||||||
|
/**
|
||||||
|
* The main PreferenceScreen used to display the current set of preferences.
|
||||||
|
*/
|
||||||
|
private PreferenceScreen preferenceScreen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the preference fragment.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void initialize() {
|
||||||
|
super.initialize();
|
||||||
|
|
||||||
|
try {
|
||||||
|
preferenceScreen = getPreferenceScreen();
|
||||||
|
Utils.sortPreferenceGroups(preferenceScreen);
|
||||||
|
setPreferenceScreenToolbar(preferenceScreen);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the fragment starts.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
try {
|
||||||
|
// Initialize search controller if needed.
|
||||||
|
if (YouTubeActivityHook.searchViewController != null) {
|
||||||
|
// Trigger search data collection after fragment is ready.
|
||||||
|
YouTubeActivityHook.searchViewController.initializeSearchData();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onStart failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets toolbar for all nested preference screens.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void customizeToolbar(Toolbar toolbar) {
|
||||||
|
YouTubeActivityHook.setToolbarLayoutParams(toolbar);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform actions after toolbar setup.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onPostToolbarSetup(Toolbar toolbar, Dialog preferenceScreenDialog) {
|
||||||
|
if (YouTubeActivityHook.searchViewController != null
|
||||||
|
&& YouTubeActivityHook.searchViewController.isSearchActive()) {
|
||||||
|
toolbar.post(() -> YouTubeActivityHook.searchViewController.closeSearch());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the preference screen for external access by SearchViewController.
|
||||||
|
*/
|
||||||
|
public PreferenceScreen getPreferenceScreenForSearch() {
|
||||||
|
return preferenceScreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package app.revanced.extension.youtube.settings.search;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchResultsAdapter;
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchViewController;
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchResultItem;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YouTube-specific search results adapter.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class YouTubeSearchResultsAdapter extends BaseSearchResultsAdapter {
|
||||||
|
|
||||||
|
public YouTubeSearchResultsAdapter(Context context, List<BaseSearchResultItem> items,
|
||||||
|
BaseSearchViewController.BasePreferenceFragment fragment,
|
||||||
|
BaseSearchViewController searchViewController) {
|
||||||
|
super(context, items, fragment, searchViewController);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected PreferenceScreen getMainPreferenceScreen() {
|
||||||
|
return fragment.getPreferenceScreenForSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package app.revanced.extension.youtube.settings.search;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchResultItem;
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchResultsAdapter;
|
||||||
|
import app.revanced.extension.shared.settings.search.BaseSearchViewController;
|
||||||
|
import app.revanced.extension.youtube.settings.preference.YouTubePreferenceFragment;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YouTube-specific search view controller implementation.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class YouTubeSearchViewController extends BaseSearchViewController {
|
||||||
|
|
||||||
|
public static YouTubeSearchViewController addSearchViewComponents(Activity activity, Toolbar toolbar,
|
||||||
|
YouTubePreferenceFragment fragment) {
|
||||||
|
return new YouTubeSearchViewController(activity, toolbar, fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
private YouTubeSearchViewController(Activity activity, Toolbar toolbar, YouTubePreferenceFragment fragment) {
|
||||||
|
super(activity, toolbar, new PreferenceFragmentAdapter(fragment));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected BaseSearchResultsAdapter createSearchResultsAdapter() {
|
||||||
|
return new YouTubeSearchResultsAdapter(activity, filteredSearchItems, fragment, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isSpecialPreferenceGroup(Preference preference) {
|
||||||
|
return preference instanceof SponsorBlockPreferenceGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setupSpecialPreferenceListeners(BaseSearchResultItem item) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static method for Activity finish.
|
||||||
|
public static boolean handleFinish(YouTubeSearchViewController searchViewController) {
|
||||||
|
if (searchViewController != null && searchViewController.isSearchActive()) {
|
||||||
|
searchViewController.closeSearch();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adapter to wrap YouTubePreferenceFragment to BasePreferenceFragment interface.
|
||||||
|
private record PreferenceFragmentAdapter(YouTubePreferenceFragment fragment) implements BasePreferenceFragment {
|
||||||
|
@Override
|
||||||
|
public PreferenceScreen getPreferenceScreenForSearch() {
|
||||||
|
return fragment.getPreferenceScreenForSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView() {
|
||||||
|
return fragment.getView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Activity getActivity() {
|
||||||
|
return fragment.getActivity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,9 @@ import static app.revanced.extension.shared.StringRef.str;
|
|||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.ShapeDrawable;
|
import android.graphics.drawable.ShapeDrawable;
|
||||||
@@ -48,9 +47,10 @@ import kotlin.Unit;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video.
|
* Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video.
|
||||||
*
|
* <p>
|
||||||
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
|
* Class is not thread safe. All methods must be called on the main thread unless otherwise specified.
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("NewApi")
|
||||||
public class SegmentPlaybackController {
|
public class SegmentPlaybackController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,7 +122,6 @@ public class SegmentPlaybackController {
|
|||||||
/**
|
/**
|
||||||
* Used to prevent re-showing a previously hidden skip button when exiting an embedded segment.
|
* Used to prevent re-showing a previously hidden skip button when exiting an embedded segment.
|
||||||
* Only used when {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is enabled.
|
* Only used when {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is enabled.
|
||||||
*
|
|
||||||
* A collection of segments that have automatically hidden the skip button for, and all segments in this list
|
* A collection of segments that have automatically hidden the skip button for, and all segments in this list
|
||||||
* contain the current video time. Segment are removed when playback exits the segment.
|
* contain the current video time. Segment are removed when playback exits the segment.
|
||||||
*/
|
*/
|
||||||
@@ -867,23 +866,11 @@ public class SegmentPlaybackController {
|
|||||||
|
|
||||||
Window window = dialog.getWindow();
|
Window window = dialog.getWindow();
|
||||||
if (window != null) {
|
if (window != null) {
|
||||||
// Remove window animations and use custom fade animation.
|
window.setWindowAnimations(0); // Remove window animations and use custom fade animation.
|
||||||
window.setWindowAnimations(0);
|
|
||||||
|
|
||||||
WindowManager.LayoutParams params = window.getAttributes();
|
|
||||||
params.gravity = Gravity.BOTTOM;
|
|
||||||
params.y = dipToPixels(72);
|
|
||||||
int portraitWidth = Utils.percentageWidthToPixels(60); // 60% of the screen width.
|
|
||||||
|
|
||||||
if (Resources.getSystem().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
||||||
portraitWidth = Math.min(portraitWidth, Utils.percentageHeightToPixels(60)); // 60% of the screen height.
|
|
||||||
}
|
|
||||||
params.width = portraitWidth;
|
|
||||||
params.dimAmount = 0.0f;
|
|
||||||
window.setAttributes(params);
|
|
||||||
window.setBackgroundDrawable(null);
|
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
|
window.addFlags(WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL);
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
|
window.addFlags(WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
|
||||||
|
|
||||||
|
Utils.setDialogWindowParameters(window, Gravity.BOTTOM, 72, 60, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dismissUndoToast()) {
|
if (dismissUndoToast()) {
|
||||||
|
|||||||
@@ -14,16 +14,19 @@ import androidx.annotation.Nullable;
|
|||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
|
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
|
||||||
|
|
||||||
|
@SuppressWarnings("NewApi")
|
||||||
public class SponsorBlockSettings {
|
public class SponsorBlockSettings {
|
||||||
/**
|
/**
|
||||||
* Minimum length a SB user id must be, as set by SB API.
|
* Minimum length a SB user id must be, as set by SB API.
|
||||||
@@ -50,11 +53,15 @@ public class SponsorBlockSettings {
|
|||||||
JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections");
|
JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections");
|
||||||
|
|
||||||
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
||||||
// clear existing behavior, as browser plugin exports no behavior for ignored categories
|
// Clear existing behavior, as browser plugin exports no behavior for ignored categories.
|
||||||
category.setBehaviour(CategoryBehaviour.IGNORE);
|
category.setBehaviour(CategoryBehaviour.IGNORE);
|
||||||
if (barTypesObject.has(category.keyValue)) {
|
if (barTypesObject.has(category.keyValue)) {
|
||||||
JSONObject categoryObject = barTypesObject.getJSONObject(category.keyValue);
|
JSONObject categoryObject = barTypesObject.getJSONObject(category.keyValue);
|
||||||
category.setColor(categoryObject.getString("color"));
|
// Older ReVanced SB exports lack an opacity value.
|
||||||
|
if (categoryObject.has("color") && categoryObject.has("opacity")) {
|
||||||
|
category.setColorWithOpacity(categoryObject.getString("color"));
|
||||||
|
category.setOpacity((float) categoryObject.getDouble("opacity"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +71,7 @@ public class SponsorBlockSettings {
|
|||||||
String categoryKey = categorySelectionObject.getString("name");
|
String categoryKey = categorySelectionObject.getString("name");
|
||||||
SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey);
|
SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey);
|
||||||
if (category == null) {
|
if (category == null) {
|
||||||
continue; // unsupported category, ignore
|
continue; // Unsupported category, ignore.
|
||||||
}
|
}
|
||||||
|
|
||||||
final int desktopValue = categorySelectionObject.getInt("option");
|
final int desktopValue = categorySelectionObject.getInt("option");
|
||||||
@@ -73,7 +80,7 @@ public class SponsorBlockSettings {
|
|||||||
Utils.showToastLong(categoryKey + " unknown behavior key: " + categoryKey);
|
Utils.showToastLong(categoryKey + " unknown behavior key: " + categoryKey);
|
||||||
} else if (category == SegmentCategory.HIGHLIGHT && behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE) {
|
} else if (category == SegmentCategory.HIGHLIGHT && behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE) {
|
||||||
Utils.showToastLong("Skip-once behavior not allowed for " + category.keyValue);
|
Utils.showToastLong("Skip-once behavior not allowed for " + category.keyValue);
|
||||||
category.setBehaviour(CategoryBehaviour.SKIP_AUTOMATICALLY); // use closest match
|
category.setBehaviour(CategoryBehaviour.SKIP_AUTOMATICALLY); // Use closest match.
|
||||||
} else {
|
} else {
|
||||||
category.setBehaviour(behaviour);
|
category.setBehaviour(behaviour);
|
||||||
}
|
}
|
||||||
@@ -93,7 +100,7 @@ public class SponsorBlockSettings {
|
|||||||
Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save(settingsJson.getBoolean("showTimeWithSkips"));
|
Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save(settingsJson.getBoolean("showTimeWithSkips"));
|
||||||
|
|
||||||
String serverAddress = settingsJson.getString("serverAddress");
|
String serverAddress = settingsJson.getString("serverAddress");
|
||||||
if (isValidSBServerAddress(serverAddress)) { // Old versions of ReVanced exported wrong url format
|
if (isValidSBServerAddress(serverAddress)) { // Old versions of ReVanced exported wrong url format.
|
||||||
Settings.SB_API_URL.save(serverAddress);
|
Settings.SB_API_URL.save(serverAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,7 +110,7 @@ public class SponsorBlockSettings {
|
|||||||
}
|
}
|
||||||
Settings.SB_SEGMENT_MIN_DURATION.save(minDuration);
|
Settings.SB_SEGMENT_MIN_DURATION.save(minDuration);
|
||||||
|
|
||||||
if (settingsJson.has("skipCount")) { // Value not exported in old versions of ReVanced
|
if (settingsJson.has("skipCount")) { // Value not exported in old versions of ReVanced.
|
||||||
int skipCount = settingsJson.getInt("skipCount");
|
int skipCount = settingsJson.getInt("skipCount");
|
||||||
if (skipCount < 0) {
|
if (skipCount < 0) {
|
||||||
throw new IllegalArgumentException("invalid skipCount: " + skipCount);
|
throw new IllegalArgumentException("invalid skipCount: " + skipCount);
|
||||||
@@ -121,7 +128,7 @@ public class SponsorBlockSettings {
|
|||||||
|
|
||||||
Utils.showToastLong(str("revanced_sb_settings_import_successful"));
|
Utils.showToastLong(str("revanced_sb_settings_import_successful"));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printInfo(() -> "failed to import settings", ex); // use info level, as we are showing our own toast
|
Logger.printInfo(() -> "failed to import settings", ex); // Use info level, as we are showing our own toast.
|
||||||
Utils.showToastLong(str("revanced_sb_settings_import_failed", ex.getMessage()));
|
Utils.showToastLong(str("revanced_sb_settings_import_failed", ex.getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,14 +140,16 @@ public class SponsorBlockSettings {
|
|||||||
Logger.printDebug(() -> "Creating SponsorBlock export settings string");
|
Logger.printDebug(() -> "Creating SponsorBlock export settings string");
|
||||||
JSONObject json = new JSONObject();
|
JSONObject json = new JSONObject();
|
||||||
|
|
||||||
JSONObject barTypesObject = new JSONObject(); // categories' colors
|
JSONObject barTypesObject = new JSONObject(); // Categories' colors.
|
||||||
JSONArray categorySelectionsArray = new JSONArray(); // categories' behavior
|
JSONArray categorySelectionsArray = new JSONArray(); // Categories' behavior.
|
||||||
|
|
||||||
SegmentCategory[] categories = SegmentCategory.categoriesWithoutUnsubmitted();
|
SegmentCategory[] categories = SegmentCategory.categoriesWithoutUnsubmitted();
|
||||||
for (SegmentCategory category : categories) {
|
for (SegmentCategory category : categories) {
|
||||||
JSONObject categoryObject = new JSONObject();
|
JSONObject categoryObject = new JSONObject();
|
||||||
String categoryKey = category.keyValue;
|
String categoryKey = category.keyValue;
|
||||||
categoryObject.put("color", category.getColorString());
|
// SB settings use separate color and opacity.
|
||||||
|
categoryObject.put("color", category.getColorStringWithoutOpacity());
|
||||||
|
categoryObject.put("opacity", category.getOpacity());
|
||||||
barTypesObject.put(categoryKey, categoryObject);
|
barTypesObject.put(categoryKey, categoryObject);
|
||||||
|
|
||||||
if (category.behaviour != CategoryBehaviour.IGNORE) {
|
if (category.behaviour != CategoryBehaviour.IGNORE) {
|
||||||
@@ -167,7 +176,7 @@ public class SponsorBlockSettings {
|
|||||||
|
|
||||||
return json.toString(2);
|
return json.toString(2);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printInfo(() -> "failed to export settings", ex); // use info level, as we are showing our own toast
|
Logger.printInfo(() -> "failed to export settings", ex); // Use info level, as we are showing our own toast.
|
||||||
Utils.showToastLong(str("revanced_sb_settings_export_failed", ex));
|
Utils.showToastLong(str("revanced_sb_settings_export_failed", ex));
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -184,7 +193,7 @@ public class SponsorBlockSettings {
|
|||||||
if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateId()
|
if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateId()
|
||||||
&& !Settings.SB_HIDE_EXPORT_WARNING.get()) {
|
&& !Settings.SB_HIDE_EXPORT_WARNING.get()) {
|
||||||
// Create the custom dialog.
|
// Create the custom dialog.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
dialogContext,
|
dialogContext,
|
||||||
null, // No title.
|
null, // No title.
|
||||||
str("revanced_sb_settings_revanced_export_user_id_warning"), // Message.
|
str("revanced_sb_settings_revanced_export_user_id_warning"), // Message.
|
||||||
@@ -217,15 +226,12 @@ public class SponsorBlockSettings {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Verify url is only the server address and does not contain a path such as: "https://sponsor.ajay.app/api/"
|
// Verify url is only the server address and does not contain a path such as: "https://sponsor.ajay.app/api/"
|
||||||
// Could use Patterns.compile, but this is simpler
|
// Could use Patterns.compile, but this is simpler.
|
||||||
final int lastDotIndex = serverAddress.lastIndexOf('.');
|
final int lastDotIndex = serverAddress.lastIndexOf('.');
|
||||||
if (lastDotIndex != -1 && serverAddress.substring(lastDotIndex).contains("/")) {
|
return lastDotIndex > 0 && !serverAddress.substring(lastDotIndex).contains("/");
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Optionally, could also verify the domain exists using "InetAddress.getByName(serverAddress)"
|
// Optionally, could also verify the domain exists using "InetAddress.getByName(serverAddress)"
|
||||||
// but that should not be done on the main thread.
|
// but that should not be done on the main thread.
|
||||||
// Instead, assume the domain exists and the user knows what they're doing.
|
// Instead, assume the domain exists and the user knows what they're doing.
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -251,6 +257,22 @@ public class SponsorBlockSettings {
|
|||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String migrateOldColorString(String colorString, float opacity) {
|
||||||
|
if (colorString.length() >= 8) {
|
||||||
|
return colorString;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change color string from #RGB to #ARGB using default alpha.
|
||||||
|
if (colorString.startsWith("#")) {
|
||||||
|
colorString = colorString.substring(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
String alphaHex = String.format(Locale.US, "%02X", (int)(opacity * 255));
|
||||||
|
String argbColorString = '#' + alphaHex + colorString.substring(0, 6);
|
||||||
|
Logger.printDebug(() -> "Migrating old color string with default opacity: " + argbColorString);
|
||||||
|
return argbColorString;
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean initialized;
|
private static boolean initialized;
|
||||||
|
|
||||||
public static void initialize() {
|
public static void initialize() {
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ import android.text.style.ForegroundColorSpan;
|
|||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
@@ -53,11 +51,11 @@ public class SponsorBlockUtils {
|
|||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
switch (which) {
|
switch (which) {
|
||||||
case DialogInterface.BUTTON_NEGATIVE:
|
case DialogInterface.BUTTON_NEGATIVE:
|
||||||
// start
|
// Start.
|
||||||
newSponsorSegmentStartMillis = newSponsorSegmentDialogShownMillis;
|
newSponsorSegmentStartMillis = newSponsorSegmentDialogShownMillis;
|
||||||
break;
|
break;
|
||||||
case DialogInterface.BUTTON_POSITIVE:
|
case DialogInterface.BUTTON_POSITIVE:
|
||||||
// end
|
// End.
|
||||||
newSponsorSegmentEndMillis = newSponsorSegmentDialogShownMillis;
|
newSponsorSegmentEndMillis = newSponsorSegmentDialogShownMillis;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -98,7 +96,7 @@ public class SponsorBlockUtils {
|
|||||||
SegmentCategory[] categories = SegmentCategory.categoriesWithoutHighlights();
|
SegmentCategory[] categories = SegmentCategory.categoriesWithoutHighlights();
|
||||||
CharSequence[] titles = new CharSequence[categories.length];
|
CharSequence[] titles = new CharSequence[categories.length];
|
||||||
for (int i = 0, length = categories.length; i < length; i++) {
|
for (int i = 0, length = categories.length; i < length; i++) {
|
||||||
titles[i] = categories[i].getTitleWithColorDot();
|
titles[i] = categories[i].getTitle().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
newUserCreatedSegmentCategory = null;
|
newUserCreatedSegmentCategory = null;
|
||||||
@@ -163,7 +161,7 @@ public class SponsorBlockUtils {
|
|||||||
SponsorSegment segment = segments[which];
|
SponsorSegment segment = segments[which];
|
||||||
|
|
||||||
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
|
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
|
||||||
? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category
|
? SegmentVote.voteTypesWithoutCategoryChange // Highlight segments cannot change category.
|
||||||
: SegmentVote.values();
|
: SegmentVote.values();
|
||||||
final int voteOptionsLength = voteOptions.length;
|
final int voteOptionsLength = voteOptions.length;
|
||||||
final boolean userIsVip = Settings.SB_USER_IS_VIP.get();
|
final boolean userIsVip = Settings.SB_USER_IS_VIP.get();
|
||||||
@@ -282,7 +280,7 @@ public class SponsorBlockUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void onVotingClicked(@NonNull Context context) {
|
public static void onVotingClicked(Context context) {
|
||||||
try {
|
try {
|
||||||
Utils.verifyOnMainThread();
|
Utils.verifyOnMainThread();
|
||||||
SponsorSegment[] segments = SegmentPlaybackController.getSegments();
|
SponsorSegment[] segments = SegmentPlaybackController.getSegments();
|
||||||
@@ -304,7 +302,7 @@ public class SponsorBlockUtils {
|
|||||||
|
|
||||||
SpannableStringBuilder spannableBuilder = new SpannableStringBuilder();
|
SpannableStringBuilder spannableBuilder = new SpannableStringBuilder();
|
||||||
|
|
||||||
spannableBuilder.append(segment.category.getTitleWithColorDot());
|
spannableBuilder.append(segment.category.getTitle().toString());
|
||||||
spannableBuilder.append('\n');
|
spannableBuilder.append('\n');
|
||||||
|
|
||||||
String startTime = formatSegmentTime(segment.start);
|
String startTime = formatSegmentTime(segment.start);
|
||||||
@@ -317,7 +315,7 @@ public class SponsorBlockUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (i + 1 != numberOfSegments) {
|
if (i + 1 != numberOfSegments) {
|
||||||
// prevents trailing new line after last segment
|
// Prevents trailing new line after last segment.
|
||||||
spannableBuilder.append('\n');
|
spannableBuilder.append('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,13 +331,13 @@ public class SponsorBlockUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void onNewCategorySelect(@NonNull SponsorSegment segment, @NonNull Context context) {
|
private static void onNewCategorySelect(SponsorSegment segment, Context context) {
|
||||||
try {
|
try {
|
||||||
Utils.verifyOnMainThread();
|
Utils.verifyOnMainThread();
|
||||||
final SegmentCategory[] values = SegmentCategory.categoriesWithoutHighlights();
|
final SegmentCategory[] values = SegmentCategory.categoriesWithoutHighlights();
|
||||||
CharSequence[] titles = new CharSequence[values.length];
|
CharSequence[] titles = new CharSequence[values.length];
|
||||||
for (int i = 0; i < values.length; i++) {
|
for (int i = 0, length = values.length; i < length; i++) {
|
||||||
titles[i] = values[i].getTitleWithColorDot();
|
titles[i] = values[i].getTitle().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context)
|
||||||
@@ -370,7 +368,6 @@ public class SponsorBlockUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static void sendViewRequestAsync(SponsorSegment segment) {
|
static void sendViewRequestAsync(SponsorSegment segment) {
|
||||||
if (segment.recordedAsSkipped || segment.category == SegmentCategory.UNSUBMITTED) {
|
if (segment.recordedAsSkipped || segment.category == SegmentCategory.UNSUBMITTED) {
|
||||||
return;
|
return;
|
||||||
@@ -424,7 +421,6 @@ public class SponsorBlockUtils {
|
|||||||
String secondsStr = matcher.group(4);
|
String secondsStr = matcher.group(4);
|
||||||
String millisecondsStr = matcher.group(6); // Milliseconds is optional.
|
String millisecondsStr = matcher.group(6); // Milliseconds is optional.
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final int hours = (hoursStr != null) ? Integer.parseInt(hoursStr) : 0;
|
final int hours = (hoursStr != null) ? Integer.parseInt(hoursStr) : 0;
|
||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
|
|||||||
@@ -1,16 +1,11 @@
|
|||||||
package app.revanced.extension.youtube.sponsorblock.objects;
|
package app.revanced.extension.youtube.sponsorblock.objects;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.sf;
|
import static app.revanced.extension.shared.StringRef.sf;
|
||||||
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.COLOR_DOT_STRING;
|
|
||||||
import static app.revanced.extension.youtube.settings.Settings.*;
|
import static app.revanced.extension.youtube.settings.Settings.*;
|
||||||
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.text.Spannable;
|
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.ForegroundColorSpan;
|
|
||||||
import android.text.style.RelativeSizeSpan;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -26,40 +21,39 @@ import java.util.Objects;
|
|||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.StringRef;
|
import app.revanced.extension.shared.StringRef;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.FloatSetting;
|
|
||||||
import app.revanced.extension.shared.settings.StringSetting;
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
public enum SegmentCategory {
|
public enum SegmentCategory {
|
||||||
SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_segments_sponsor_sum"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"),
|
SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_segments_sponsor_sum"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"),
|
||||||
SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR, SB_CATEGORY_SPONSOR_OPACITY),
|
SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR),
|
||||||
SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_segments_selfpromo_sum"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"),
|
SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_segments_selfpromo_sum"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"),
|
||||||
SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR, SB_CATEGORY_SELF_PROMO_OPACITY),
|
SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR),
|
||||||
INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_segments_interaction_sum"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"),
|
INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_segments_interaction_sum"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"),
|
||||||
SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR, SB_CATEGORY_INTERACTION_OPACITY),
|
SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR),
|
||||||
/**
|
/**
|
||||||
* Unique category that is treated differently than the rest.
|
* Unique category that is treated differently than the rest.
|
||||||
*/
|
*/
|
||||||
HIGHLIGHT("poi_highlight", sf("revanced_sb_segments_highlight"), sf("revanced_sb_segments_highlight_sum"), sf("revanced_sb_skip_button_highlight"), sf("revanced_sb_skipped_highlight"),
|
HIGHLIGHT("poi_highlight", sf("revanced_sb_segments_highlight"), sf("revanced_sb_segments_highlight_sum"), sf("revanced_sb_skip_button_highlight"), sf("revanced_sb_skipped_highlight"),
|
||||||
SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR, SB_CATEGORY_HIGHLIGHT_OPACITY),
|
SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR),
|
||||||
INTRO("intro", sf("revanced_sb_segments_intro"), sf("revanced_sb_segments_intro_sum"),
|
INTRO("intro", sf("revanced_sb_segments_intro"), sf("revanced_sb_segments_intro_sum"),
|
||||||
sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"),
|
sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"),
|
||||||
sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"),
|
sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"),
|
||||||
SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR, SB_CATEGORY_INTRO_OPACITY),
|
SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR),
|
||||||
OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_segments_outro_sum"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"),
|
OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_segments_outro_sum"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"),
|
||||||
SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR, SB_CATEGORY_OUTRO_OPACITY),
|
SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR),
|
||||||
PREVIEW("preview", sf("revanced_sb_segments_preview"), sf("revanced_sb_segments_preview_sum"),
|
PREVIEW("preview", sf("revanced_sb_segments_preview"), sf("revanced_sb_segments_preview_sum"),
|
||||||
sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"),
|
sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"),
|
||||||
sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"),
|
sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"),
|
||||||
SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR, SB_CATEGORY_PREVIEW_OPACITY),
|
SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR),
|
||||||
HOOK("hook", sf("revanced_sb_segments_hook"), sf("revanced_sb_segments_hook_sum"), sf("revanced_sb_skip_button_hook"), sf("revanced_sb_skipped_hook"),
|
HOOK("hook", sf("revanced_sb_segments_hook"), sf("revanced_sb_segments_hook_sum"), sf("revanced_sb_skip_button_hook"), sf("revanced_sb_skipped_hook"),
|
||||||
SB_CATEGORY_HOOK, SB_CATEGORY_HOOK_COLOR, SB_CATEGORY_HOOK_OPACITY),
|
SB_CATEGORY_HOOK, SB_CATEGORY_HOOK_COLOR),
|
||||||
FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_segments_filler_sum"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"),
|
FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_segments_filler_sum"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"),
|
||||||
SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR, SB_CATEGORY_FILLER_OPACITY),
|
SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR),
|
||||||
MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_segments_nomusic_sum"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"),
|
MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_segments_nomusic_sum"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"),
|
||||||
SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR, SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY),
|
SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR),
|
||||||
UNSUBMITTED("unsubmitted", StringRef.empty, StringRef.empty, sf("revanced_sb_skip_button_unsubmitted"), sf("revanced_sb_skipped_unsubmitted"),
|
UNSUBMITTED("unsubmitted", StringRef.empty, StringRef.empty, sf("revanced_sb_skip_button_unsubmitted"), sf("revanced_sb_skipped_unsubmitted"),
|
||||||
SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR, SB_CATEGORY_UNSUBMITTED_OPACITY);
|
SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR);
|
||||||
|
|
||||||
private static final StringRef skipSponsorTextCompact = sf("revanced_sb_skip_button_compact");
|
private static final StringRef skipSponsorTextCompact = sf("revanced_sb_skip_button_compact");
|
||||||
private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight");
|
private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight");
|
||||||
@@ -88,10 +82,13 @@ public enum SegmentCategory {
|
|||||||
FILLER,
|
FILLER,
|
||||||
MUSIC_OFFTOPIC,
|
MUSIC_OFFTOPIC,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final float CATEGORY_DEFAULT_OPACITY = 0.7f;
|
||||||
|
|
||||||
private static final Map<String, SegmentCategory> mValuesMap = new HashMap<>(2 * categoriesWithoutUnsubmitted.length);
|
private static final Map<String, SegmentCategory> mValuesMap = new HashMap<>(2 * categoriesWithoutUnsubmitted.length);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Categories currently enabled, formatted for an API call
|
* Categories currently enabled, formatted for an API call.
|
||||||
*/
|
*/
|
||||||
public static String sponsorBlockAPIFetchCategories = "[]";
|
public static String sponsorBlockAPIFetchCategories = "[]";
|
||||||
|
|
||||||
@@ -100,21 +97,30 @@ public enum SegmentCategory {
|
|||||||
mValuesMap.put(value.keyValue, value);
|
mValuesMap.put(value.keyValue, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of categories excluding the unsubmitted category.
|
||||||
|
*/
|
||||||
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
|
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
|
||||||
return categoriesWithoutUnsubmitted;
|
return categoriesWithoutUnsubmitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of categories excluding the highlight category.
|
||||||
|
*/
|
||||||
public static SegmentCategory[] categoriesWithoutHighlights() {
|
public static SegmentCategory[] categoriesWithoutHighlights() {
|
||||||
return categoriesWithoutHighlights;
|
return categoriesWithoutHighlights;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a category by its key.
|
||||||
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public static SegmentCategory byCategoryKey(@NonNull String key) {
|
public static SegmentCategory byCategoryKey(@NonNull String key) {
|
||||||
return mValuesMap.get(key);
|
return mValuesMap.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must be called if behavior of any category is changed.
|
* Updates the list of enabled categories for API calls. Must be called when any category's behavior changes.
|
||||||
*/
|
*/
|
||||||
public static void updateEnabledCategories() {
|
public static void updateEnabledCategories() {
|
||||||
Utils.verifyOnMainThread();
|
Utils.verifyOnMainThread();
|
||||||
@@ -134,6 +140,9 @@ public enum SegmentCategory {
|
|||||||
sponsorBlockAPIFetchCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]";
|
sponsorBlockAPIFetchCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads all category settings from persistent storage.
|
||||||
|
*/
|
||||||
public static void loadAllCategoriesFromSettings() {
|
public static void loadAllCategoriesFromSettings() {
|
||||||
for (SegmentCategory category : values()) {
|
for (SegmentCategory category : values()) {
|
||||||
category.loadFromSettings();
|
category.loadFromSettings();
|
||||||
@@ -141,45 +150,35 @@ public enum SegmentCategory {
|
|||||||
updateEnabledCategories();
|
updateEnabledCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
public static int applyOpacityToColor(@ColorInt int color, float opacity) {
|
|
||||||
if (opacity < 0 || opacity > 1.0f) {
|
|
||||||
throw new IllegalArgumentException("Invalid opacity: " + opacity);
|
|
||||||
}
|
|
||||||
final int opacityInt = (int) (255 * opacity);
|
|
||||||
return (color & 0x00FFFFFF) | (opacityInt << 24);
|
|
||||||
}
|
|
||||||
|
|
||||||
public final String keyValue;
|
public final String keyValue;
|
||||||
public final StringSetting behaviorSetting; // TODO: Replace with EnumSetting.
|
public final StringSetting behaviorSetting;
|
||||||
private final StringSetting colorSetting;
|
public final StringSetting colorSetting;
|
||||||
private final FloatSetting opacitySetting;
|
|
||||||
|
|
||||||
public final StringRef title;
|
public final StringRef title;
|
||||||
public final StringRef description;
|
public final StringRef description;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skip button text, if the skip occurs in the first quarter of the video
|
* Skip button text, if the skip occurs in the first quarter of the video.
|
||||||
*/
|
*/
|
||||||
public final StringRef skipButtonTextBeginning;
|
public final StringRef skipButtonTextBeginning;
|
||||||
/**
|
/**
|
||||||
* Skip button text, if the skip occurs in the middle half of the video
|
* Skip button text, if the skip occurs in the middle half of the video.
|
||||||
*/
|
*/
|
||||||
public final StringRef skipButtonTextMiddle;
|
public final StringRef skipButtonTextMiddle;
|
||||||
/**
|
/**
|
||||||
* Skip button text, if the skip occurs in the last quarter of the video
|
* Skip button text, if the skip occurs in the last quarter of the video.
|
||||||
*/
|
*/
|
||||||
public final StringRef skipButtonTextEnd;
|
public final StringRef skipButtonTextEnd;
|
||||||
/**
|
/**
|
||||||
* Skipped segment toast, if the skip occurred in the first quarter of the video
|
* Skipped segment toast, if the skip occurred in the first quarter of the video.
|
||||||
*/
|
*/
|
||||||
public final StringRef skippedToastBeginning;
|
public final StringRef skippedToastBeginning;
|
||||||
/**
|
/**
|
||||||
* Skipped segment toast, if the skip occurred in the middle half of the video
|
* Skipped segment toast, if the skip occurred in the middle half of the video.
|
||||||
*/
|
*/
|
||||||
public final StringRef skippedToastMiddle;
|
public final StringRef skippedToastMiddle;
|
||||||
/**
|
/**
|
||||||
* Skipped segment toast, if the skip occurred in the last quarter of the video
|
* Skipped segment toast, if the skip occurred in the last quarter of the video.
|
||||||
*/
|
*/
|
||||||
public final StringRef skippedToastEnd;
|
public final StringRef skippedToastEnd;
|
||||||
|
|
||||||
@@ -193,7 +192,7 @@ public enum SegmentCategory {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
|
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
|
||||||
* Caller must also {@link #updateEnabledCategories()}.
|
* Caller must also call {@link #updateEnabledCategories()}.
|
||||||
*/
|
*/
|
||||||
public CategoryBehaviour behaviour = CategoryBehaviour.IGNORE;
|
public CategoryBehaviour behaviour = CategoryBehaviour.IGNORE;
|
||||||
|
|
||||||
@@ -201,19 +200,19 @@ public enum SegmentCategory {
|
|||||||
StringRef skipButtonText,
|
StringRef skipButtonText,
|
||||||
StringRef skippedToastText,
|
StringRef skippedToastText,
|
||||||
StringSetting behavior,
|
StringSetting behavior,
|
||||||
StringSetting color, FloatSetting opacity) {
|
StringSetting color) {
|
||||||
this(keyValue, title, description,
|
this(keyValue, title, description,
|
||||||
skipButtonText, skipButtonText, skipButtonText,
|
skipButtonText, skipButtonText, skipButtonText,
|
||||||
skippedToastText, skippedToastText, skippedToastText,
|
skippedToastText, skippedToastText, skippedToastText,
|
||||||
behavior,
|
behavior,
|
||||||
color, opacity);
|
color);
|
||||||
}
|
}
|
||||||
|
|
||||||
SegmentCategory(String keyValue, StringRef title, StringRef description,
|
SegmentCategory(String keyValue, StringRef title, StringRef description,
|
||||||
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
|
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
|
||||||
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
|
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
|
||||||
StringSetting behavior,
|
StringSetting behavior,
|
||||||
StringSetting color, FloatSetting opacity) {
|
StringSetting color) {
|
||||||
this.keyValue = Objects.requireNonNull(keyValue);
|
this.keyValue = Objects.requireNonNull(keyValue);
|
||||||
this.title = Objects.requireNonNull(title);
|
this.title = Objects.requireNonNull(title);
|
||||||
this.description = Objects.requireNonNull(description);
|
this.description = Objects.requireNonNull(description);
|
||||||
@@ -225,11 +224,13 @@ public enum SegmentCategory {
|
|||||||
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
|
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
|
||||||
this.behaviorSetting = Objects.requireNonNull(behavior);
|
this.behaviorSetting = Objects.requireNonNull(behavior);
|
||||||
this.colorSetting = Objects.requireNonNull(color);
|
this.colorSetting = Objects.requireNonNull(color);
|
||||||
this.opacitySetting = Objects.requireNonNull(opacity);
|
|
||||||
this.paint = new Paint();
|
this.paint = new Paint();
|
||||||
loadFromSettings();
|
loadFromSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the category's behavior and color from settings.
|
||||||
|
*/
|
||||||
private void loadFromSettings() {
|
private void loadFromSettings() {
|
||||||
String behaviorString = behaviorSetting.get();
|
String behaviorString = behaviorSetting.get();
|
||||||
CategoryBehaviour savedBehavior = CategoryBehaviour.byReVancedKeyValue(behaviorString);
|
CategoryBehaviour savedBehavior = CategoryBehaviour.byReVancedKeyValue(behaviorString);
|
||||||
@@ -242,118 +243,93 @@ public enum SegmentCategory {
|
|||||||
this.behaviour = savedBehavior;
|
this.behaviour = savedBehavior;
|
||||||
|
|
||||||
String colorString = colorSetting.get();
|
String colorString = colorSetting.get();
|
||||||
final float opacity = opacitySetting.get();
|
|
||||||
try {
|
try {
|
||||||
setColor(colorString);
|
setColorWithOpacity(colorString);
|
||||||
setOpacity(opacity);
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Invalid color: " + colorString + " opacity: " + opacity, ex);
|
Logger.printException(() -> "Invalid color: " + colorString, ex);
|
||||||
colorSetting.resetToDefault();
|
colorSetting.resetToDefault();
|
||||||
opacitySetting.resetToDefault();
|
|
||||||
loadFromSettings();
|
loadFromSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the behavior of the category and saves it to settings.
|
||||||
|
*/
|
||||||
public void setBehaviour(CategoryBehaviour behaviour) {
|
public void setBehaviour(CategoryBehaviour behaviour) {
|
||||||
this.behaviour = Objects.requireNonNull(behaviour);
|
this.behaviour = Objects.requireNonNull(behaviour);
|
||||||
this.behaviorSetting.save(behaviour.reVancedKeyValue);
|
this.behaviorSetting.save(behaviour.reVancedKeyValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateColor() {
|
/**
|
||||||
color = applyOpacityToColor(color, opacitySetting.get());
|
* Sets the segment color with opacity from a color string in #AARRGGBB format.
|
||||||
|
*/
|
||||||
|
public void setColorWithOpacity(String colorString) throws IllegalArgumentException {
|
||||||
|
int colorWithOpacity = Color.parseColor(colorString);
|
||||||
|
colorSetting.save(String.format(Locale.US, "#%08X", colorWithOpacity));
|
||||||
|
color = colorWithOpacity;
|
||||||
paint.setColor(color);
|
paint.setColor(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param opacity Segment color opacity between [0, 1].
|
* @param opacity [0, 1] opacity value.
|
||||||
*/
|
*/
|
||||||
public void setOpacity(float opacity) throws IllegalArgumentException {
|
public void setOpacity(double opacity) {
|
||||||
if (opacity < 0 || opacity > 1) {
|
color = Color.argb((int) (opacity * 255), Color.red(color), Color.green(color), Color.blue(color));
|
||||||
throw new IllegalArgumentException("Invalid opacity: " + opacity);
|
paint.setColor(color);
|
||||||
}
|
|
||||||
|
|
||||||
opacitySetting.save(opacity);
|
|
||||||
updateColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getOpacity() {
|
|
||||||
return opacitySetting.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
public float getOpacityDefault() {
|
|
||||||
return opacitySetting.defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetColorAndOpacity() {
|
|
||||||
setColor(colorSetting.defaultValue);
|
|
||||||
setOpacity(opacitySetting.defaultValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param colorString Segment color with #RRGGBB format.
|
* Gets the color with opacity applied (ARGB).
|
||||||
*/
|
|
||||||
public void setColor(String colorString) throws IllegalArgumentException {
|
|
||||||
color = Color.parseColor(colorString);
|
|
||||||
colorSetting.save(colorString);
|
|
||||||
|
|
||||||
updateColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Integer color of #RRGGBB format.
|
|
||||||
*/
|
*/
|
||||||
@ColorInt
|
@ColorInt
|
||||||
public int getColorNoOpacity() {
|
public int getColorWithOpacity() {
|
||||||
return color & 0x00FFFFFF;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Integer color of #RRGGBB format.
|
* @return The default color with opacity applied.
|
||||||
*/
|
*/
|
||||||
@ColorInt
|
@ColorInt
|
||||||
public int getColorNoOpacityDefault() {
|
public int getDefaultColorWithOpacity() {
|
||||||
return Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
|
return Color.parseColor(colorSetting.defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Hex color string of #RRGGBB format with no opacity level.
|
* Gets the color as a hex string with opacity (#AARRGGBB).
|
||||||
*/
|
*/
|
||||||
public String getColorString() {
|
public String getColorStringWithOpacity() {
|
||||||
return String.format(Locale.US, "#%06X", getColorNoOpacity());
|
return String.format(Locale.US, "#%08X", getColorWithOpacity());
|
||||||
}
|
|
||||||
|
|
||||||
private static SpannableString getCategoryColorDotSpan(String text, @ColorInt int color) {
|
|
||||||
SpannableString dotSpan = new SpannableString(COLOR_DOT_STRING + text);
|
|
||||||
dotSpan.setSpan(new ForegroundColorSpan(color), 0, 1,
|
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
return dotSpan;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static SpannableString getCategoryColorDot(@ColorInt int color) {
|
|
||||||
SpannableString dotSpan = new SpannableString(COLOR_DOT_STRING);
|
|
||||||
dotSpan.setSpan(new ForegroundColorSpan(color), 0, 1,
|
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
dotSpan.setSpan(new RelativeSizeSpan(1.5f), 0, 1,
|
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
return dotSpan;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SpannableString getCategoryColorDot() {
|
|
||||||
return getCategoryColorDot(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SpannableString getTitleWithColorDot(@ColorInt int categoryColor) {
|
|
||||||
return getCategoryColorDotSpan(" " + title, categoryColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SpannableString getTitleWithColorDot() {
|
|
||||||
return getTitleWithColorDot(color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param segmentStartTime video time the segment category started
|
* @return The color as a hex string without opacity (#RRGGBB).
|
||||||
* @param videoLength length of the video
|
*/
|
||||||
* @return the skip button text
|
public String getColorStringWithoutOpacity() {
|
||||||
|
final int colorNoOpacity = getColorWithOpacity() & 0x00FFFFFF;
|
||||||
|
return String.format(Locale.US, "#%06X", colorNoOpacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return [0, 1] opacity value.
|
||||||
|
*/
|
||||||
|
public double getOpacity() {
|
||||||
|
double opacity = Color.alpha(color) / 255.0;
|
||||||
|
return Math.round(opacity * 100.0) / 100.0; // Round to 2 decimal digits.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the title of the category.
|
||||||
|
*/
|
||||||
|
public StringRef getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the skip button text based on segment position.
|
||||||
|
*
|
||||||
|
* @param segmentStartTime Video time the segment category started.
|
||||||
|
* @param videoLength Length of the video.
|
||||||
|
* @return The skip button text.
|
||||||
*/
|
*/
|
||||||
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
|
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
|
||||||
if (Settings.SB_COMPACT_SKIP_BUTTON.get()) {
|
if (Settings.SB_COMPACT_SKIP_BUTTON.get()) {
|
||||||
@@ -375,9 +351,11 @@ public enum SegmentCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param segmentStartTime video time the segment category started
|
* Gets the skipped segment toast message based on segment position.
|
||||||
* @param videoLength length of the video
|
*
|
||||||
* @return 'skipped segment' toast message
|
* @param segmentStartTime Video time the segment category started.
|
||||||
|
* @param videoLength Length of the video.
|
||||||
|
* @return The skipped segment toast message.
|
||||||
*/
|
*/
|
||||||
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
|
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
|
||||||
if (videoLength == 0) {
|
if (videoLength == 0) {
|
||||||
|
|||||||
@@ -1,371 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.sponsorblock.objects;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
|
||||||
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
|
|
||||||
import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor;
|
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Typeface;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.ListPreference;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.InputType;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.util.Pair;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.*;
|
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
|
||||||
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
|
||||||
import app.revanced.extension.shared.settings.preference.ColorPickerView;
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public class SegmentCategoryListPreference extends ListPreference {
|
|
||||||
private final SegmentCategory category;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* RGB format (no alpha).
|
|
||||||
*/
|
|
||||||
@ColorInt
|
|
||||||
private int categoryColor;
|
|
||||||
/**
|
|
||||||
* [0, 1]
|
|
||||||
*/
|
|
||||||
private float categoryOpacity;
|
|
||||||
private int selectedDialogEntryIndex;
|
|
||||||
|
|
||||||
private TextView dialogColorDotView;
|
|
||||||
private EditText dialogColorEditText;
|
|
||||||
private EditText dialogOpacityEditText;
|
|
||||||
private ColorPickerView dialogColorPickerView;
|
|
||||||
private Dialog dialog;
|
|
||||||
|
|
||||||
public SegmentCategoryListPreference(Context context, SegmentCategory category) {
|
|
||||||
super(context);
|
|
||||||
this.category = Objects.requireNonNull(category);
|
|
||||||
|
|
||||||
// Edit: Using preferences to sync together multiple pieces
|
|
||||||
// of code is messy and should be rethought.
|
|
||||||
setKey(category.behaviorSetting.key);
|
|
||||||
setDefaultValue(category.behaviorSetting.defaultValue);
|
|
||||||
|
|
||||||
final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
|
|
||||||
setEntries(isHighlightCategory
|
|
||||||
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
|
|
||||||
: CategoryBehaviour.getBehaviorDescriptions());
|
|
||||||
setEntryValues(isHighlightCategory
|
|
||||||
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
|
|
||||||
: CategoryBehaviour.getBehaviorKeyValues());
|
|
||||||
super.setSummary(category.description.toString());
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void showDialog(Bundle state) {
|
|
||||||
try {
|
|
||||||
Context context = getContext();
|
|
||||||
categoryColor = category.getColorNoOpacity();
|
|
||||||
categoryOpacity = category.getOpacity();
|
|
||||||
selectedDialogEntryIndex = findIndexOfValue(getValue());
|
|
||||||
|
|
||||||
// Create the main layout for the dialog content.
|
|
||||||
LinearLayout contentLayout = new LinearLayout(context);
|
|
||||||
contentLayout.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
|
|
||||||
// Add behavior selection radio buttons.
|
|
||||||
RadioGroup radioGroup = new RadioGroup(context);
|
|
||||||
radioGroup.setOrientation(RadioGroup.VERTICAL);
|
|
||||||
CharSequence[] entries = getEntries();
|
|
||||||
for (int i = 0; i < entries.length; i++) {
|
|
||||||
RadioButton radioButton = new RadioButton(context);
|
|
||||||
radioButton.setText(entries[i]);
|
|
||||||
radioButton.setId(i);
|
|
||||||
radioButton.setChecked(i == selectedDialogEntryIndex);
|
|
||||||
radioGroup.addView(radioButton);
|
|
||||||
}
|
|
||||||
radioGroup.setOnCheckedChangeListener((group, checkedId) -> selectedDialogEntryIndex = checkedId);
|
|
||||||
radioGroup.setPadding(dipToPixels(10), 0, 0, 0);
|
|
||||||
contentLayout.addView(radioGroup);
|
|
||||||
|
|
||||||
// Inflate the color picker view.
|
|
||||||
View colorPickerContainer = LayoutInflater.from(context)
|
|
||||||
.inflate(getResourceIdentifier("revanced_color_picker", "layout"), null);
|
|
||||||
dialogColorPickerView = colorPickerContainer.findViewById(
|
|
||||||
getResourceIdentifier("revanced_color_picker_view", "id"));
|
|
||||||
dialogColorPickerView.setColor(categoryColor);
|
|
||||||
contentLayout.addView(colorPickerContainer);
|
|
||||||
|
|
||||||
// Grid layout for color and opacity inputs.
|
|
||||||
GridLayout gridLayout = new GridLayout(context);
|
|
||||||
gridLayout.setColumnCount(3);
|
|
||||||
gridLayout.setRowCount(2);
|
|
||||||
gridLayout.setPadding(dipToPixels(16), 0, 0, 0);
|
|
||||||
|
|
||||||
GridLayout.LayoutParams gridParams = new GridLayout.LayoutParams();
|
|
||||||
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
|
||||||
gridParams.columnSpec = GridLayout.spec(0); // First column.
|
|
||||||
TextView colorTextLabel = new TextView(context);
|
|
||||||
colorTextLabel.setText(str("revanced_sb_color_dot_label"));
|
|
||||||
colorTextLabel.setLayoutParams(gridParams);
|
|
||||||
gridLayout.addView(colorTextLabel);
|
|
||||||
|
|
||||||
gridParams = new GridLayout.LayoutParams();
|
|
||||||
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
|
||||||
gridParams.columnSpec = GridLayout.spec(1); // Second column.
|
|
||||||
gridParams.setMargins(0, 0, dipToPixels(10), 0);
|
|
||||||
dialogColorDotView = new TextView(context);
|
|
||||||
dialogColorDotView.setLayoutParams(gridParams);
|
|
||||||
gridLayout.addView(dialogColorDotView);
|
|
||||||
updateCategoryColorDot();
|
|
||||||
|
|
||||||
gridParams = new GridLayout.LayoutParams();
|
|
||||||
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
|
||||||
gridParams.columnSpec = GridLayout.spec(2); // Third column.
|
|
||||||
dialogColorEditText = new EditText(context);
|
|
||||||
dialogColorEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
|
||||||
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
|
||||||
dialogColorEditText.setAutofillHints((String) null);
|
|
||||||
dialogColorEditText.setTypeface(Typeface.MONOSPACE);
|
|
||||||
dialogColorEditText.setTextLocale(Locale.US);
|
|
||||||
dialogColorEditText.setText(getColorString(categoryColor));
|
|
||||||
dialogColorEditText.addTextChangedListener(new TextWatcher() {
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable edit) {
|
|
||||||
try {
|
|
||||||
String colorString = edit.toString();
|
|
||||||
String normalizedColorString = ColorPickerPreference.cleanupColorCodeString(colorString);
|
|
||||||
|
|
||||||
if (!normalizedColorString.equals(colorString)) {
|
|
||||||
edit.replace(0, colorString.length(), normalizedColorString);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (normalizedColorString.length() != ColorPickerPreference.COLOR_STRING_LENGTH) {
|
|
||||||
// User is still typing out the color.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the alpha channel.
|
|
||||||
final int newColor = Color.parseColor(colorString) & 0x00FFFFFF;
|
|
||||||
// Changing view color causes callback into this class.
|
|
||||||
dialogColorPickerView.setColor(newColor);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
// Should never be reached since input is validated before using.
|
|
||||||
Logger.printException(() -> "colorEditText afterTextChanged failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
gridLayout.addView(dialogColorEditText, gridParams);
|
|
||||||
|
|
||||||
gridParams = new GridLayout.LayoutParams();
|
|
||||||
gridParams.rowSpec = GridLayout.spec(1); // Second row.
|
|
||||||
gridParams.columnSpec = GridLayout.spec(0, 1); // First and second column.
|
|
||||||
TextView opacityLabel = new TextView(context);
|
|
||||||
opacityLabel.setText(str("revanced_sb_color_opacity_label"));
|
|
||||||
opacityLabel.setLayoutParams(gridParams);
|
|
||||||
gridLayout.addView(opacityLabel);
|
|
||||||
|
|
||||||
gridParams = new GridLayout.LayoutParams();
|
|
||||||
gridParams.rowSpec = GridLayout.spec(1); // Second row.
|
|
||||||
gridParams.columnSpec = GridLayout.spec(2); // Third column.
|
|
||||||
dialogOpacityEditText = new EditText(context);
|
|
||||||
dialogOpacityEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL
|
|
||||||
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
|
||||||
dialogOpacityEditText.setAutofillHints((String) null);
|
|
||||||
dialogOpacityEditText.setTypeface(Typeface.MONOSPACE);
|
|
||||||
dialogOpacityEditText.setTextLocale(Locale.US);
|
|
||||||
dialogOpacityEditText.addTextChangedListener(new TextWatcher() {
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable edit) {
|
|
||||||
try {
|
|
||||||
String editString = edit.toString();
|
|
||||||
final int opacityStringLength = editString.length();
|
|
||||||
|
|
||||||
final int maxOpacityStringLength = 4; // [0.00, 1.00]
|
|
||||||
if (opacityStringLength > maxOpacityStringLength) {
|
|
||||||
edit.delete(maxOpacityStringLength, opacityStringLength);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final float opacity = opacityStringLength == 0
|
|
||||||
? 0
|
|
||||||
: Float.parseFloat(editString);
|
|
||||||
if (opacity < 0) {
|
|
||||||
categoryOpacity = 0;
|
|
||||||
edit.replace(0, opacityStringLength, "0");
|
|
||||||
return;
|
|
||||||
} else if (opacity > 1.0f) {
|
|
||||||
categoryOpacity = 1;
|
|
||||||
edit.replace(0, opacityStringLength, "1.0");
|
|
||||||
return;
|
|
||||||
} else if (!editString.endsWith(".")) {
|
|
||||||
// Ignore "0." and "1." until the user finishes entering a valid number.
|
|
||||||
categoryOpacity = opacity;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateCategoryColorDot();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
// Should never happen.
|
|
||||||
Logger.printException(() -> "opacityEditText afterTextChanged failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
gridLayout.addView(dialogOpacityEditText, gridParams);
|
|
||||||
updateOpacityText();
|
|
||||||
|
|
||||||
contentLayout.addView(gridLayout);
|
|
||||||
|
|
||||||
// Create ScrollView to wrap the content layout.
|
|
||||||
ScrollView contentScrollView = new ScrollView(context);
|
|
||||||
contentScrollView.setVerticalScrollBarEnabled(false); // Disable vertical scrollbar.
|
|
||||||
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); // Disable overscroll effect.
|
|
||||||
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
0,
|
|
||||||
1.0f
|
|
||||||
);
|
|
||||||
contentScrollView.setLayoutParams(scrollViewParams);
|
|
||||||
contentScrollView.addView(contentLayout);
|
|
||||||
|
|
||||||
// Create the custom dialog.
|
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
|
||||||
context,
|
|
||||||
category.title.toString(), // Title.
|
|
||||||
null, // No message (replaced by contentLayout).
|
|
||||||
null, // No EditText.
|
|
||||||
null, // OK button text.
|
|
||||||
() -> {
|
|
||||||
// OK button action.
|
|
||||||
if (selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
|
|
||||||
String value = getEntryValues()[selectedDialogEntryIndex].toString();
|
|
||||||
if (callChangeListener(value)) {
|
|
||||||
setValue(value);
|
|
||||||
category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
|
|
||||||
SegmentCategory.updateEnabledCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
category.setColor(dialogColorEditText.getText().toString());
|
|
||||||
category.setOpacity(categoryOpacity);
|
|
||||||
} catch (IllegalArgumentException ex) {
|
|
||||||
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
|
||||||
}
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
() -> {}, // Cancel button action (dismiss only).
|
|
||||||
str("revanced_settings_reset_color"), // Neutral button text.
|
|
||||||
() -> {
|
|
||||||
// Neutral button action (Reset).
|
|
||||||
try {
|
|
||||||
// Setting view color causes callback to update the UI.
|
|
||||||
dialogColorPickerView.setColor(category.getColorNoOpacityDefault());
|
|
||||||
|
|
||||||
categoryOpacity = category.getOpacityDefault();
|
|
||||||
updateOpacityText();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "resetButton onClick failure", ex);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
false // Do not dismiss dialog on Neutral button click.
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add the ScrollView to the dialog's main layout.
|
|
||||||
LinearLayout dialogMainLayout = dialogPair.second;
|
|
||||||
dialogMainLayout.addView(contentScrollView, dialogMainLayout.getChildCount() - 1);
|
|
||||||
|
|
||||||
// Set up color picker listener.
|
|
||||||
// Do last to prevent listener callbacks while setting up view.
|
|
||||||
dialogColorPickerView.setOnColorChangedListener(color -> {
|
|
||||||
if (categoryColor == color) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
categoryColor = color;
|
|
||||||
String hexColor = getColorString(color);
|
|
||||||
Logger.printDebug(() -> "onColorChanged: " + hexColor);
|
|
||||||
|
|
||||||
updateCategoryColorDot();
|
|
||||||
dialogColorEditText.setText(hexColor);
|
|
||||||
dialogColorEditText.setSelection(hexColor.length());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show the dialog.
|
|
||||||
dialog = dialogPair.first;
|
|
||||||
dialog.show();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "showDialog failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDialogClosed(boolean positiveResult) {
|
|
||||||
// Nullify dialog references.
|
|
||||||
dialogColorDotView = null;
|
|
||||||
dialogColorEditText = null;
|
|
||||||
dialogOpacityEditText = null;
|
|
||||||
dialogColorPickerView = null;
|
|
||||||
|
|
||||||
if (dialog != null) {
|
|
||||||
dialog.dismiss();
|
|
||||||
dialog = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorInt
|
|
||||||
private int applyOpacityToCategoryColor() {
|
|
||||||
return applyOpacityToColor(categoryColor, categoryOpacity);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateUI() {
|
|
||||||
categoryColor = category.getColorNoOpacity();
|
|
||||||
categoryOpacity = category.getOpacity();
|
|
||||||
|
|
||||||
setTitle(category.getTitleWithColorDot(applyOpacityToCategoryColor()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCategoryColorDot() {
|
|
||||||
dialogColorDotView.setText(SegmentCategory.getCategoryColorDot(applyOpacityToCategoryColor()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateOpacityText() {
|
|
||||||
dialogOpacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setSummary(CharSequence summary) {
|
|
||||||
// Ignore calls to set the summary.
|
|
||||||
// Summary is always the description of the category.
|
|
||||||
//
|
|
||||||
// This is required otherwise the ReVanced preference fragment
|
|
||||||
// sets all ListPreference summaries to show the current selection.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
package app.revanced.extension.youtube.sponsorblock.objects;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
import static app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings.migrateOldColorString;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.RadioButton;
|
||||||
|
import android.widget.RadioGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
||||||
|
import app.revanced.extension.shared.ui.ColorDot;
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class SegmentCategoryPreference extends ColorPickerPreference {
|
||||||
|
public final SegmentCategory category;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View displaying a colored dot in the widget area.
|
||||||
|
*/
|
||||||
|
private View widgetColorDot;
|
||||||
|
|
||||||
|
// Fields to store dialog state for the OK button handler.
|
||||||
|
private int selectedDialogEntryIndex;
|
||||||
|
private CharSequence[] entryValues;
|
||||||
|
|
||||||
|
|
||||||
|
public SegmentCategoryPreference(Context context, SegmentCategory category) {
|
||||||
|
super(context);
|
||||||
|
this.category = Objects.requireNonNull(category);
|
||||||
|
|
||||||
|
// Set key to color setting for persistence.
|
||||||
|
// Edit: Using preferences to sync together multiple pieces of code is messy and should be rethought.
|
||||||
|
setKey(category.colorSetting.key);
|
||||||
|
setTitle(category.title.toString());
|
||||||
|
setSummary(category.description.toString());
|
||||||
|
|
||||||
|
// Enable opacity slider for this preference.
|
||||||
|
setOpacitySliderEnabled(true);
|
||||||
|
|
||||||
|
setWidgetLayoutResource(LAYOUT_REVANCED_COLOR_DOT_WIDGET);
|
||||||
|
|
||||||
|
// Sync initial color from category.
|
||||||
|
setText(category.getColorStringWithOpacity());
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public final void setText(String colorString) {
|
||||||
|
try {
|
||||||
|
// Migrate old data imported in the settings UI.
|
||||||
|
// This migration is needed here because pasting into the settings
|
||||||
|
// immediately syncs the data with the preferences.
|
||||||
|
colorString = migrateOldColorString(colorString, SegmentCategory.CATEGORY_DEFAULT_OPACITY);
|
||||||
|
super.setText(colorString);
|
||||||
|
|
||||||
|
// Save to category.
|
||||||
|
category.setColorWithOpacity(colorString);
|
||||||
|
updateUI();
|
||||||
|
|
||||||
|
// Notify the listener about the color change.
|
||||||
|
if (colorChangeListener != null) {
|
||||||
|
colorChangeListener.onColorChanged(getKey(), category.getColorWithOpacity());
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||||
|
setText(category.colorSetting.defaultValue);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
String colorStringFinal = colorString;
|
||||||
|
Logger.printException(() -> "setText failure: " + colorStringFinal, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
protected View createExtraDialogContentView(Context context) {
|
||||||
|
final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
|
||||||
|
entryValues = isHighlightCategory
|
||||||
|
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
|
||||||
|
: CategoryBehaviour.getBehaviorKeyValues();
|
||||||
|
|
||||||
|
String currentBehavior = category.behaviorSetting.get();
|
||||||
|
selectedDialogEntryIndex = -1;
|
||||||
|
for (int i = 0; i < entryValues.length; i++) {
|
||||||
|
if (entryValues[i].equals(currentBehavior)) {
|
||||||
|
selectedDialogEntryIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RadioGroup radioGroup = new RadioGroup(context);
|
||||||
|
radioGroup.setOrientation(RadioGroup.VERTICAL);
|
||||||
|
CharSequence[] entries = isHighlightCategory
|
||||||
|
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
|
||||||
|
: CategoryBehaviour.getBehaviorDescriptions();
|
||||||
|
|
||||||
|
for (int i = 0; i < entries.length; i++) {
|
||||||
|
RadioButton radioButton = new RadioButton(context);
|
||||||
|
radioButton.setText(entries[i]);
|
||||||
|
radioButton.setId(i);
|
||||||
|
radioButton.setChecked(i == selectedDialogEntryIndex);
|
||||||
|
radioGroup.addView(radioButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
radioGroup.setOnCheckedChangeListener((group, checkedId) -> selectedDialogEntryIndex = checkedId);
|
||||||
|
radioGroup.setPadding(dipToPixels(10), 0, dipToPixels(10), dipToPixels(10));
|
||||||
|
return radioGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDialogOkClicked() {
|
||||||
|
if (selectedDialogEntryIndex >= 0 && entryValues != null) {
|
||||||
|
String value = entryValues[selectedDialogEntryIndex].toString();
|
||||||
|
category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
|
||||||
|
SegmentCategory.updateEnabledCategories();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDialogNeutralClicked() {
|
||||||
|
try {
|
||||||
|
final int defaultColor = category.getDefaultColorWithOpacity();
|
||||||
|
dialogColorPickerView.setColor(defaultColor);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "Reset button failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateUI() {
|
||||||
|
try {
|
||||||
|
if (category.behaviorSetting != null) {
|
||||||
|
setEnabled(category.behaviorSetting.isAvailable());
|
||||||
|
}
|
||||||
|
|
||||||
|
updateWidgetColorDot();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "updateUI failure for category: " + category.keyValue, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBindView(View view) {
|
||||||
|
super.onBindView(view);
|
||||||
|
|
||||||
|
widgetColorDot = view.findViewById(ID_PREFERENCE_COLOR_DOT);
|
||||||
|
updateWidgetColorDot();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWidgetColorDot() {
|
||||||
|
if (widgetColorDot == null) return;
|
||||||
|
|
||||||
|
ColorDot.applyColorDot(
|
||||||
|
widgetColorDot,
|
||||||
|
category.getColorWithOpacity(),
|
||||||
|
isEnabled()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
|||||||
public enum SegmentVote {
|
public enum SegmentVote {
|
||||||
UPVOTE(sf("revanced_sb_vote_upvote"), 1,false),
|
UPVOTE(sf("revanced_sb_vote_upvote"), 1,false),
|
||||||
DOWNVOTE(sf("revanced_sb_vote_downvote"), 0, true),
|
DOWNVOTE(sf("revanced_sb_vote_downvote"), 0, true),
|
||||||
CATEGORY_CHANGE(sf("revanced_sb_vote_category"), -1, true); // apiVoteType is not used for category change
|
CATEGORY_CHANGE(sf("revanced_sb_vote_category"), -1, true); // ApiVoteType is not used for category change.
|
||||||
|
|
||||||
public static final SegmentVote[] voteTypesWithoutCategoryChange = {
|
public static final SegmentVote[] voteTypesWithoutCategoryChange = {
|
||||||
UPVOTE,
|
UPVOTE,
|
||||||
@@ -104,7 +104,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
|||||||
/**
|
/**
|
||||||
* @return The start/end time in range form.
|
* @return The start/end time in range form.
|
||||||
* Range times are adjusted since it uses inclusive and Segments use exclusive.
|
* Range times are adjusted since it uses inclusive and Segments use exclusive.
|
||||||
*
|
* <p>
|
||||||
* {@link SegmentCategory#HIGHLIGHT} is unique and
|
* {@link SegmentCategory#HIGHLIGHT} is unique and
|
||||||
* returns a range from the start of the video until the highlight.
|
* returns a range from the start of the video until the highlight.
|
||||||
*/
|
*/
|
||||||
@@ -116,7 +116,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the length of this segment, in milliseconds. Always a positive number.
|
* @return the length of this segment, in milliseconds. Always a positive number.
|
||||||
*/
|
*/
|
||||||
public long length() {
|
public long length() {
|
||||||
return end - start;
|
return end - start;
|
||||||
@@ -148,8 +148,7 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
|||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (!(o instanceof SponsorSegment)) return false;
|
if (!(o instanceof SponsorSegment other)) return false;
|
||||||
SponsorSegment other = (SponsorSegment) o;
|
|
||||||
return Objects.equals(UUID, other.UUID)
|
return Objects.equals(UUID, other.UUID)
|
||||||
&& category == other.category
|
&& category == other.category
|
||||||
&& start == other.start
|
&& start == other.start
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package app.revanced.extension.youtube.sponsorblock.ui;
|
package app.revanced.extension.youtube.sponsorblock.ui;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceColor;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceDimensionPixelSize;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.ColorStateList;
|
import android.content.res.ColorStateList;
|
||||||
import android.graphics.drawable.GradientDrawable;
|
import android.graphics.drawable.GradientDrawable;
|
||||||
@@ -10,14 +14,10 @@ import android.view.ViewGroup;
|
|||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.youtube.patches.VideoInformation;
|
import app.revanced.extension.youtube.patches.VideoInformation;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceColor;
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceDimensionPixelSize;
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
|
||||||
|
|
||||||
public final class NewSegmentLayout extends FrameLayout {
|
public final class NewSegmentLayout extends FrameLayout {
|
||||||
private static final ColorStateList rippleColorStateList = new ColorStateList(
|
private static final ColorStateList rippleColorStateList = new ColorStateList(
|
||||||
@@ -45,7 +45,7 @@ public final class NewSegmentLayout extends FrameLayout {
|
|||||||
super(context, attributeSet, defStyleAttr, defStyleRes);
|
super(context, attributeSet, defStyleAttr, defStyleRes);
|
||||||
|
|
||||||
LayoutInflater.from(context).inflate(
|
LayoutInflater.from(context).inflate(
|
||||||
getResourceIdentifier(context, "revanced_sb_new_segment", "layout"), this, true
|
getResourceIdentifierOrThrow(context, "revanced_sb_new_segment", "layout"), this, true
|
||||||
);
|
);
|
||||||
|
|
||||||
initializeButton(
|
initializeButton(
|
||||||
@@ -104,7 +104,7 @@ public final class NewSegmentLayout extends FrameLayout {
|
|||||||
*/
|
*/
|
||||||
private void initializeButton(final Context context, final String resourceIdentifierName,
|
private void initializeButton(final Context context, final String resourceIdentifierName,
|
||||||
final ButtonOnClickHandlerFunction handler, final String debugMessage) {
|
final ButtonOnClickHandlerFunction handler, final String debugMessage) {
|
||||||
ImageButton button = findViewById(getResourceIdentifier(context, resourceIdentifierName, "id"));
|
ImageButton button = findViewById(getResourceIdentifierOrThrow(context, resourceIdentifierName, "id"));
|
||||||
|
|
||||||
// Add ripple effect
|
// Add ripple effect
|
||||||
RippleDrawable rippleDrawable = new RippleDrawable(
|
RippleDrawable rippleDrawable = new RippleDrawable(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import static app.revanced.extension.shared.Utils.getResourceColor;
|
|||||||
import static app.revanced.extension.shared.Utils.getResourceDimension;
|
import static app.revanced.extension.shared.Utils.getResourceDimension;
|
||||||
import static app.revanced.extension.shared.Utils.getResourceDimensionPixelSize;
|
import static app.revanced.extension.shared.Utils.getResourceDimensionPixelSize;
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
@@ -56,9 +57,11 @@ public class SkipSponsorButton extends FrameLayout {
|
|||||||
public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
|
public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) {
|
||||||
super(context, attributeSet, defStyleAttr, defStyleRes);
|
super(context, attributeSet, defStyleAttr, defStyleRes);
|
||||||
|
|
||||||
LayoutInflater.from(context).inflate(getResourceIdentifier(context, "revanced_sb_skip_sponsor_button", "layout"), this, true); // layout:skip_ad_button
|
LayoutInflater.from(context).inflate(getResourceIdentifierOrThrow(context,
|
||||||
|
"revanced_sb_skip_sponsor_button", "layout"), this, true); // layout:skip_ad_button
|
||||||
setMinimumHeight(getResourceDimensionPixelSize("ad_skip_ad_button_min_height")); // dimen:ad_skip_ad_button_min_height
|
setMinimumHeight(getResourceDimensionPixelSize("ad_skip_ad_button_min_height")); // dimen:ad_skip_ad_button_min_height
|
||||||
skipSponsorBtnContainer = Objects.requireNonNull(findViewById(getResourceIdentifier(context, "revanced_sb_skip_sponsor_button_container", "id"))); // id:skip_ad_button_container
|
skipSponsorBtnContainer = Objects.requireNonNull(findViewById(getResourceIdentifierOrThrow(
|
||||||
|
context, "revanced_sb_skip_sponsor_button_container", "id"))); // id:skip_ad_button_container
|
||||||
|
|
||||||
background = new Paint();
|
background = new Paint();
|
||||||
background.setColor(getResourceColor("skip_ad_button_background_color")); // color:skip_ad_button_background_color);
|
background.setColor(getResourceColor("skip_ad_button_background_color")); // color:skip_ad_button_background_color);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package app.revanced.extension.youtube.sponsorblock.ui;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
import app.revanced.extension.youtube.settings.preference.UrlLinkPreference;
|
import app.revanced.extension.shared.settings.preference.UrlLinkPreference;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SponsorBlockAboutPreference extends UrlLinkPreference {
|
public class SponsorBlockAboutPreference extends UrlLinkPreference {
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import android.content.Intent;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.preference.EditTextPreference;
|
import android.preference.EditTextPreference;
|
||||||
|
import android.preference.ListPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceCategory;
|
import android.preference.PreferenceCategory;
|
||||||
import android.preference.PreferenceGroup;
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.SwitchPreference;
|
import android.preference.SwitchPreference;
|
||||||
import android.text.Html;
|
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
@@ -29,13 +29,16 @@ import java.util.List;
|
|||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||||
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
|
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
|
||||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryListPreference;
|
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryPreference;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lots of old code that could be converted to a half dozen custom preferences,
|
* Lots of old code that could be converted to a half dozen custom preferences,
|
||||||
@@ -54,27 +57,9 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
*/
|
*/
|
||||||
private boolean preferencesInitialized;
|
private boolean preferencesInitialized;
|
||||||
|
|
||||||
private SwitchPreference sbEnabled;
|
|
||||||
private SwitchPreference addNewSegment;
|
|
||||||
private SwitchPreference votingEnabled;
|
|
||||||
private SwitchPreference autoHideSkipSegmentButton;
|
|
||||||
private SwitchPreference compactSkipButton;
|
|
||||||
private SwitchPreference squareLayout;
|
|
||||||
private SwitchPreference showSkipToast;
|
|
||||||
private SwitchPreference trackSkips;
|
|
||||||
private SwitchPreference showTimeWithoutSegments;
|
|
||||||
private SwitchPreference toastOnConnectionError;
|
|
||||||
private CustomDialogListPreference autoHideSkipSegmentButtonDuration;
|
|
||||||
private CustomDialogListPreference showSkipToastDuration;
|
|
||||||
|
|
||||||
private ResettableEditTextPreference newSegmentStep;
|
|
||||||
private ResettableEditTextPreference minSegmentDuration;
|
|
||||||
private EditTextPreference privateUserId;
|
|
||||||
private EditTextPreference importExport;
|
private EditTextPreference importExport;
|
||||||
private Preference apiUrl;
|
|
||||||
|
|
||||||
private PreferenceCategory segmentCategory;
|
private final List<SegmentCategoryPreference> segmentCategories = new ArrayList<>();
|
||||||
private final List<SegmentCategoryListPreference> segmentCategories = new ArrayList<>();
|
|
||||||
|
|
||||||
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
@@ -99,60 +84,17 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
try {
|
try {
|
||||||
Logger.printDebug(() -> "updateUI");
|
Logger.printDebug(() -> "updateUI");
|
||||||
|
|
||||||
final boolean enabled = Settings.SB_ENABLED.get();
|
if (!Settings.SB_ENABLED.get()) {
|
||||||
if (!enabled) {
|
|
||||||
SponsorBlockViewController.hideAll();
|
SponsorBlockViewController.hideAll();
|
||||||
SegmentPlaybackController.setCurrentVideoId(null);
|
SegmentPlaybackController.setCurrentVideoId(null);
|
||||||
} else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) {
|
} else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) {
|
||||||
SponsorBlockViewController.hideNewSegmentLayout();
|
SponsorBlockViewController.hideNewSegmentLayout();
|
||||||
}
|
}
|
||||||
// Voting and add new segment buttons automatically show/hide themselves.
|
|
||||||
|
|
||||||
SponsorBlockViewController.updateLayout();
|
SponsorBlockViewController.updateLayout();
|
||||||
|
|
||||||
sbEnabled.setChecked(enabled);
|
// Preferences are synced by AbstractPreferenceFragment since keys are set
|
||||||
|
// and a Setting exist with the same key.
|
||||||
addNewSegment.setChecked(Settings.SB_CREATE_NEW_SEGMENT.get());
|
|
||||||
addNewSegment.setEnabled(enabled);
|
|
||||||
|
|
||||||
votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get());
|
|
||||||
votingEnabled.setEnabled(enabled);
|
|
||||||
|
|
||||||
autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get());
|
|
||||||
autoHideSkipSegmentButton.setEnabled(enabled);
|
|
||||||
|
|
||||||
autoHideSkipSegmentButtonDuration.setValue(Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.get().toString());
|
|
||||||
autoHideSkipSegmentButtonDuration.setEnabled(Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.isAvailable());
|
|
||||||
|
|
||||||
compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get());
|
|
||||||
compactSkipButton.setEnabled(enabled);
|
|
||||||
|
|
||||||
showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
|
|
||||||
showSkipToast.setEnabled(enabled);
|
|
||||||
|
|
||||||
squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get());
|
|
||||||
squareLayout.setEnabled(enabled);
|
|
||||||
|
|
||||||
showSkipToastDuration.setValue(Settings.SB_TOAST_ON_SKIP_DURATION.get().toString());
|
|
||||||
showSkipToastDuration.setEnabled(Settings.SB_TOAST_ON_SKIP_DURATION.isAvailable());
|
|
||||||
|
|
||||||
toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get());
|
|
||||||
toastOnConnectionError.setEnabled(enabled);
|
|
||||||
|
|
||||||
trackSkips.setChecked(Settings.SB_TRACK_SKIP_COUNT.get());
|
|
||||||
trackSkips.setEnabled(enabled);
|
|
||||||
|
|
||||||
showTimeWithoutSegments.setChecked(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get());
|
|
||||||
showTimeWithoutSegments.setEnabled(enabled);
|
|
||||||
|
|
||||||
newSegmentStep.setText((Settings.SB_CREATE_NEW_SEGMENT_STEP.get()).toString());
|
|
||||||
newSegmentStep.setEnabled(enabled);
|
|
||||||
|
|
||||||
minSegmentDuration.setText((Settings.SB_SEGMENT_MIN_DURATION.get()).toString());
|
|
||||||
minSegmentDuration.setEnabled(enabled);
|
|
||||||
|
|
||||||
privateUserId.setText(Settings.SB_PRIVATE_USER_ID.get());
|
|
||||||
privateUserId.setEnabled(enabled);
|
|
||||||
|
|
||||||
// If the user has a private user id, then include a subtext that mentions not to share it.
|
// If the user has a private user id, then include a subtext that mentions not to share it.
|
||||||
String importExportSummary = SponsorBlockSettings.userHasSBPrivateId()
|
String importExportSummary = SponsorBlockSettings.userHasSBPrivateId()
|
||||||
@@ -160,11 +102,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
: str("revanced_sb_settings_ie_sum");
|
: str("revanced_sb_settings_ie_sum");
|
||||||
importExport.setSummary(importExportSummary);
|
importExport.setSummary(importExportSummary);
|
||||||
|
|
||||||
apiUrl.setEnabled(enabled);
|
for (SegmentCategoryPreference category : segmentCategories) {
|
||||||
importExport.setEnabled(enabled);
|
|
||||||
segmentCategory.setEnabled(enabled);
|
|
||||||
|
|
||||||
for (SegmentCategoryListPreference category : segmentCategories) {
|
|
||||||
category.updateUI();
|
category.updateUI();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -172,6 +110,50 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void updateUIDelayed() {
|
||||||
|
// Must use a delay, so AbstractPreferenceFragment can
|
||||||
|
// update the availability of the settings.
|
||||||
|
Utils.runOnMainThreadDelayed(this::updateUI, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializePreference(Preference preference, Setting<?> setting, String key) {
|
||||||
|
initializePreference(preference, setting, key, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializePreference(Preference preference, Setting<?> setting,
|
||||||
|
String key, boolean setDetailedSummary) {
|
||||||
|
preference.setKey(setting.key);
|
||||||
|
preference.setTitle(str(key));
|
||||||
|
preference.setEnabled(setting.isAvailable());
|
||||||
|
boolean shouldSetSummary = true;
|
||||||
|
|
||||||
|
if (preference instanceof SwitchPreference switchPref && setting instanceof BooleanSetting boolSetting) {
|
||||||
|
switchPref.setChecked(boolSetting.get());
|
||||||
|
if (setDetailedSummary) {
|
||||||
|
switchPref.setSummaryOn(str(key + "_sum_on"));
|
||||||
|
switchPref.setSummaryOff(str(key + "_sum_off"));
|
||||||
|
shouldSetSummary = false;
|
||||||
|
}
|
||||||
|
} else if (preference instanceof ResettableEditTextPreference resetPref) {
|
||||||
|
resetPref.setText(setting.get().toString());
|
||||||
|
} else if (preference instanceof EditTextPreference editPref) {
|
||||||
|
editPref.setText(setting.get().toString());
|
||||||
|
} else if (preference instanceof ListPreference listPref) {
|
||||||
|
listPref.setEntries(Utils.getResourceStringArray(key + "_entries"));
|
||||||
|
listPref.setEntryValues(Utils.getResourceStringArray(key + "_entry_values"));
|
||||||
|
listPref.setValue(setting.get().toString());
|
||||||
|
|
||||||
|
if (preference instanceof CustomDialogListPreference dialogPref) {
|
||||||
|
// Sets a static summary without overwriting it.
|
||||||
|
dialogPref.setStaticSummary(str(key + "_sum"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldSetSummary) {
|
||||||
|
preference.setSummary(str(key + "_sum"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void onAttachedToActivity() {
|
protected void onAttachedToActivity() {
|
||||||
try {
|
try {
|
||||||
super.onAttachedToActivity();
|
super.onAttachedToActivity();
|
||||||
@@ -183,20 +165,19 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
preferencesInitialized = true;
|
preferencesInitialized = true;
|
||||||
|
|
||||||
Logger.printDebug(() -> "Creating settings preferences");
|
Logger.printDebug(() -> "Creating settings preferences");
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
SponsorBlockSettings.initialize();
|
SponsorBlockSettings.initialize();
|
||||||
|
|
||||||
sbEnabled = new SwitchPreference(context);
|
SwitchPreference sbEnabled = new SwitchPreference(context);
|
||||||
sbEnabled.setTitle(str("revanced_sb_enable_sb"));
|
initializePreference(sbEnabled, Settings.SB_ENABLED,
|
||||||
sbEnabled.setSummary(str("revanced_sb_enable_sb_sum"));
|
"revanced_sb_enable_sb", false);
|
||||||
addPreference(sbEnabled);
|
addPreference(sbEnabled);
|
||||||
sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_ENABLED.save((Boolean) newValue);
|
Settings.SB_ENABLED.save((Boolean) newValue);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,109 +185,98 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
appearanceCategory.setTitle(str("revanced_sb_appearance_category"));
|
appearanceCategory.setTitle(str("revanced_sb_appearance_category"));
|
||||||
addPreference(appearanceCategory);
|
addPreference(appearanceCategory);
|
||||||
|
|
||||||
votingEnabled = new SwitchPreference(context);
|
SwitchPreference votingEnabled = new SwitchPreference(context);
|
||||||
votingEnabled.setTitle(str("revanced_sb_enable_voting"));
|
initializePreference(votingEnabled, Settings.SB_VOTING_BUTTON,
|
||||||
votingEnabled.setSummaryOn(str("revanced_sb_enable_voting_sum_on"));
|
"revanced_sb_enable_voting");
|
||||||
votingEnabled.setSummaryOff(str("revanced_sb_enable_voting_sum_off"));
|
|
||||||
votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_VOTING_BUTTON.save((Boolean) newValue);
|
Settings.SB_VOTING_BUTTON.save((Boolean) newValue);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
appearanceCategory.addPreference(votingEnabled);
|
appearanceCategory.addPreference(votingEnabled);
|
||||||
|
|
||||||
compactSkipButton = new SwitchPreference(context);
|
SwitchPreference compactSkipButton = new SwitchPreference(context);
|
||||||
compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button"));
|
initializePreference(compactSkipButton, Settings.SB_COMPACT_SKIP_BUTTON,
|
||||||
compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on"));
|
"revanced_sb_enable_compact_skip_button");
|
||||||
compactSkipButton.setSummaryOff(str("revanced_sb_enable_compact_skip_button_sum_off"));
|
|
||||||
compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue);
|
Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
appearanceCategory.addPreference(compactSkipButton);
|
appearanceCategory.addPreference(compactSkipButton);
|
||||||
|
|
||||||
autoHideSkipSegmentButton = new SwitchPreference(context);
|
SwitchPreference autoHideSkipSegmentButton = new SwitchPreference(context);
|
||||||
autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
|
initializePreference(autoHideSkipSegmentButton, Settings.SB_AUTO_HIDE_SKIP_BUTTON,
|
||||||
autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
|
"revanced_sb_enable_auto_hide_skip_segment_button");
|
||||||
autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
|
|
||||||
autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
|
Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
appearanceCategory.addPreference(autoHideSkipSegmentButton);
|
appearanceCategory.addPreference(autoHideSkipSegmentButton);
|
||||||
|
|
||||||
String[] durationEntries = Utils.getResourceStringArray("revanced_sb_duration_entries");
|
CustomDialogListPreference autoHideSkipSegmentButtonDuration = new CustomDialogListPreference(context);
|
||||||
String[] durationEntryValues = Utils.getResourceStringArray("revanced_sb_duration_entry_values");
|
initializePreference(autoHideSkipSegmentButtonDuration, Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION,
|
||||||
|
"revanced_sb_auto_hide_skip_button_duration");
|
||||||
autoHideSkipSegmentButtonDuration = new CustomDialogListPreference(context);
|
|
||||||
autoHideSkipSegmentButtonDuration.setTitle(str("revanced_sb_auto_hide_skip_button_duration"));
|
|
||||||
autoHideSkipSegmentButtonDuration.setSummary(str("revanced_sb_auto_hide_skip_button_duration_sum"));
|
|
||||||
autoHideSkipSegmentButtonDuration.setEntries(durationEntries);
|
|
||||||
autoHideSkipSegmentButtonDuration.setEntryValues(durationEntryValues);
|
|
||||||
autoHideSkipSegmentButtonDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
autoHideSkipSegmentButtonDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.save(
|
SponsorBlockDuration newDuration = SponsorBlockDuration.valueOf((String) newValue);
|
||||||
SponsorBlockDuration.valueOf((String) newValue)
|
Settings.SB_AUTO_HIDE_SKIP_BUTTON_DURATION.save(newDuration);
|
||||||
);
|
((CustomDialogListPreference) preference1).setValue(newDuration.name());
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
appearanceCategory.addPreference(autoHideSkipSegmentButtonDuration);
|
appearanceCategory.addPreference(autoHideSkipSegmentButtonDuration);
|
||||||
|
|
||||||
showSkipToast = new SwitchPreference(context);
|
SwitchPreference showSkipToast = new SwitchPreference(context);
|
||||||
showSkipToast.setTitle(str("revanced_sb_general_skiptoast"));
|
initializePreference(showSkipToast, Settings.SB_TOAST_ON_SKIP,
|
||||||
showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on"));
|
"revanced_sb_general_skiptoast");
|
||||||
showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off"));
|
|
||||||
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
|
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
|
Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
appearanceCategory.addPreference(showSkipToast);
|
appearanceCategory.addPreference(showSkipToast);
|
||||||
|
|
||||||
showSkipToastDuration = new CustomDialogListPreference(context);
|
CustomDialogListPreference showSkipToastDuration = new CustomDialogListPreference(context);
|
||||||
showSkipToastDuration.setTitle(str("revanced_sb_toast_on_skip_duration"));
|
initializePreference(showSkipToastDuration, Settings.SB_TOAST_ON_SKIP_DURATION,
|
||||||
showSkipToastDuration.setSummary(str("revanced_sb_toast_on_skip_duration_sum"));
|
"revanced_sb_toast_on_skip_duration");
|
||||||
showSkipToastDuration.setEntries(durationEntries);
|
// Sets a static summary without overwriting it.
|
||||||
showSkipToastDuration.setEntryValues(durationEntryValues);
|
showSkipToastDuration.setStaticSummary(str("revanced_sb_toast_on_skip_duration_sum"));
|
||||||
showSkipToastDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
showSkipToastDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_TOAST_ON_SKIP_DURATION.save(
|
SponsorBlockDuration newDuration = SponsorBlockDuration.valueOf((String) newValue);
|
||||||
SponsorBlockDuration.valueOf((String) newValue)
|
Settings.SB_TOAST_ON_SKIP_DURATION.save(newDuration);
|
||||||
);
|
((CustomDialogListPreference) preference1).setValue(newDuration.name());
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
appearanceCategory.addPreference(showSkipToastDuration);
|
appearanceCategory.addPreference(showSkipToastDuration);
|
||||||
|
|
||||||
showTimeWithoutSegments = new SwitchPreference(context);
|
SwitchPreference showTimeWithoutSegments = new SwitchPreference(context);
|
||||||
showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without"));
|
initializePreference(showTimeWithoutSegments, Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS,
|
||||||
showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on"));
|
"revanced_sb_general_time_without");
|
||||||
showTimeWithoutSegments.setSummaryOff(str("revanced_sb_general_time_without_sum_off"));
|
|
||||||
showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> {
|
showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue);
|
Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
appearanceCategory.addPreference(showTimeWithoutSegments);
|
appearanceCategory.addPreference(showTimeWithoutSegments);
|
||||||
|
|
||||||
squareLayout = new SwitchPreference(context);
|
SwitchPreference squareLayout = new SwitchPreference(context);
|
||||||
squareLayout.setTitle(str("revanced_sb_square_layout"));
|
initializePreference(squareLayout, Settings.SB_SQUARE_LAYOUT,
|
||||||
squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
|
"revanced_sb_square_layout");
|
||||||
squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
|
|
||||||
squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
|
squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
|
Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
appearanceCategory.addPreference(squareLayout);
|
appearanceCategory.addPreference(squareLayout);
|
||||||
|
|
||||||
segmentCategory = new PreferenceCategory(context);
|
PreferenceCategory segmentCategory = new PreferenceCategory(context);
|
||||||
segmentCategory.setTitle(str("revanced_sb_diff_segments"));
|
segmentCategory.setTitle(str("revanced_sb_diff_segments"));
|
||||||
addPreference(segmentCategory);
|
addPreference(segmentCategory);
|
||||||
|
|
||||||
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
||||||
SegmentCategoryListPreference categoryPreference = new SegmentCategoryListPreference(context, category);
|
SegmentCategoryPreference categoryPreference = new SegmentCategoryPreference(context, category);
|
||||||
segmentCategories.add(categoryPreference);
|
segmentCategories.add(categoryPreference);
|
||||||
segmentCategory.addPreference(categoryPreference);
|
segmentCategory.addPreference(categoryPreference);
|
||||||
}
|
}
|
||||||
@@ -315,24 +285,23 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
createSegmentCategory.setTitle(str("revanced_sb_create_segment_category"));
|
createSegmentCategory.setTitle(str("revanced_sb_create_segment_category"));
|
||||||
addPreference(createSegmentCategory);
|
addPreference(createSegmentCategory);
|
||||||
|
|
||||||
addNewSegment = new SwitchPreference(context);
|
SwitchPreference addNewSegment = new SwitchPreference(context);
|
||||||
addNewSegment.setTitle(str("revanced_sb_enable_create_segment"));
|
initializePreference(addNewSegment, Settings.SB_CREATE_NEW_SEGMENT,
|
||||||
addNewSegment.setSummaryOn(str("revanced_sb_enable_create_segment_sum_on"));
|
"revanced_sb_enable_create_segment");
|
||||||
addNewSegment.setSummaryOff(str("revanced_sb_enable_create_segment_sum_off"));
|
|
||||||
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
|
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
|
||||||
Boolean newValue = (Boolean) o;
|
Boolean newValue = (Boolean) o;
|
||||||
if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
|
if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
preference1.getContext(),
|
preference1.getContext(),
|
||||||
str("revanced_sb_guidelines_popup_title"), // Title.
|
str("revanced_sb_guidelines_popup_title"), // Title.
|
||||||
str("revanced_sb_guidelines_popup_content"), // Message.
|
str("revanced_sb_guidelines_popup_content"), // Message.
|
||||||
null, // No EditText.
|
null, // No EditText.
|
||||||
str("revanced_sb_guidelines_popup_open"), // OK button text.
|
str("revanced_sb_guidelines_popup_open"), // OK button text.
|
||||||
() -> openGuidelines(), // OK button action.
|
this::openGuidelines, // OK button action.
|
||||||
null, // Cancel button action.
|
null, // Cancel button action.
|
||||||
str("revanced_sb_guidelines_popup_already_read"), // Neutral button text.
|
str("revanced_sb_guidelines_popup_already_read"), // Neutral button text.
|
||||||
() -> {}, // Neutral button action (dismiss only).
|
() -> {}, // Neutral button action (dismiss only).
|
||||||
true // Dismiss dialog when onNeutralClick.
|
true // Dismiss dialog when onNeutralClick.
|
||||||
);
|
);
|
||||||
|
|
||||||
// Set dialog as non-cancelable.
|
// Set dialog as non-cancelable.
|
||||||
@@ -344,21 +313,21 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
dialogPair.first.show();
|
dialogPair.first.show();
|
||||||
}
|
}
|
||||||
Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
|
Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
createSegmentCategory.addPreference(addNewSegment);
|
createSegmentCategory.addPreference(addNewSegment);
|
||||||
|
|
||||||
newSegmentStep = new ResettableEditTextPreference(context);
|
ResettableEditTextPreference newSegmentStep = new ResettableEditTextPreference(context);
|
||||||
newSegmentStep.setSetting(Settings.SB_CREATE_NEW_SEGMENT_STEP);
|
initializePreference(newSegmentStep, Settings.SB_CREATE_NEW_SEGMENT_STEP,
|
||||||
newSegmentStep.setTitle(str("revanced_sb_general_adjusting"));
|
"revanced_sb_general_adjusting");
|
||||||
newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum"));
|
|
||||||
newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
|
newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||||
newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> {
|
newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
try {
|
try {
|
||||||
final int newAdjustmentValue = Integer.parseInt(newValue.toString());
|
final int newAdjustmentValue = Integer.parseInt(newValue.toString());
|
||||||
if (newAdjustmentValue != 0) {
|
if (newAdjustmentValue != 0) {
|
||||||
Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue);
|
Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue);
|
||||||
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (NumberFormatException ex) {
|
} catch (NumberFormatException ex) {
|
||||||
@@ -366,7 +335,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Utils.showToastLong(str("revanced_sb_general_adjusting_invalid"));
|
Utils.showToastLong(str("revanced_sb_general_adjusting_invalid"));
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
createSegmentCategory.addPreference(newSegmentStep);
|
createSegmentCategory.addPreference(newSegmentStep);
|
||||||
@@ -384,49 +353,47 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
generalCategory.setTitle(str("revanced_sb_general"));
|
generalCategory.setTitle(str("revanced_sb_general"));
|
||||||
addPreference(generalCategory);
|
addPreference(generalCategory);
|
||||||
|
|
||||||
toastOnConnectionError = new SwitchPreference(context);
|
SwitchPreference toastOnConnectionError = new SwitchPreference(context);
|
||||||
toastOnConnectionError.setTitle(str("revanced_sb_toast_on_connection_error_title"));
|
initializePreference(toastOnConnectionError, Settings.SB_TOAST_ON_CONNECTION_ERROR,
|
||||||
toastOnConnectionError.setSummaryOn(str("revanced_sb_toast_on_connection_error_summary_on"));
|
"revanced_sb_toast_on_connection_error");
|
||||||
toastOnConnectionError.setSummaryOff(str("revanced_sb_toast_on_connection_error_summary_off"));
|
|
||||||
toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> {
|
toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
|
Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
generalCategory.addPreference(toastOnConnectionError);
|
generalCategory.addPreference(toastOnConnectionError);
|
||||||
|
|
||||||
trackSkips = new SwitchPreference(context);
|
SwitchPreference trackSkips = new SwitchPreference(context);
|
||||||
trackSkips.setTitle(str("revanced_sb_general_skipcount"));
|
initializePreference(trackSkips, Settings.SB_TRACK_SKIP_COUNT,
|
||||||
trackSkips.setSummaryOn(str("revanced_sb_general_skipcount_sum_on"));
|
"revanced_sb_general_skipcount");
|
||||||
trackSkips.setSummaryOff(str("revanced_sb_general_skipcount_sum_off"));
|
|
||||||
trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> {
|
trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue);
|
Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
generalCategory.addPreference(trackSkips);
|
generalCategory.addPreference(trackSkips);
|
||||||
|
|
||||||
minSegmentDuration = new ResettableEditTextPreference(context);
|
ResettableEditTextPreference minSegmentDuration = new ResettableEditTextPreference(context);
|
||||||
minSegmentDuration.setSetting(Settings.SB_SEGMENT_MIN_DURATION);
|
initializePreference(minSegmentDuration, Settings.SB_SEGMENT_MIN_DURATION,
|
||||||
minSegmentDuration.setTitle(str("revanced_sb_general_min_duration"));
|
"revanced_sb_general_min_duration");
|
||||||
minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum"));
|
|
||||||
minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
||||||
minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
try {
|
try {
|
||||||
Float minTimeDuration = Float.valueOf(newValue.toString());
|
Float minTimeDuration = Float.valueOf(newValue.toString());
|
||||||
Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration);
|
Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration);
|
||||||
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
} catch (NumberFormatException ex) {
|
} catch (NumberFormatException ex) {
|
||||||
Logger.printInfo(() -> "Invalid minimum segment duration", ex);
|
Logger.printInfo(() -> "Invalid minimum segment duration", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
Utils.showToastLong(str("revanced_sb_general_min_duration_invalid"));
|
Utils.showToastLong(str("revanced_sb_general_min_duration_invalid"));
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
generalCategory.addPreference(minSegmentDuration);
|
generalCategory.addPreference(minSegmentDuration);
|
||||||
|
|
||||||
privateUserId = new EditTextPreference(context) {
|
EditTextPreference privateUserId = new EditTextPreference(context) {
|
||||||
@Override
|
@Override
|
||||||
protected void showDialog(Bundle state) {
|
protected void showDialog(Bundle state) {
|
||||||
try {
|
try {
|
||||||
@@ -439,7 +406,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
editText.setSelection(initialValue.length()); // Move cursor to end.
|
editText.setSelection(initialValue.length()); // Move cursor to end.
|
||||||
|
|
||||||
// Create custom dialog.
|
// Create custom dialog.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
getTitle() != null ? getTitle().toString() : "", // Title.
|
getTitle() != null ? getTitle().toString() : "", // Title.
|
||||||
null, // Message is replaced by EditText.
|
null, // Message is replaced by EditText.
|
||||||
@@ -475,31 +442,32 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
privateUserId.setTitle(str("revanced_sb_general_uuid"));
|
initializePreference(privateUserId, Settings.SB_PRIVATE_USER_ID,
|
||||||
privateUserId.setSummary(str("revanced_sb_general_uuid_sum"));
|
"revanced_sb_general_uuid");
|
||||||
privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> {
|
privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
String newUUID = newValue.toString();
|
String newUUID = newValue.toString();
|
||||||
if (!SponsorBlockSettings.isValidSBUserId(newUUID)) {
|
if (!SponsorBlockSettings.isValidSBUserId(newUUID)) {
|
||||||
Utils.showToastLong(str("revanced_sb_general_uuid_invalid"));
|
Utils.showToastLong(str("revanced_sb_general_uuid_invalid"));
|
||||||
|
updateUIDelayed();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Settings.SB_PRIVATE_USER_ID.save(newUUID);
|
Settings.SB_PRIVATE_USER_ID.save(newUUID);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
generalCategory.addPreference(privateUserId);
|
generalCategory.addPreference(privateUserId);
|
||||||
|
|
||||||
apiUrl = new Preference(context);
|
Preference apiUrl = new Preference(context);
|
||||||
apiUrl.setTitle(str("revanced_sb_general_api_url"));
|
initializePreference(apiUrl, Settings.SB_API_URL,
|
||||||
apiUrl.setSummary(Html.fromHtml(str("revanced_sb_general_api_url_sum")));
|
"revanced_sb_general_api_url");
|
||||||
apiUrl.setOnPreferenceClickListener(preference1 -> {
|
apiUrl.setOnPreferenceClickListener(preference1 -> {
|
||||||
EditText editText = new EditText(context);
|
EditText editText = new EditText(context);
|
||||||
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||||
editText.setText(Settings.SB_API_URL.get());
|
editText.setText(Settings.SB_API_URL.get());
|
||||||
|
|
||||||
// Create a custom dialog.
|
// Create a custom dialog.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
str("revanced_sb_general_api_url"), // Title.
|
str("revanced_sb_general_api_url"), // Title.
|
||||||
null, // No message, EditText replaces it.
|
null, // No message, EditText replaces it.
|
||||||
@@ -538,8 +506,11 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
EditText editText = getEditText();
|
EditText editText = getEditText();
|
||||||
|
|
||||||
|
editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||||
|
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap.
|
||||||
|
|
||||||
// Create a custom dialog.
|
// Create a custom dialog.
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
context,
|
context,
|
||||||
str("revanced_sb_settings_ie"), // Title.
|
str("revanced_sb_settings_ie"), // Title.
|
||||||
null, // No message, EditText replaces it.
|
null, // No message, EditText replaces it.
|
||||||
@@ -588,7 +559,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
|||||||
});
|
});
|
||||||
importExport.setOnPreferenceChangeListener((preference1, newValue) -> {
|
importExport.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
SponsorBlockSettings.importDesktopSettings((String) newValue);
|
SponsorBlockSettings.importDesktopSettings((String) newValue);
|
||||||
updateUI();
|
updateUIDelayed();
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
generalCategory.addPreference(importExport);
|
generalCategory.addPreference(importExport);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.extension.youtube.sponsorblock.ui;
|
package app.revanced.extension.youtube.sponsorblock.ui;
|
||||||
|
|
||||||
|
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||||
import static android.text.Html.fromHtml;
|
import static android.text.Html.fromHtml;
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
@@ -7,7 +8,6 @@ import android.app.Dialog;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.preference.EditTextPreference;
|
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceCategory;
|
import android.preference.PreferenceCategory;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
@@ -19,6 +19,7 @@ import androidx.annotation.Nullable;
|
|||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
||||||
|
import app.revanced.extension.shared.ui.CustomDialog;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
|
||||||
@@ -27,7 +28,6 @@ import app.revanced.extension.youtube.sponsorblock.requests.SBRequester;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* User skip stats.
|
* User skip stats.
|
||||||
*
|
|
||||||
* None of the preferences here show up in search results because
|
* None of the preferences here show up in search results because
|
||||||
* a category cannot be added to another category for the search results.
|
* a category cannot be added to another category for the search results.
|
||||||
* Additionally the stats must load remotely on a background thread which means the
|
* Additionally the stats must load remotely on a background thread which means the
|
||||||
@@ -48,6 +48,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
|||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
protected void onAttachedToActivity() {
|
protected void onAttachedToActivity() {
|
||||||
try {
|
try {
|
||||||
super.onAttachedToActivity();
|
super.onAttachedToActivity();
|
||||||
@@ -97,8 +98,8 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
|||||||
if (stats.totalSegmentCountIncludingIgnored > 0) {
|
if (stats.totalSegmentCountIncludingIgnored > 0) {
|
||||||
// If user has not created any segments, there's no reason to set a username.
|
// If user has not created any segments, there's no reason to set a username.
|
||||||
String userName = stats.userName;
|
String userName = stats.userName;
|
||||||
EditTextPreference preference = new ResettableEditTextPreference(context);
|
ResettableEditTextPreference preference = new ResettableEditTextPreference(context);
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName)));
|
preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName), FROM_HTML_MODE_COMPACT));
|
||||||
preference.setSummary(str("revanced_sb_stats_username_change"));
|
preference.setSummary(str("revanced_sb_stats_username_change"));
|
||||||
preference.setText(userName);
|
preference.setText(userName);
|
||||||
preference.setOnPreferenceChangeListener((preference1, value) -> {
|
preference.setOnPreferenceChangeListener((preference1, value) -> {
|
||||||
@@ -107,7 +108,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
|||||||
String errorMessage = SBRequester.setUsername(newUserName);
|
String errorMessage = SBRequester.setUsername(newUserName);
|
||||||
Utils.runOnMainThread(() -> {
|
Utils.runOnMainThread(() -> {
|
||||||
if (errorMessage == null) {
|
if (errorMessage == null) {
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName)));
|
preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName), FROM_HTML_MODE_COMPACT));
|
||||||
preference.setText(newUserName);
|
preference.setText(newUserName);
|
||||||
Utils.showToastLong(str("revanced_sb_stats_username_changed"));
|
Utils.showToastLong(str("revanced_sb_stats_username_changed"));
|
||||||
} else {
|
} else {
|
||||||
@@ -118,6 +119,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
|||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
preference.setEnabled(Settings.SB_PRIVATE_USER_ID.isAvailable()); // Sync with private user ID setting.
|
||||||
addPreference(preference);
|
addPreference(preference);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,7 +127,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
|||||||
// Number of segment submissions (does not include ignored segments).
|
// Number of segment submissions (does not include ignored segments).
|
||||||
Preference preference = new Preference(context);
|
Preference preference = new Preference(context);
|
||||||
String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount);
|
String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount);
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted)));
|
preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted), FROM_HTML_MODE_COMPACT));
|
||||||
preference.setSummary(str("revanced_sb_stats_submissions_sum"));
|
preference.setSummary(str("revanced_sb_stats_submissions_sum"));
|
||||||
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
||||||
preference.setSelectable(false);
|
preference.setSelectable(false);
|
||||||
@@ -137,6 +139,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
preference.setEnabled(Settings.SB_ENABLED.isAvailable());
|
||||||
addPreference(preference);
|
addPreference(preference);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,8 +147,9 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
|||||||
// "user reputation". Usually not useful since it appears most users have zero reputation.
|
// "user reputation". Usually not useful since it appears most users have zero reputation.
|
||||||
// But if there is a reputation then show it here.
|
// But if there is a reputation then show it here.
|
||||||
Preference preference = new Preference(context);
|
Preference preference = new Preference(context);
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation)));
|
preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation), FROM_HTML_MODE_COMPACT));
|
||||||
preference.setSelectable(false);
|
preference.setSelectable(false);
|
||||||
|
preference.setEnabled(Settings.SB_ENABLED.isAvailable());
|
||||||
if (stats.reputation != 0) {
|
if (stats.reputation != 0) {
|
||||||
addPreference(preference);
|
addPreference(preference);
|
||||||
}
|
}
|
||||||
@@ -166,14 +170,15 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
|||||||
stats_saved_sum = str("revanced_sb_stats_saved_sum",
|
stats_saved_sum = str("revanced_sb_stats_saved_sum",
|
||||||
SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved)));
|
SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved)));
|
||||||
}
|
}
|
||||||
preference.setTitle(fromHtml(stats_saved));
|
preference.setTitle(fromHtml(stats_saved, FROM_HTML_MODE_COMPACT));
|
||||||
preference.setSummary(fromHtml(stats_saved_sum));
|
preference.setSummary(fromHtml(stats_saved_sum, FROM_HTML_MODE_COMPACT));
|
||||||
preference.setOnPreferenceClickListener(preference1 -> {
|
preference.setOnPreferenceClickListener(preference1 -> {
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
|
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
|
||||||
preference1.getContext().startActivity(i);
|
preference1.getContext().startActivity(i);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
preference.setEnabled(Settings.SB_ENABLED.isAvailable());
|
||||||
addPreference(preference);
|
addPreference(preference);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -187,16 +192,16 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
|||||||
Runnable updateStatsSelfSaved = () -> {
|
Runnable updateStatsSelfSaved = () -> {
|
||||||
String formatted = SponsorBlockUtils.getNumberOfSkipsString(
|
String formatted = SponsorBlockUtils.getNumberOfSkipsString(
|
||||||
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get());
|
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get());
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted)));
|
preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted), FROM_HTML_MODE_COMPACT));
|
||||||
|
|
||||||
String formattedSaved = SponsorBlockUtils.getTimeSavedString(
|
String formattedSaved = SponsorBlockUtils.getTimeSavedString(
|
||||||
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / 1000);
|
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / 1000);
|
||||||
preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved)));
|
preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved), FROM_HTML_MODE_COMPACT));
|
||||||
};
|
};
|
||||||
updateStatsSelfSaved.run();
|
updateStatsSelfSaved.run();
|
||||||
|
|
||||||
preference.setOnPreferenceClickListener(preference1 -> {
|
preference.setOnPreferenceClickListener(preference1 -> {
|
||||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
Pair<Dialog, LinearLayout> dialogPair = CustomDialog.create(
|
||||||
preference.getContext(),
|
preference.getContext(),
|
||||||
str("revanced_sb_stats_self_saved_reset_title"), // Title.
|
str("revanced_sb_stats_self_saved_reset_title"), // Title.
|
||||||
null, // No message.
|
null, // No message.
|
||||||
@@ -219,6 +224,7 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
preference.setEnabled(Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.isAvailable());
|
||||||
addPreference(preference);
|
addPreference(preference);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package app.revanced.extension.youtube.sponsorblock.ui;
|
package app.revanced.extension.youtube.sponsorblock.ui;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
import static app.revanced.extension.shared.Utils.getResourceIdentifierOrThrow;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -63,7 +63,8 @@ public class SponsorBlockViewController {
|
|||||||
Context context = Utils.getContext();
|
Context context = Utils.getContext();
|
||||||
RelativeLayout layout = new RelativeLayout(context);
|
RelativeLayout layout = new RelativeLayout(context);
|
||||||
layout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,RelativeLayout.LayoutParams.MATCH_PARENT));
|
layout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,RelativeLayout.LayoutParams.MATCH_PARENT));
|
||||||
LayoutInflater.from(context).inflate(getResourceIdentifier("revanced_sb_inline_sponsor_overlay", "layout"), layout);
|
LayoutInflater.from(context).inflate(getResourceIdentifierOrThrow(
|
||||||
|
"revanced_sb_inline_sponsor_overlay", "layout"), layout);
|
||||||
inlineSponsorOverlayRef = new WeakReference<>(layout);
|
inlineSponsorOverlayRef = new WeakReference<>(layout);
|
||||||
|
|
||||||
viewGroup.addView(layout);
|
viewGroup.addView(layout);
|
||||||
@@ -82,14 +83,14 @@ public class SponsorBlockViewController {
|
|||||||
});
|
});
|
||||||
youtubeOverlaysLayoutRef = new WeakReference<>(viewGroup);
|
youtubeOverlaysLayoutRef = new WeakReference<>(viewGroup);
|
||||||
|
|
||||||
skipHighlightButtonRef = new WeakReference<>(Objects.requireNonNull(
|
skipHighlightButtonRef = new WeakReference<>(layout.findViewById(getResourceIdentifierOrThrow(
|
||||||
layout.findViewById(getResourceIdentifier("revanced_sb_skip_highlight_button", "id"))));
|
"revanced_sb_skip_highlight_button", "id")));
|
||||||
|
|
||||||
skipSponsorButtonRef = new WeakReference<>(Objects.requireNonNull(
|
skipSponsorButtonRef = new WeakReference<>(layout.findViewById(getResourceIdentifierOrThrow(
|
||||||
layout.findViewById(getResourceIdentifier("revanced_sb_skip_sponsor_button", "id"))));
|
"revanced_sb_skip_sponsor_button", "id")));
|
||||||
|
|
||||||
NewSegmentLayout newSegmentLayout = Objects.requireNonNull(
|
NewSegmentLayout newSegmentLayout = layout.findViewById(getResourceIdentifierOrThrow(
|
||||||
layout.findViewById(getResourceIdentifier("revanced_sb_new_segment_view", "id")));
|
"revanced_sb_new_segment_view", "id"));
|
||||||
newSegmentLayoutRef = new WeakReference<>(newSegmentLayout);
|
newSegmentLayoutRef = new WeakReference<>(newSegmentLayout);
|
||||||
newSegmentLayout.updateLayout();
|
newSegmentLayout.updateLayout();
|
||||||
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class SwipeControlsConfigurationProvider {
|
|||||||
val overlayBackgroundOpacity: Int by lazy {
|
val overlayBackgroundOpacity: Int by lazy {
|
||||||
var opacity = Settings.SWIPE_OVERLAY_OPACITY.get()
|
var opacity = Settings.SWIPE_OVERLAY_OPACITY.get()
|
||||||
|
|
||||||
if (opacity < 0 || opacity > 100) {
|
if (opacity !in 0..100) {
|
||||||
Utils.showToastLong(str("revanced_swipe_overlay_background_opacity_invalid_toast"))
|
Utils.showToastLong(str("revanced_swipe_overlay_background_opacity_invalid_toast"))
|
||||||
opacity = Settings.SWIPE_OVERLAY_OPACITY.resetToDefault()
|
opacity = Settings.SWIPE_OVERLAY_OPACITY.resetToDefault()
|
||||||
}
|
}
|
||||||
@@ -115,14 +115,13 @@ class SwipeControlsConfigurationProvider {
|
|||||||
* Resets to default and shows a toast if the color string is invalid or empty.
|
* Resets to default and shows a toast if the color string is invalid or empty.
|
||||||
*/
|
*/
|
||||||
val overlayVolumeProgressColor: Int by lazy {
|
val overlayVolumeProgressColor: Int by lazy {
|
||||||
|
// Use lazy to avoid repeat parsing. Changing color requires app restart.
|
||||||
getSettingColor(Settings.SWIPE_OVERLAY_VOLUME_COLOR)
|
getSettingColor(Settings.SWIPE_OVERLAY_VOLUME_COLOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSettingColor(setting: StringSetting): Int {
|
private fun getSettingColor(setting: StringSetting): Int {
|
||||||
try {
|
return try {
|
||||||
//noinspection UseKtx
|
Color.parseColor(setting.get())
|
||||||
val color = Color.parseColor(setting.get())
|
|
||||||
return (0xBF000000.toInt() or (color and 0x00FFFFFF))
|
|
||||||
} catch (ex: IllegalArgumentException) {
|
} catch (ex: IllegalArgumentException) {
|
||||||
// This code should never be reached.
|
// This code should never be reached.
|
||||||
// Color picker rejects and will not save bad colors to a setting.
|
// Color picker rejects and will not save bad colors to a setting.
|
||||||
@@ -151,7 +150,7 @@ class SwipeControlsConfigurationProvider {
|
|||||||
*/
|
*/
|
||||||
val overlayTextSize: Int by lazy {
|
val overlayTextSize: Int by lazy {
|
||||||
val size = Settings.SWIPE_OVERLAY_TEXT_SIZE.get()
|
val size = Settings.SWIPE_OVERLAY_TEXT_SIZE.get()
|
||||||
if (size < 1 || size > 30) {
|
if (size !in 1..30) {
|
||||||
Utils.showToastLong(str("revanced_swipe_text_overlay_size_invalid_toast"))
|
Utils.showToastLong(str("revanced_swipe_text_overlay_size_invalid_toast"))
|
||||||
return@lazy Settings.SWIPE_OVERLAY_TEXT_SIZE.resetToDefault()
|
return@lazy Settings.SWIPE_OVERLAY_TEXT_SIZE.resetToDefault()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class PlayerControlButton {
|
|||||||
boolean buttonEnabled();
|
boolean buttonEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int fadeInDuration = Utils.getResourceInteger("fade_duration_fast");
|
public static final int fadeInDuration = Utils.getResourceInteger("fade_duration_fast");
|
||||||
private static final int fadeOutDuration = Utils.getResourceInteger("fade_duration_scheduled");
|
private static final int fadeOutDuration = Utils.getResourceInteger("fade_duration_scheduled");
|
||||||
|
|
||||||
private final WeakReference<View> containerRef;
|
private final WeakReference<View> containerRef;
|
||||||
@@ -249,4 +249,13 @@ public class PlayerControlButton {
|
|||||||
textOverlay.setText(text);
|
textOverlay.setText(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the appropriate dialog background color depending on the current theme.
|
||||||
|
*/
|
||||||
|
public static int getDialogBackgroundColor() {
|
||||||
|
return Utils.getResourceColor(
|
||||||
|
Utils.isDarkModeEnabled() ? "yt_black1" : "yt_white1"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,38 +2,30 @@ package app.revanced.extension.youtube.videoplayer;
|
|||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
import static app.revanced.extension.shared.settings.preference.CustomDialogListPreference.*;
|
||||||
import static app.revanced.extension.youtube.patches.VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE;
|
import static app.revanced.extension.youtube.patches.VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE;
|
||||||
import static app.revanced.extension.youtube.patches.VideoInformation.VIDEO_QUALITY_PREMIUM_NAME;
|
import static app.revanced.extension.youtube.patches.VideoInformation.VIDEO_QUALITY_PREMIUM_NAME;
|
||||||
|
import static app.revanced.extension.youtube.videoplayer.PlayerControlButton.fadeInDuration;
|
||||||
|
import static app.revanced.extension.youtube.videoplayer.PlayerControlButton.getDialogBackgroundColor;
|
||||||
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.graphics.drawable.ShapeDrawable;
|
|
||||||
import android.graphics.drawable.shapes.RoundRectShape;
|
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.UnderlineSpan;
|
import android.text.style.UnderlineSpan;
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.Window;
|
import android.widget.*;
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.view.animation.Animation;
|
|
||||||
import android.view.animation.TranslateAnimation;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.ui.SheetBottomDialog;
|
||||||
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
|
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -43,6 +35,7 @@ import app.revanced.extension.youtube.patches.VideoInformation;
|
|||||||
import app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch;
|
import app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
|
import kotlin.jvm.functions.Function1;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class VideoQualityDialogButton {
|
public class VideoQualityDialogButton {
|
||||||
@@ -60,6 +53,11 @@ public class VideoQualityDialogButton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Weak reference to the currently open dialog.
|
||||||
|
*/
|
||||||
|
private static WeakReference<SheetBottomDialog.SlideDialog> currentDialog;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@@ -216,41 +214,14 @@ public class VideoQualityDialogButton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog dialog = new Dialog(context);
|
// Preset size constants.
|
||||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
final int dip8 = dipToPixels(8);
|
||||||
dialog.setCanceledOnTouchOutside(true);
|
final int dip12 = dipToPixels(12);
|
||||||
dialog.setCancelable(true);
|
final int dip16 = dipToPixels(16);
|
||||||
|
|
||||||
final int dip4 = dipToPixels(4); // Height for handle bar.
|
// Create main layout.
|
||||||
final int dip5 = dipToPixels(5); // Padding for mainLayout.
|
SheetBottomDialog.DraggableLinearLayout mainLayout =
|
||||||
final int dip6 = dipToPixels(6); // Bottom margin.
|
SheetBottomDialog.createMainLayout(context, getDialogBackgroundColor());
|
||||||
final int dip8 = dipToPixels(8); // Side padding.
|
|
||||||
final int dip16 = dipToPixels(16); // Left padding for ListView.
|
|
||||||
final int dip20 = dipToPixels(20); // Margin below handle.
|
|
||||||
final int dip40 = dipToPixels(40); // Width for handle bar.
|
|
||||||
|
|
||||||
LinearLayout mainLayout = new LinearLayout(context);
|
|
||||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
mainLayout.setPadding(dip5, dip8, dip5, dip8);
|
|
||||||
|
|
||||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Utils.createCornerRadii(12), null, null));
|
|
||||||
background.getPaint().setColor(Utils.getDialogBackgroundColor());
|
|
||||||
mainLayout.setBackground(background);
|
|
||||||
|
|
||||||
View handleBar = new View(context);
|
|
||||||
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
|
|
||||||
Utils.createCornerRadii(4), null, null));
|
|
||||||
final int baseColor = Utils.getDialogBackgroundColor();
|
|
||||||
final int adjustedHandleBarBackgroundColor = Utils.adjustColorBrightness(
|
|
||||||
baseColor, 0.9f, 1.25f);
|
|
||||||
handleBackground.getPaint().setColor(adjustedHandleBarBackgroundColor);
|
|
||||||
handleBar.setBackground(handleBackground);
|
|
||||||
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(dip40, dip4);
|
|
||||||
handleParams.gravity = Gravity.CENTER_HORIZONTAL;
|
|
||||||
handleParams.setMargins(0, 0, 0, dip20);
|
|
||||||
handleBar.setLayoutParams(handleParams);
|
|
||||||
mainLayout.addView(handleBar);
|
|
||||||
|
|
||||||
// Create SpannableStringBuilder for formatted text.
|
// Create SpannableStringBuilder for formatted text.
|
||||||
SpannableStringBuilder spannableTitle = new SpannableStringBuilder();
|
SpannableStringBuilder spannableTitle = new SpannableStringBuilder();
|
||||||
@@ -298,16 +269,20 @@ public class VideoQualityDialogButton {
|
|||||||
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
|
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
titleParams.setMargins(dip8, 0, 0, dip20);
|
titleParams.setMargins(dip12, dip16, 0, dip16);
|
||||||
titleView.setLayoutParams(titleParams);
|
titleView.setLayoutParams(titleParams);
|
||||||
mainLayout.addView(titleView);
|
mainLayout.addView(titleView);
|
||||||
|
|
||||||
|
// Create ListView for quality selection.
|
||||||
ListView listView = new ListView(context);
|
ListView listView = new ListView(context);
|
||||||
CustomQualityAdapter adapter = new CustomQualityAdapter(context, qualityLabels);
|
CustomQualityAdapter adapter = new CustomQualityAdapter(context, qualityLabels);
|
||||||
adapter.setSelectedPosition(listViewSelectedIndex);
|
adapter.setSelectedPosition(listViewSelectedIndex);
|
||||||
listView.setAdapter(adapter);
|
listView.setAdapter(adapter);
|
||||||
listView.setDivider(null);
|
listView.setDivider(null);
|
||||||
listView.setPadding(dip16, 0, 0, 0);
|
|
||||||
|
// Create dialog.
|
||||||
|
SheetBottomDialog.SlideDialog dialog = SheetBottomDialog.createSlideDialog(context, mainLayout, fadeInDuration);
|
||||||
|
currentDialog = new WeakReference<>(dialog);
|
||||||
|
|
||||||
listView.setOnItemClickListener((parent, view, which, id) -> {
|
listView.setOnItemClickListener((parent, view, which, id) -> {
|
||||||
try {
|
try {
|
||||||
@@ -322,112 +297,41 @@ public class VideoQualityDialogButton {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
|
||||||
listViewParams.setMargins(0, 0, 0, dip5);
|
|
||||||
listView.setLayoutParams(listViewParams);
|
|
||||||
mainLayout.addView(listView);
|
mainLayout.addView(listView);
|
||||||
|
|
||||||
LinearLayout wrapperLayout = new LinearLayout(context);
|
// Create observer for PlayerType changes.
|
||||||
wrapperLayout.setOrientation(LinearLayout.VERTICAL);
|
Function1<PlayerType, Unit> playerTypeObserver = new Function1<>() {
|
||||||
wrapperLayout.setPadding(dip8, 0, dip8, 0);
|
|
||||||
wrapperLayout.addView(mainLayout);
|
|
||||||
dialog.setContentView(wrapperLayout);
|
|
||||||
|
|
||||||
Window window = dialog.getWindow();
|
|
||||||
if (window != null) {
|
|
||||||
WindowManager.LayoutParams params = window.getAttributes();
|
|
||||||
params.gravity = Gravity.BOTTOM;
|
|
||||||
params.y = dip6;
|
|
||||||
int portraitWidth = context.getResources().getDisplayMetrics().widthPixels;
|
|
||||||
if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
||||||
portraitWidth = Math.min(
|
|
||||||
portraitWidth,
|
|
||||||
context.getResources().getDisplayMetrics().heightPixels);
|
|
||||||
// Limit height in landscape mode.
|
|
||||||
params.height = Utils.percentageHeightToPixels(80);
|
|
||||||
} else {
|
|
||||||
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
|
||||||
}
|
|
||||||
params.width = portraitWidth;
|
|
||||||
window.setAttributes(params);
|
|
||||||
window.setBackgroundDrawable(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
|
|
||||||
Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
|
|
||||||
slideInABottomAnimation.setDuration(fadeDurationFast);
|
|
||||||
mainLayout.startAnimation(slideInABottomAnimation);
|
|
||||||
|
|
||||||
// noinspection ClickableViewAccessibility
|
|
||||||
mainLayout.setOnTouchListener(new View.OnTouchListener() {
|
|
||||||
final float dismissThreshold = dipToPixels(100);
|
|
||||||
float touchY;
|
|
||||||
float translationY;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onTouch(View v, MotionEvent event) {
|
public Unit invoke(PlayerType type) {
|
||||||
switch (event.getAction()) {
|
SheetBottomDialog.SlideDialog current = currentDialog.get();
|
||||||
case MotionEvent.ACTION_DOWN:
|
if (current == null || !current.isShowing()) {
|
||||||
touchY = event.getRawY();
|
// Should never happen.
|
||||||
translationY = mainLayout.getTranslationY();
|
PlayerType.getOnChange().removeObserver(this);
|
||||||
return true;
|
Logger.printException(() -> "Removing player type listener as dialog is null or closed");
|
||||||
case MotionEvent.ACTION_MOVE:
|
} else if (type == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) {
|
||||||
final float deltaY = event.getRawY() - touchY;
|
current.dismiss();
|
||||||
if (deltaY >= 0) {
|
Logger.printDebug(() -> "Playback speed dialog dismissed due to PiP mode");
|
||||||
mainLayout.setTranslationY(translationY + deltaY);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
case MotionEvent.ACTION_UP:
|
|
||||||
case MotionEvent.ACTION_CANCEL:
|
|
||||||
if (mainLayout.getTranslationY() > dismissThreshold) {
|
|
||||||
//noinspection ExtractMethodRecommender
|
|
||||||
final float remainingDistance = context.getResources().getDisplayMetrics().heightPixels
|
|
||||||
- mainLayout.getTop();
|
|
||||||
TranslateAnimation slideOut = new TranslateAnimation(
|
|
||||||
0, 0, mainLayout.getTranslationY(), remainingDistance);
|
|
||||||
slideOut.setDuration(fadeDurationFast);
|
|
||||||
slideOut.setAnimationListener(new Animation.AnimationListener() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationStart(Animation animation) {}
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animation animation) {
|
|
||||||
dialog.dismiss();
|
|
||||||
}
|
|
||||||
@Override
|
|
||||||
public void onAnimationRepeat(Animation animation) {}
|
|
||||||
});
|
|
||||||
mainLayout.startAnimation(slideOut);
|
|
||||||
} else {
|
|
||||||
TranslateAnimation slideBack = new TranslateAnimation(
|
|
||||||
0, 0, mainLayout.getTranslationY(), 0);
|
|
||||||
slideBack.setDuration(fadeDurationFast);
|
|
||||||
mainLayout.startAnimation(slideBack);
|
|
||||||
mainLayout.setTranslationY(0);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return Unit.INSTANCE;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add observer to dismiss dialog when entering PiP mode.
|
||||||
|
PlayerType.getOnChange().addObserver(playerTypeObserver);
|
||||||
|
|
||||||
|
// Remove observer when dialog is dismissed.
|
||||||
|
dialog.setOnDismissListener(d -> {
|
||||||
|
PlayerType.getOnChange().removeObserver(playerTypeObserver);
|
||||||
|
Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss");
|
||||||
});
|
});
|
||||||
|
|
||||||
dialog.show();
|
dialog.show(); // Show the dialog.
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "showVideoQualityDialog failure", ex);
|
Logger.printException(() -> "showVideoQualityDialog failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class CustomQualityAdapter extends ArrayAdapter<String> {
|
private static class CustomQualityAdapter extends ArrayAdapter<String> {
|
||||||
private static final int CUSTOM_LIST_ITEM_CHECKED_ID = Utils.getResourceIdentifier(
|
|
||||||
"revanced_custom_list_item_checked", "layout");
|
|
||||||
private static final int CHECK_ICON_ID = Utils.getResourceIdentifier(
|
|
||||||
"revanced_check_icon", "id");
|
|
||||||
private static final int CHECK_ICON_PLACEHOLDER_ID = Utils.getResourceIdentifier(
|
|
||||||
"revanced_check_icon_placeholder", "id");
|
|
||||||
private static final int ITEM_TEXT_ID = Utils.getResourceIdentifier(
|
|
||||||
"revanced_item_text", "id");
|
|
||||||
|
|
||||||
private int selectedPosition = -1;
|
private int selectedPosition = -1;
|
||||||
|
|
||||||
@@ -447,14 +351,14 @@ public class VideoQualityDialogButton {
|
|||||||
|
|
||||||
if (convertView == null) {
|
if (convertView == null) {
|
||||||
convertView = LayoutInflater.from(getContext()).inflate(
|
convertView = LayoutInflater.from(getContext()).inflate(
|
||||||
CUSTOM_LIST_ITEM_CHECKED_ID,
|
LAYOUT_REVANCED_CUSTOM_LIST_ITEM_CHECKED,
|
||||||
parent,
|
parent,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
viewHolder = new ViewHolder();
|
viewHolder = new ViewHolder();
|
||||||
viewHolder.checkIcon = convertView.findViewById(CHECK_ICON_ID);
|
viewHolder.checkIcon = convertView.findViewById(ID_REVANCED_CHECK_ICON);
|
||||||
viewHolder.placeholder = convertView.findViewById(CHECK_ICON_PLACEHOLDER_ID);
|
viewHolder.placeholder = convertView.findViewById(ID_REVANCED_CHECK_ICON_PLACEHOLDER);
|
||||||
viewHolder.textView = convertView.findViewById(ITEM_TEXT_ID);
|
viewHolder.textView = convertView.findViewById(ID_REVANCED_ITEM_TEXT);
|
||||||
convertView.setTag(viewHolder);
|
convertView.setTag(viewHolder);
|
||||||
} else {
|
} else {
|
||||||
viewHolder = (ViewHolder) convertView.getTag();
|
viewHolder = (ViewHolder) convertView.getTag();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package app.revanced.patches.music.misc.settings
|
package app.revanced.patches.music.misc.settings
|
||||||
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patcher.patch.resourcePatch
|
import app.revanced.patcher.patch.resourcePatch
|
||||||
import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName
|
import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName
|
||||||
@@ -10,20 +9,19 @@ import app.revanced.patches.music.misc.extension.sharedExtensionPatch
|
|||||||
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||||
import app.revanced.patches.shared.misc.settings.preference.BasePreference
|
import app.revanced.patches.shared.misc.settings.preference.BasePreference
|
||||||
import app.revanced.patches.shared.misc.settings.preference.BasePreferenceScreen
|
import app.revanced.patches.shared.misc.settings.preference.BasePreferenceScreen
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.InputType
|
||||||
import app.revanced.patches.shared.misc.settings.preference.IntentPreference
|
import app.revanced.patches.shared.misc.settings.preference.IntentPreference
|
||||||
import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference
|
import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference
|
||||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||||
|
import app.revanced.patches.shared.misc.settings.preference.TextPreference
|
||||||
import app.revanced.patches.shared.misc.settings.settingsPatch
|
import app.revanced.patches.shared.misc.settings.settingsPatch
|
||||||
import app.revanced.util.ResourceGroup
|
import app.revanced.patches.youtube.misc.settings.modifyActivityForSettingsInjection
|
||||||
import app.revanced.util.copyResources
|
|
||||||
import app.revanced.util.copyXmlNode
|
import app.revanced.util.copyXmlNode
|
||||||
import app.revanced.util.inputStreamFromBundledResource
|
import app.revanced.util.inputStreamFromBundledResource
|
||||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
|
||||||
|
|
||||||
private const val BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
|
|
||||||
"Lapp/revanced/extension/shared/settings/BaseActivityHook;"
|
|
||||||
private const val GOOGLE_API_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
|
private const val GOOGLE_API_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
|
||||||
"Lapp/revanced/extension/music/settings/GoogleApiActivityHook;"
|
"Lapp/revanced/extension/music/settings/MusicActivityHook;"
|
||||||
|
|
||||||
private val preferences = mutableSetOf<BasePreference>()
|
private val preferences = mutableSetOf<BasePreference>()
|
||||||
|
|
||||||
@@ -31,26 +29,19 @@ private val settingsResourcePatch = resourcePatch {
|
|||||||
dependsOn(
|
dependsOn(
|
||||||
resourceMappingPatch,
|
resourceMappingPatch,
|
||||||
settingsPatch(
|
settingsPatch(
|
||||||
IntentPreference(
|
listOf(
|
||||||
titleKey = "revanced_settings_title",
|
IntentPreference(
|
||||||
summaryKey = null,
|
titleKey = "revanced_settings_title",
|
||||||
intent = newIntent("revanced_settings_intent"),
|
summaryKey = null,
|
||||||
) to "settings_headers",
|
intent = newIntent("revanced_settings_intent"),
|
||||||
|
) to "settings_headers",
|
||||||
|
),
|
||||||
preferences
|
preferences
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
|
|
||||||
// TODO: Remove this when search will be abstract.
|
|
||||||
copyResources(
|
|
||||||
"settings",
|
|
||||||
ResourceGroup(
|
|
||||||
"layout",
|
|
||||||
"revanced_music_settings_with_toolbar.xml"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
val targetResource = "values/styles.xml"
|
val targetResource = "values/styles.xml"
|
||||||
inputStreamFromBundledResource(
|
inputStreamFromBundledResource(
|
||||||
"settings/music",
|
"settings/music",
|
||||||
@@ -98,24 +89,25 @@ val settingsPatch = bytecodePatch(
|
|||||||
selectable = true,
|
selectable = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Modify GoogleApiActivity and remove all existing layout code.
|
PreferenceScreen.GENERAL.addPreferences(
|
||||||
// Must modify an existing activity and cannot add a new activity to the manifest,
|
SwitchPreference("revanced_settings_search_history")
|
||||||
// as that fails for root installations.
|
|
||||||
|
|
||||||
googleApiActivityFingerprint.method.addInstructions(
|
|
||||||
1,
|
|
||||||
"""
|
|
||||||
invoke-static { }, $GOOGLE_API_ACTIVITY_HOOK_CLASS_DESCRIPTOR->createInstance()Lapp/revanced/extension/music/settings/GoogleApiActivityHook;
|
|
||||||
move-result-object v0
|
|
||||||
invoke-static { v0, p0 }, $BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->initialize(Lapp/revanced/extension/shared/settings/BaseActivityHook;Landroid/app/Activity;)V
|
|
||||||
return-void
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remove other methods as they will break as the onCreate method is modified above.
|
PreferenceScreen.MISC.addPreferences(
|
||||||
googleApiActivityFingerprint.classDef.apply {
|
TextPreference(
|
||||||
methods.removeIf { it.name != "onCreate" && !MethodUtil.isConstructor(it) }
|
key = null,
|
||||||
}
|
titleKey = "revanced_pref_import_export_title",
|
||||||
|
summaryKey = "revanced_pref_import_export_summary",
|
||||||
|
inputType = InputType.TEXT_MULTI_LINE,
|
||||||
|
tag = "app.revanced.extension.shared.settings.preference.ImportExportPreference",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
modifyActivityForSettingsInjection(
|
||||||
|
googleApiActivityFingerprint.classDef,
|
||||||
|
googleApiActivityFingerprint.method,
|
||||||
|
GOOGLE_API_ACTIVITY_HOOK_CLASS_DESCRIPTOR
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
finalize {
|
finalize {
|
||||||
|
|||||||
@@ -65,14 +65,15 @@ fun settingsPatch (
|
|||||||
copyResources(
|
copyResources(
|
||||||
"settings",
|
"settings",
|
||||||
ResourceGroup("xml", "revanced_prefs.xml", "revanced_prefs_icons.xml"),
|
ResourceGroup("xml", "revanced_prefs.xml", "revanced_prefs_icons.xml"),
|
||||||
|
ResourceGroup("menu", "revanced_search_menu.xml"),
|
||||||
ResourceGroup("drawable",
|
ResourceGroup("drawable",
|
||||||
// CustomListPreference resources.
|
// CustomListPreference resources.
|
||||||
"revanced_ic_dialog_alert.xml",
|
"revanced_ic_dialog_alert.xml",
|
||||||
"revanced_settings_arrow_time.xml",
|
"revanced_settings_arrow_time.xml",
|
||||||
"revanced_settings_circle_background.xml",
|
|
||||||
"revanced_settings_cursor.xml",
|
"revanced_settings_cursor.xml",
|
||||||
"revanced_settings_custom_checkmark.xml",
|
"revanced_settings_custom_checkmark.xml",
|
||||||
"revanced_settings_search_icon.xml",
|
"revanced_settings_search_icon.xml",
|
||||||
|
"revanced_settings_search_remove.xml",
|
||||||
"revanced_settings_toolbar_arrow_left.xml",
|
"revanced_settings_toolbar_arrow_left.xml",
|
||||||
),
|
),
|
||||||
ResourceGroup("layout",
|
ResourceGroup("layout",
|
||||||
@@ -80,6 +81,16 @@ fun settingsPatch (
|
|||||||
// Color picker.
|
// Color picker.
|
||||||
"revanced_color_dot_widget.xml",
|
"revanced_color_dot_widget.xml",
|
||||||
"revanced_color_picker.xml",
|
"revanced_color_picker.xml",
|
||||||
|
// Search.
|
||||||
|
"revanced_preference_search_history_item.xml",
|
||||||
|
"revanced_preference_search_history_screen.xml",
|
||||||
|
"revanced_preference_search_no_result.xml",
|
||||||
|
"revanced_preference_search_result_color.xml",
|
||||||
|
"revanced_preference_search_result_group_header.xml",
|
||||||
|
"revanced_preference_search_result_list.xml",
|
||||||
|
"revanced_preference_search_result_regular.xml",
|
||||||
|
"revanced_preference_search_result_switch.xml",
|
||||||
|
"revanced_settings_with_toolbar.xml"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c
|
|||||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||||
|
|
||||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||||
"Lapp/revanced/extension/tiktok/settings/AdPersonalizationActivityHook;"
|
"Lapp/revanced/extension/tiktok/settings/TikTokActivityHook;"
|
||||||
|
|
||||||
val settingsPatch = bytecodePatch(
|
val settingsPatch = bytecodePatch(
|
||||||
name = "Settings",
|
name = "Settings",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ private const val MENU_DISMISS_EVENT_CLASS_DESCRIPTOR =
|
|||||||
"Ltv/twitch/android/feature/settings/menu/SettingsMenuViewDelegate\$Event\$OnDismissClicked;"
|
"Ltv/twitch/android/feature/settings/menu/SettingsMenuViewDelegate\$Event\$OnDismissClicked;"
|
||||||
|
|
||||||
private const val EXTENSION_PACKAGE = "app/revanced/extension/twitch"
|
private const val EXTENSION_PACKAGE = "app/revanced/extension/twitch"
|
||||||
private const val ACTIVITY_HOOKS_CLASS_DESCRIPTOR = "L$EXTENSION_PACKAGE/settings/AppCompatActivityHook;"
|
private const val ACTIVITY_HOOKS_CLASS_DESCRIPTOR = "L$EXTENSION_PACKAGE/settings/TwitchActivityHook;"
|
||||||
private const val UTILS_CLASS_DESCRIPTOR = "L$EXTENSION_PACKAGE/Utils;"
|
private const val UTILS_CLASS_DESCRIPTOR = "L$EXTENSION_PACKAGE/Utils;"
|
||||||
|
|
||||||
private val preferences = mutableSetOf<BasePreference>()
|
private val preferences = mutableSetOf<BasePreference>()
|
||||||
|
|||||||
@@ -46,10 +46,10 @@ private val swipeControlsResourcePatch = resourcePatch {
|
|||||||
ListPreference("revanced_swipe_overlay_style"),
|
ListPreference("revanced_swipe_overlay_style"),
|
||||||
TextPreference("revanced_swipe_overlay_background_opacity", inputType = InputType.NUMBER),
|
TextPreference("revanced_swipe_overlay_background_opacity", inputType = InputType.NUMBER),
|
||||||
TextPreference("revanced_swipe_overlay_progress_brightness_color",
|
TextPreference("revanced_swipe_overlay_progress_brightness_color",
|
||||||
tag = "app.revanced.extension.shared.settings.preference.ColorPickerPreference",
|
tag = "app.revanced.extension.shared.settings.preference.ColorPickerWithOpacitySliderPreference",
|
||||||
inputType = InputType.TEXT_CAP_CHARACTERS),
|
inputType = InputType.TEXT_CAP_CHARACTERS),
|
||||||
TextPreference("revanced_swipe_overlay_progress_volume_color",
|
TextPreference("revanced_swipe_overlay_progress_volume_color",
|
||||||
tag = "app.revanced.extension.shared.settings.preference.ColorPickerPreference",
|
tag = "app.revanced.extension.shared.settings.preference.ColorPickerWithOpacitySliderPreference",
|
||||||
inputType = InputType.TEXT_CAP_CHARACTERS),
|
inputType = InputType.TEXT_CAP_CHARACTERS),
|
||||||
TextPreference("revanced_swipe_text_overlay_size", inputType = InputType.NUMBER),
|
TextPreference("revanced_swipe_text_overlay_size", inputType = InputType.NUMBER),
|
||||||
TextPreference("revanced_swipe_overlay_timeout", inputType = InputType.NUMBER),
|
TextPreference("revanced_swipe_overlay_timeout", inputType = InputType.NUMBER),
|
||||||
|
|||||||
@@ -189,7 +189,10 @@ val hideLayoutComponentsPatch = bytecodePatch(
|
|||||||
SwitchPreference("revanced_hide_keyword_content_subscriptions"),
|
SwitchPreference("revanced_hide_keyword_content_subscriptions"),
|
||||||
SwitchPreference("revanced_hide_keyword_content_search"),
|
SwitchPreference("revanced_hide_keyword_content_search"),
|
||||||
TextPreference("revanced_hide_keyword_content_phrases", inputType = InputType.TEXT_MULTI_LINE),
|
TextPreference("revanced_hide_keyword_content_phrases", inputType = InputType.TEXT_MULTI_LINE),
|
||||||
NonInteractivePreference("revanced_hide_keyword_content_about"),
|
NonInteractivePreference(
|
||||||
|
key = "revanced_hide_keyword_content_about",
|
||||||
|
tag = "app.revanced.extension.shared.settings.preference.BulletPointPreference"
|
||||||
|
),
|
||||||
NonInteractivePreference(
|
NonInteractivePreference(
|
||||||
key = "revanced_hide_keyword_content_about_whole_words",
|
key = "revanced_hide_keyword_content_about_whole_words",
|
||||||
tag = "app.revanced.extension.youtube.settings.preference.HtmlPreference",
|
tag = "app.revanced.extension.youtube.settings.preference.HtmlPreference",
|
||||||
@@ -223,7 +226,10 @@ val hideLayoutComponentsPatch = bytecodePatch(
|
|||||||
SwitchPreference("revanced_hide_chips_shelf"),
|
SwitchPreference("revanced_hide_chips_shelf"),
|
||||||
SwitchPreference("revanced_hide_expandable_card"),
|
SwitchPreference("revanced_hide_expandable_card"),
|
||||||
SwitchPreference("revanced_hide_floating_microphone_button"),
|
SwitchPreference("revanced_hide_floating_microphone_button"),
|
||||||
SwitchPreference("revanced_hide_horizontal_shelves"),
|
SwitchPreference(
|
||||||
|
key = "revanced_hide_horizontal_shelves",
|
||||||
|
tag = "app.revanced.extension.shared.settings.preference.BulletPointSwitchPreference"
|
||||||
|
),
|
||||||
SwitchPreference("revanced_hide_image_shelf"),
|
SwitchPreference("revanced_hide_image_shelf"),
|
||||||
SwitchPreference("revanced_hide_latest_posts"),
|
SwitchPreference("revanced_hide_latest_posts"),
|
||||||
SwitchPreference("revanced_hide_mix_playlists"),
|
SwitchPreference("revanced_hide_mix_playlists"),
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
|||||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patcher.patch.resourcePatch
|
import app.revanced.patcher.patch.resourcePatch
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
||||||
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||||
import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName
|
import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName
|
||||||
import app.revanced.patches.all.misc.resources.addResources
|
import app.revanced.patches.all.misc.resources.addResources
|
||||||
@@ -31,8 +33,8 @@ import com.android.tools.smali.dexlib2.util.MethodUtil
|
|||||||
|
|
||||||
private const val BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
|
private const val BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
|
||||||
"Lapp/revanced/extension/shared/settings/BaseActivityHook;"
|
"Lapp/revanced/extension/shared/settings/BaseActivityHook;"
|
||||||
private const val LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
|
private const val YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR =
|
||||||
"Lapp/revanced/extension/youtube/settings/LicenseActivityHook;"
|
"Lapp/revanced/extension/youtube/settings/YouTubeActivityHook;"
|
||||||
|
|
||||||
internal var appearanceStringId = -1L
|
internal var appearanceStringId = -1L
|
||||||
private set
|
private set
|
||||||
@@ -73,7 +75,8 @@ private val settingsResourcePatch = resourcePatch {
|
|||||||
// Use same colors as stock YouTube.
|
// Use same colors as stock YouTube.
|
||||||
overrideThemeColors("@color/yt_white1", "@color/yt_black3")
|
overrideThemeColors("@color/yt_white1", "@color/yt_black3")
|
||||||
|
|
||||||
arrayOf(
|
copyResources(
|
||||||
|
"settings",
|
||||||
ResourceGroup("drawable",
|
ResourceGroup("drawable",
|
||||||
"revanced_settings_icon.xml",
|
"revanced_settings_icon.xml",
|
||||||
"revanced_settings_screen_00_about.xml",
|
"revanced_settings_screen_00_about.xml",
|
||||||
@@ -89,23 +92,15 @@ private val settingsResourcePatch = resourcePatch {
|
|||||||
"revanced_settings_screen_10_sponsorblock.xml",
|
"revanced_settings_screen_10_sponsorblock.xml",
|
||||||
"revanced_settings_screen_11_misc.xml",
|
"revanced_settings_screen_11_misc.xml",
|
||||||
"revanced_settings_screen_12_video.xml",
|
"revanced_settings_screen_12_video.xml",
|
||||||
),
|
)
|
||||||
ResourceGroup("layout",
|
)
|
||||||
"revanced_preference_with_icon_no_search_result.xml",
|
|
||||||
"revanced_search_suggestion_item.xml",
|
|
||||||
"revanced_settings_with_toolbar.xml"
|
|
||||||
),
|
|
||||||
ResourceGroup("menu", "revanced_search_menu.xml")
|
|
||||||
).forEach { resourceGroup ->
|
|
||||||
copyResources("settings", resourceGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy style properties used to fix over-sized copy menu that appear in EditTextPreference.
|
// Copy style properties used to fix over-sized copy menu that appear in EditTextPreference.
|
||||||
// For a full explanation of how this fixes the issue, see the comments in this style file
|
// For a full explanation of how this fixes the issue, see the comments in this style file
|
||||||
// and the comments in the extension code.
|
// and the comments in the extension code.
|
||||||
val targetResource = "values/styles.xml"
|
val targetResource = "values/styles.xml"
|
||||||
inputStreamFromBundledResource(
|
inputStreamFromBundledResource(
|
||||||
"settings/host",
|
"settings/youtube",
|
||||||
targetResource,
|
targetResource,
|
||||||
)!!.let { inputStream ->
|
)!!.let { inputStream ->
|
||||||
"resources".copyXmlNode(
|
"resources".copyXmlNode(
|
||||||
@@ -215,92 +210,6 @@ val settingsPatch = bytecodePatch(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
// Modify the license activity and remove all existing layout code.
|
|
||||||
// Must modify an existing activity and cannot add a new activity to the manifest,
|
|
||||||
// as that fails for root installations.
|
|
||||||
|
|
||||||
licenseActivityOnCreateFingerprint.method.addInstructions(
|
|
||||||
1,
|
|
||||||
"""
|
|
||||||
invoke-static {}, $LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->createInstance()Lapp/revanced/extension/youtube/settings/LicenseActivityHook;
|
|
||||||
move-result-object v0
|
|
||||||
invoke-static { v0, p0 }, $BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->initialize(Lapp/revanced/extension/shared/settings/BaseActivityHook;Landroid/app/Activity;)V
|
|
||||||
return-void
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
|
|
||||||
// Remove other methods as they will break as the onCreate method is modified above.
|
|
||||||
licenseActivityOnCreateFingerprint.classDef.apply {
|
|
||||||
methods.removeIf { it.name != "onCreate" && !MethodUtil.isConstructor(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
licenseActivityOnCreateFingerprint.classDef.apply {
|
|
||||||
// Add attachBaseContext method to override the context for setting a specific language.
|
|
||||||
ImmutableMethod(
|
|
||||||
type,
|
|
||||||
"attachBaseContext",
|
|
||||||
listOf(ImmutableMethodParameter("Landroid/content/Context;", null, null)),
|
|
||||||
"V",
|
|
||||||
AccessFlags.PROTECTED.value,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
MutableMethodImplementation(3),
|
|
||||||
).toMutable().apply {
|
|
||||||
addInstructions(
|
|
||||||
"""
|
|
||||||
invoke-static { p1 }, $LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->getAttachBaseContext(Landroid/content/Context;)Landroid/content/Context;
|
|
||||||
move-result-object p1
|
|
||||||
invoke-super { p0, p1 }, $superclass->attachBaseContext(Landroid/content/Context;)V
|
|
||||||
return-void
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
}.let(methods::add)
|
|
||||||
|
|
||||||
// Add onBackPressed method to handle back button presses, delegating to SearchViewController.
|
|
||||||
ImmutableMethod(
|
|
||||||
type,
|
|
||||||
"onBackPressed",
|
|
||||||
emptyList(),
|
|
||||||
"V",
|
|
||||||
AccessFlags.PUBLIC.value,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
MutableMethodImplementation(3),
|
|
||||||
).toMutable().apply {
|
|
||||||
addInstructions(
|
|
||||||
"""
|
|
||||||
invoke-static {}, Lapp/revanced/extension/youtube/settings/SearchViewController;->handleBackPress()Z
|
|
||||||
move-result v0
|
|
||||||
if-nez v0, :search_handled
|
|
||||||
invoke-virtual { p0 }, Landroid/app/Activity;->finish()V
|
|
||||||
:search_handled
|
|
||||||
return-void
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
}.let(methods::add)
|
|
||||||
|
|
||||||
// Add onConfigurationChanged method to handle configuration changes (e.g., screen orientation).
|
|
||||||
ImmutableMethod(
|
|
||||||
type,
|
|
||||||
"onConfigurationChanged",
|
|
||||||
listOf(ImmutableMethodParameter("Landroid/content/res/Configuration;", null, null)),
|
|
||||||
"V",
|
|
||||||
AccessFlags.PUBLIC.value,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
MutableMethodImplementation(3)
|
|
||||||
).toMutable().apply {
|
|
||||||
addInstructions(
|
|
||||||
"""
|
|
||||||
invoke-super { p0, p1 }, Landroid/app/Activity;->onConfigurationChanged(Landroid/content/res/Configuration;)V
|
|
||||||
invoke-static { p0, p1 }, $LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->handleConfigurationChanged(Landroid/app/Activity;Landroid/content/res/Configuration;)V
|
|
||||||
return-void
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
}.let(methods::add)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update shared dark mode status based on YT theme.
|
// Update shared dark mode status based on YT theme.
|
||||||
// This is needed because YT allows forcing light/dark mode
|
// This is needed because YT allows forcing light/dark mode
|
||||||
// which then differs from the system dark mode status.
|
// which then differs from the system dark mode status.
|
||||||
@@ -309,7 +218,7 @@ val settingsPatch = bytecodePatch(
|
|||||||
val register = getInstruction<OneRegisterInstruction>(index).registerA
|
val register = getInstruction<OneRegisterInstruction>(index).registerA
|
||||||
addInstructionsAtControlFlowLabel(
|
addInstructionsAtControlFlowLabel(
|
||||||
index,
|
index,
|
||||||
"invoke-static { v$register }, ${LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR}->updateLightDarkModeStatus(Ljava/lang/Enum;)V",
|
"invoke-static { v$register }, ${YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR}->updateLightDarkModeStatus(Ljava/lang/Enum;)V",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -317,7 +226,13 @@ val settingsPatch = bytecodePatch(
|
|||||||
// Add setting to force Cairo settings fragment on/off.
|
// Add setting to force Cairo settings fragment on/off.
|
||||||
cairoFragmentConfigFingerprint.method.insertLiteralOverride(
|
cairoFragmentConfigFingerprint.method.insertLiteralOverride(
|
||||||
CAIRO_CONFIG_LITERAL_VALUE,
|
CAIRO_CONFIG_LITERAL_VALUE,
|
||||||
"$LICENSE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z"
|
"$YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->useCairoSettingsFragment(Z)Z"
|
||||||
|
)
|
||||||
|
|
||||||
|
modifyActivityForSettingsInjection(
|
||||||
|
licenseActivityOnCreateFingerprint.classDef,
|
||||||
|
licenseActivityOnCreateFingerprint.method,
|
||||||
|
YOUTUBE_ACTIVITY_HOOK_CLASS_DESCRIPTOR
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,6 +241,76 @@ val settingsPatch = bytecodePatch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modifies the activity to show ReVanced settings instead of it's original purpose.
|
||||||
|
*/
|
||||||
|
internal fun modifyActivityForSettingsInjection(
|
||||||
|
activityOnCreateClass: MutableClass,
|
||||||
|
activityOnCreateMethod: MutableMethod,
|
||||||
|
extensionClassType: String
|
||||||
|
) {
|
||||||
|
// Modify Activity and remove all existing layout code.
|
||||||
|
// Must modify an existing activity and cannot add a new activity to the manifest,
|
||||||
|
// as that fails for root installations.
|
||||||
|
activityOnCreateMethod.addInstructions(
|
||||||
|
1,
|
||||||
|
"""
|
||||||
|
invoke-static { p0 }, $extensionClassType->initialize(Landroid/app/Activity;)V
|
||||||
|
return-void
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
// Remove other methods as they will break as the onCreate method is modified above.
|
||||||
|
activityOnCreateClass.apply {
|
||||||
|
methods.removeIf { it != activityOnCreateMethod && !MethodUtil.isConstructor(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override base context to allow using ReVanced specific settings.
|
||||||
|
ImmutableMethod(
|
||||||
|
activityOnCreateClass.type,
|
||||||
|
"attachBaseContext",
|
||||||
|
listOf(ImmutableMethodParameter("Landroid/content/Context;", null, null)),
|
||||||
|
"V",
|
||||||
|
AccessFlags.PROTECTED.value,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
MutableMethodImplementation(3),
|
||||||
|
).toMutable().apply {
|
||||||
|
addInstructions(
|
||||||
|
"""
|
||||||
|
invoke-static { p1 }, $BASE_ACTIVITY_HOOK_CLASS_DESCRIPTOR->getAttachBaseContext(Landroid/content/Context;)Landroid/content/Context;
|
||||||
|
move-result-object p1
|
||||||
|
invoke-super { p0, p1 }, ${activityOnCreateClass.superclass}->attachBaseContext(Landroid/content/Context;)V
|
||||||
|
return-void
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}.let(activityOnCreateClass.methods::add)
|
||||||
|
|
||||||
|
// Override finish() to intercept back gesture.
|
||||||
|
ImmutableMethod(
|
||||||
|
activityOnCreateClass.type,
|
||||||
|
"finish",
|
||||||
|
emptyList(),
|
||||||
|
"V",
|
||||||
|
AccessFlags.PUBLIC.value,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
MutableMethodImplementation(3),
|
||||||
|
).toMutable().apply {
|
||||||
|
addInstructions(
|
||||||
|
"""
|
||||||
|
invoke-static {}, $extensionClassType->handleFinish()Z
|
||||||
|
move-result v0
|
||||||
|
if-nez v0, :search_handled
|
||||||
|
invoke-super { p0 }, Landroid/app/Activity;->finish()V
|
||||||
|
return-void
|
||||||
|
:search_handled
|
||||||
|
return-void
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
}.let(activityOnCreateClass.methods::add)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an intent to open ReVanced settings.
|
* Creates an intent to open ReVanced settings.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ internal val settingsMenuVideoSpeedGroup = mutableSetOf<BasePreference>()
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val playbackSpeedPatch = bytecodePatch(
|
val playbackSpeedPatch = bytecodePatch(
|
||||||
name = "Playback speed",
|
name = "Playback speed",
|
||||||
description = "Adds options to customize available playback speeds, set default a playback speed, " +
|
description = "Adds options to customize available playback speeds, set a default playback speed, " +
|
||||||
"and show a speed dialog button in the video player.",
|
"and show a speed dialog button in the video player.",
|
||||||
) {
|
) {
|
||||||
dependsOn(
|
dependsOn(
|
||||||
|
|||||||
@@ -382,7 +382,7 @@
|
|||||||
</string-array>
|
</string-array>
|
||||||
</patch>
|
</patch>
|
||||||
<patch id="layout.sponsorblock.sponsorBlockResourcePatch">
|
<patch id="layout.sponsorblock.sponsorBlockResourcePatch">
|
||||||
<string-array name="revanced_sb_duration_entries">
|
<string-array name="revanced_sb_auto_hide_skip_button_duration_entries">
|
||||||
<item>@string/revanced_sb_duration_1s</item>
|
<item>@string/revanced_sb_duration_1s</item>
|
||||||
<item>@string/revanced_sb_duration_2s</item>
|
<item>@string/revanced_sb_duration_2s</item>
|
||||||
<item>@string/revanced_sb_duration_3s</item>
|
<item>@string/revanced_sb_duration_3s</item>
|
||||||
@@ -394,7 +394,7 @@
|
|||||||
<item>@string/revanced_sb_duration_9s</item>
|
<item>@string/revanced_sb_duration_9s</item>
|
||||||
<item>@string/revanced_sb_duration_10s</item>
|
<item>@string/revanced_sb_duration_10s</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
<string-array name="revanced_sb_duration_entry_values">
|
<string-array name="revanced_sb_auto_hide_skip_button_duration_entry_values">
|
||||||
<item>ONE_SECOND</item>
|
<item>ONE_SECOND</item>
|
||||||
<item>TWO_SECONDS</item>
|
<item>TWO_SECONDS</item>
|
||||||
<item>THREE_SECONDS</item>
|
<item>THREE_SECONDS</item>
|
||||||
@@ -406,6 +406,32 @@
|
|||||||
<item>NINE_SECONDS</item>
|
<item>NINE_SECONDS</item>
|
||||||
<item>TEN_SECONDS</item>
|
<item>TEN_SECONDS</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<!-- No easy way to make an alias to another array declaration, so copy it again here. -->
|
||||||
|
<string-array name="revanced_sb_toast_on_skip_duration_entries">
|
||||||
|
<item>@string/revanced_sb_duration_1s</item>
|
||||||
|
<item>@string/revanced_sb_duration_2s</item>
|
||||||
|
<item>@string/revanced_sb_duration_3s</item>
|
||||||
|
<item>@string/revanced_sb_duration_4s</item>
|
||||||
|
<item>@string/revanced_sb_duration_5s</item>
|
||||||
|
<item>@string/revanced_sb_duration_6s</item>
|
||||||
|
<item>@string/revanced_sb_duration_7s</item>
|
||||||
|
<item>@string/revanced_sb_duration_8s</item>
|
||||||
|
<item>@string/revanced_sb_duration_9s</item>
|
||||||
|
<item>@string/revanced_sb_duration_10s</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="revanced_sb_toast_on_skip_duration_entry_values">
|
||||||
|
<item>ONE_SECOND</item>
|
||||||
|
<item>TWO_SECONDS</item>
|
||||||
|
<item>THREE_SECONDS</item>
|
||||||
|
<item>FOUR_SECONDS</item>
|
||||||
|
<item>FIVE_SECONDS</item>
|
||||||
|
<item>SIX_SECONDS</item>
|
||||||
|
<item>SEVEN_SECONDS</item>
|
||||||
|
<item>EIGHT_SECONDS</item>
|
||||||
|
<item>NINE_SECONDS</item>
|
||||||
|
<item>TEN_SECONDS</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
</patch>
|
</patch>
|
||||||
<patch id="layout.shortsplayer.shortsPlayerTypePatch">
|
<patch id="layout.shortsplayer.shortsPlayerTypePatch">
|
||||||
<string-array name="revanced_shorts_player_type_legacy_entries">
|
<string-array name="revanced_shorts_player_type_legacy_entries">
|
||||||
|
|||||||
@@ -48,14 +48,28 @@ Second \"item\" text"</string>
|
|||||||
<string name="revanced_settings_search_hint">Search settings</string>
|
<string name="revanced_settings_search_hint">Search settings</string>
|
||||||
<string name="revanced_settings_search_no_results_title">No results found for \'%s\'</string>
|
<string name="revanced_settings_search_no_results_title">No results found for \'%s\'</string>
|
||||||
<string name="revanced_settings_search_no_results_summary">Try another keyword</string>
|
<string name="revanced_settings_search_no_results_summary">Try another keyword</string>
|
||||||
|
<string name="revanced_settings_search_recent_searches">Recent searches</string>
|
||||||
<string name="revanced_settings_search_remove_message">Remove from search history?</string>
|
<string name="revanced_settings_search_remove_message">Remove from search history?</string>
|
||||||
|
<string name="revanced_settings_search_clear_history">Clear search history</string>
|
||||||
|
<string name="revanced_settings_search_clear_history_message">Are you sure you want to clear all search history?</string>
|
||||||
|
<string name="revanced_settings_search_tips_title">Search Tips</string>
|
||||||
|
<string name="revanced_settings_search_tips_summary">"• Tap a path to navigate to it
|
||||||
|
• Long-press a setting to navigate to it
|
||||||
|
• Press Enter to save a search query to history
|
||||||
|
• Search ignores casing and punctuation
|
||||||
|
• Parent settings appear above disabled child settings"</string>
|
||||||
|
<string name="revanced_settings_search_empty_history_title">Search history is empty</string>
|
||||||
|
<string name="revanced_settings_search_empty_history_summary">To save search history, type a search query and press Enter</string>
|
||||||
|
<string name="revanced_settings_search_history_title">Show settings search history</string>
|
||||||
|
<string name="revanced_settings_search_history_summary_on">Settings search history is shown</string>
|
||||||
|
<string name="revanced_settings_search_history_summary_off">Settings search history is not shown</string>
|
||||||
<string name="revanced_show_menu_icons_title">Show ReVanced setting icons</string>
|
<string name="revanced_show_menu_icons_title">Show ReVanced setting icons</string>
|
||||||
<string name="revanced_show_menu_icons_summary_on">Setting icons are shown</string>
|
<string name="revanced_show_menu_icons_summary_on">Setting icons are shown</string>
|
||||||
<string name="revanced_show_menu_icons_summary_off">Setting icons are not shown</string>
|
<string name="revanced_show_menu_icons_summary_off">Setting icons are not shown</string>
|
||||||
<string name="revanced_language_title">ReVanced language</string>
|
<string name="revanced_language_title">ReVanced language</string>
|
||||||
<string name="revanced_language_user_dialog_message">"Translations for some languages may be missing or incomplete.
|
<string name="revanced_language_user_dialog_message">"Translations for some languages may be missing or incomplete.
|
||||||
|
|
||||||
To translate new languages visit translate.revanced.app"</string>
|
To translate new languages or improve the existing translations, visit translate.revanced.app"</string>
|
||||||
<string name="revanced_language_DEFAULT">App language</string>
|
<string name="revanced_language_DEFAULT">App language</string>
|
||||||
<string name="revanced_language_AM" translatable="false">አማርኛ</string>
|
<string name="revanced_language_AM" translatable="false">አማርኛ</string>
|
||||||
<string name="revanced_language_AR" translatable="false">العربية</string>
|
<string name="revanced_language_AR" translatable="false">العربية</string>
|
||||||
@@ -200,9 +214,6 @@ You will not be notified of any unexpected events."</string>
|
|||||||
<string name="revanced_restore_old_settings_menus_title">Restore old settings menus</string>
|
<string name="revanced_restore_old_settings_menus_title">Restore old settings menus</string>
|
||||||
<string name="revanced_restore_old_settings_menus_summary_on">Old settings menus are shown</string>
|
<string name="revanced_restore_old_settings_menus_summary_on">Old settings menus are shown</string>
|
||||||
<string name="revanced_restore_old_settings_menus_summary_off">Old settings menus are not shown</string>
|
<string name="revanced_restore_old_settings_menus_summary_off">Old settings menus are not shown</string>
|
||||||
<string name="revanced_settings_search_history_title">Show settings search history</string>
|
|
||||||
<string name="revanced_settings_search_history_summary_on">Settings search history is shown</string>
|
|
||||||
<string name="revanced_settings_search_history_summary_off">Settings search history is not shown</string>
|
|
||||||
</patch>
|
</patch>
|
||||||
<patch id="misc.backgroundplayback.backgroundPlaybackPatch">
|
<patch id="misc.backgroundplayback.backgroundPlaybackPatch">
|
||||||
<string name="revanced_shorts_disable_background_playback_title">Disable Shorts background play</string>
|
<string name="revanced_shorts_disable_background_playback_title">Disable Shorts background play</string>
|
||||||
@@ -246,6 +257,7 @@ However, enabling this will also log some user data such as your IP address."</s
|
|||||||
<string name="revanced_hide_floating_microphone_button_summary_off">Floating microphone button in search is shown</string>
|
<string name="revanced_hide_floating_microphone_button_summary_off">Floating microphone button in search is shown</string>
|
||||||
<string name="revanced_hide_horizontal_shelves_title">Hide horizontal shelves</string>
|
<string name="revanced_hide_horizontal_shelves_title">Hide horizontal shelves</string>
|
||||||
<string name="revanced_hide_horizontal_shelves_summary_on">"Horizontal shelves are hidden, such as:
|
<string name="revanced_hide_horizontal_shelves_summary_on">"Horizontal shelves are hidden, such as:
|
||||||
|
|
||||||
• Breaking news
|
• Breaking news
|
||||||
• Continue watching
|
• Continue watching
|
||||||
• Explore more channels
|
• Explore more channels
|
||||||
@@ -1128,9 +1140,9 @@ This feature works best with a video quality of 720p or lower and when using a v
|
|||||||
<string name="revanced_sb_guidelines_popup_already_read">Already read</string>
|
<string name="revanced_sb_guidelines_popup_already_read">Already read</string>
|
||||||
<string name="revanced_sb_guidelines_popup_open">Show me</string>
|
<string name="revanced_sb_guidelines_popup_open">Show me</string>
|
||||||
<string name="revanced_sb_general">General</string>
|
<string name="revanced_sb_general">General</string>
|
||||||
<string name="revanced_sb_toast_on_connection_error_title">Show a toast if API is not available</string>
|
<string name="revanced_sb_toast_on_connection_error">Show a toast if API is not available</string>
|
||||||
<string name="revanced_sb_toast_on_connection_error_summary_on">Toast is shown if SponsorBlock is not available</string>
|
<string name="revanced_sb_toast_on_connection_error_sum_on">Toast is shown if SponsorBlock is not available</string>
|
||||||
<string name="revanced_sb_toast_on_connection_error_summary_off">Toast is not shown if SponsorBlock is not available</string>
|
<string name="revanced_sb_toast_on_connection_error_sum_off">Toast is not shown if SponsorBlock is not available</string>
|
||||||
<string name="revanced_sb_general_skipcount">Enable skip count tracking</string>
|
<string name="revanced_sb_general_skipcount">Enable skip count tracking</string>
|
||||||
<string name="revanced_sb_general_skipcount_sum_on">Lets the SponsorBlock leaderboard know how much time is saved. A message is sent to the leaderboard each time a segment is skipped</string>
|
<string name="revanced_sb_general_skipcount_sum_on">Lets the SponsorBlock leaderboard know how much time is saved. A message is sent to the leaderboard each time a segment is skipped</string>
|
||||||
<string name="revanced_sb_general_skipcount_sum_off">Skip count tracking is not enabled</string>
|
<string name="revanced_sb_general_skipcount_sum_off">Skip count tracking is not enabled</string>
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<solid android:color="#FF0000" />
|
|
||||||
<size
|
|
||||||
android:width="20dp"
|
|
||||||
android:height="20dp" />
|
|
||||||
</shape>
|
|
||||||
@@ -5,5 +5,5 @@
|
|||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path
|
<path
|
||||||
android:fillColor="?android:attr/textColorPrimary"
|
android:fillColor="?android:attr/textColorPrimary"
|
||||||
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z"/>
|
android:pathData="M9.29446,19 L3.4,12.708 L4.24339,11.8029 L9.29446,17.1896 L20.1565,5.6 L21,6.50026 Z M9.29446,19" />
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="?android:attr/textColorPrimary"
|
||||||
|
android:pathData="M7.61719,20 C7.16797,20,6.78516,19.8438,6.47266,19.5273 C6.15625,19.2148,6,18.832,6,18.3828 L6,6 L5,6 L5,5 L9,5 L9,4.23047 L15,4.23047 L15,5 L19,5 L19,6 L18,6 L18,18.3828 C18,18.8438,17.8477,19.2305,17.5391,19.5391 C17.2305,19.8477,16.8438,20,16.3828,20 Z M17,6 L7,6 L7,18.3828 C7,18.5625,7.05859,18.7109,7.17188,18.8281 C7.28906,18.9414,7.4375,19,7.61719,19 L16.3828,19 C16.5391,19,16.6797,18.9375,16.8086,18.8086 C16.9375,18.6797,17,18.5391,17,18.3828 Z M9.80859,17 L10.8086,17 L10.8086,8 L9.80859,8 Z M13.1914,17 L14.1914,17 L14.1914,8 L13.1914,8 Z M7,6 L7,19 Z M7,6" />
|
||||||
|
</vector>
|
||||||
@@ -8,13 +8,9 @@
|
|||||||
android:clipToPadding="false">
|
android:clipToPadding="false">
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/revanced_color_dot_widget"
|
android:id="@+id/preference_color_dot"
|
||||||
android:layout_width="20dp"
|
android:layout_width="20dp"
|
||||||
android:layout_height="20dp"
|
android:layout_height="20dp"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center" />
|
||||||
android:background="@drawable/revanced_settings_circle_background"
|
|
||||||
android:elevation="2dp"
|
|
||||||
android:translationZ="2dp"
|
|
||||||
android:outlineProvider="background" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
android:id="@+id/revanced_check_icon"
|
android:id="@+id/revanced_check_icon"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="24dp"
|
||||||
android:src="@drawable/revanced_settings_custom_checkmark"
|
android:src="@drawable/revanced_settings_custom_checkmark"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:contentDescription="@null" />
|
android:contentDescription="@null" />
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
android:id="@+id/revanced_check_icon_placeholder"
|
android:id="@+id/revanced_check_icon_placeholder"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="24dp"
|
||||||
android:visibility="invisible" />
|
android:visibility="invisible" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:fitsSystemWindows="true"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:transitionGroup="true">
|
|
||||||
|
|
||||||
<!-- Parent container for Toolbar -->
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/revanced_toolbar_parent"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/action_bar_height"
|
|
||||||
android:background="@color/ytm_color_black"
|
|
||||||
android:elevation="0dp">
|
|
||||||
|
|
||||||
<!-- Toolbar -->
|
|
||||||
<android.support.v7.widget.Toolbar
|
|
||||||
android:id="@+id/revanced_toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="@dimen/action_bar_height"
|
|
||||||
android:background="@color/ytm_color_black"
|
|
||||||
app:titleTextColor="?attr/colorOnSurface"
|
|
||||||
app:navigationIcon="@drawable/revanced_settings_toolbar_arrow_left"
|
|
||||||
app:title="@string/revanced_settings_title" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<!-- Preference content container -->
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/revanced_settings_fragments"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:background="@color/ytm_color_black" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
</merge>
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingBottom="12dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true">
|
||||||
|
|
||||||
|
<!-- History icon -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/history_icon"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:src="@drawable/revanced_settings_arrow_time"
|
||||||
|
android:tint="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
|
<!-- Search history text -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/history_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="end" />
|
||||||
|
|
||||||
|
<!-- Delete icon -->
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/delete_icon"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:src="@drawable/revanced_settings_search_remove"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:padding="4dp"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
android:tint="?android:attr/textColorSecondary"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- Scrollable content -->
|
||||||
|
<ScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fillViewport="true">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<!-- Empty history message (hidden by default) -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/empty_history_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/revanced_settings_search_empty_history_title"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/empty_history_summary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/revanced_settings_search_empty_history_summary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
<!-- History header -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/search_history_header"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/revanced_settings_search_recent_searches"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false" />
|
||||||
|
|
||||||
|
<!-- History list -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/search_history_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
|
||||||
|
<!-- Clear history button -->
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/clear_history_button"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:text="@string/revanced_settings_search_clear_history"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:textColor="?android:attr/colorAccent"
|
||||||
|
android:paddingStart="20dp"
|
||||||
|
android:paddingEnd="20dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true" />
|
||||||
|
|
||||||
|
<!-- Search Tips -->
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/search_tips_card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginStart="20dp"
|
||||||
|
android:layout_marginEnd="20dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false">
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/revanced_settings_search_tips_title"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/revanced_settings_search_tips_summary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:textColor="?android:attr/textColorSecondary" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</ScrollView>
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingTop="16dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@android:id/icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_marginHorizontal="18dp"
|
||||||
|
android:contentDescription="@null" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/preference_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/preference_summary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="10"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:minHeight="48dp"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/preference_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/preference_summary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="10"
|
||||||
|
android:visibility="gone" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Include color dot layout -->
|
||||||
|
<include
|
||||||
|
layout="@layout/revanced_color_dot_widget"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="16dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/preference_path"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp"
|
||||||
|
android:paddingStart="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textColor="?android:attr/textColorTertiary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textStyle="italic"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true" />
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:minHeight="48dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/preference_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/preference_summary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="10"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:minHeight="48dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/preference_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textSize="16sp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="2" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/preference_summary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||||
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
android:textSize="14sp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="10"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user