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:
MarcaD
2025-09-21 16:19:29 +03:00
committed by GitHub
parent ebb446b22a
commit ece8076f7c
104 changed files with 6066 additions and 3453 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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].

View File

@@ -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.

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(

View File

@@ -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,

View File

@@ -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()

View File

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

View File

@@ -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() {

View File

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

View File

@@ -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);
}
} }
/** /**

View File

@@ -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).

View File

@@ -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) {

View File

@@ -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.

View File

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

View File

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

View File

@@ -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.

View File

@@ -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();

View File

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

View File

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

View File

@@ -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.

View File

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

View File

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

View File

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

View File

@@ -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.

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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()) {

View File

@@ -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() {

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

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

View File

@@ -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(

View File

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

View File

@@ -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 {

View File

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

View File

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

View File

@@ -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();

View File

@@ -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()
} }

View File

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

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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"
) )
) )

View File

@@ -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",

View File

@@ -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>()

View File

@@ -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),

View File

@@ -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"),

View File

@@ -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.
*/ */

View File

@@ -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(

View File

@@ -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">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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" />

View File

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

View File

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