From 1e0e398574329173aff11a4dc9acfc3fcdeabe16 Mon Sep 17 00:00:00 2001 From: MarcaD <152095496+MarcaDian@users.noreply.github.com> Date: Mon, 26 May 2025 10:08:45 +0300 Subject: [PATCH] feat(YouTube - Settings): Add a color picker (#4981) Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> --- .../app/revanced/extension/shared/Utils.java | 27 +- .../preference/ColorPickerPreference.java | 442 ++++++++++++++++ .../settings/preference/ColorPickerView.java | 500 ++++++++++++++++++ .../preference/ReVancedAboutPreference.java | 3 +- .../app/revanced/extension/tiktok/Utils.java | 5 +- .../preference/DownloadPathPreference.java | 2 +- .../preference/InputTextPreference.java | 2 +- .../preference/RangeValuePreference.java | 2 +- .../ReVancedTikTokAboutPreference.java | 2 +- .../settings/preference/TogglePreference.java | 2 +- .../patches/NavigationButtonsPatch.java | 2 +- .../youtube/patches/WideSearchbarPatch.java | 5 +- .../patches/theme/ProgressBarDrawable.java | 5 +- .../patches/theme/SeekbarColorPatch.java | 2 +- .../ReturnYouTubeDislike.java | 13 +- .../youtube/settings/LicenseActivityHook.java | 4 +- .../ReVancedPreferenceFragment.java | 5 +- .../SegmentPlaybackController.java | 17 +- .../sponsorblock/objects/SegmentCategory.java | 45 +- .../SegmentCategoryListPreference.java | 180 +++++-- .../ui/SponsorBlockPreferenceGroup.java | 2 +- .../swipecontrols/SwipeControlsPatch.kt | 4 +- .../youtube/layout/theme/ThemePatch.kt | 11 +- .../youtube/misc/settings/SettingsPatch.kt | 3 + .../resources/addresources/values/strings.xml | 7 +- .../revanced_settings_circle_background.xml | 8 + .../layout/revanced_color_dot_widget.xml | 18 + .../settings/layout/revanced_color_picker.xml | 11 + 28 files changed, 1199 insertions(+), 130 deletions(-) create mode 100644 extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java create mode 100644 extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java create mode 100644 patches/src/main/resources/settings/drawable/revanced_settings_circle_background.xml create mode 100644 patches/src/main/resources/settings/layout/revanced_color_dot_widget.xml create mode 100644 patches/src/main/resources/settings/layout/revanced_color_picker.xml diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java index 46aba8191..dead738b4 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java @@ -23,6 +23,7 @@ import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; import android.util.Pair; +import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; @@ -557,14 +558,14 @@ public class Utils { ); } - public static boolean isDarkModeEnabled(Context context) { - Configuration config = context.getResources().getConfiguration(); + public static boolean isDarkModeEnabled() { + Configuration config = Resources.getSystem().getConfiguration(); final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; return currentNightMode == Configuration.UI_MODE_NIGHT_YES; } public static boolean isLandscapeOrientation() { - final int orientation = context.getResources().getConfiguration().orientation; + final int orientation = Resources.getSystem().getConfiguration().orientation; return orientation == Configuration.ORIENTATION_LANDSCAPE; } @@ -640,7 +641,11 @@ public class Utils { || networkType == NetworkType.OTHER; } - @SuppressLint({"MissingPermission", "deprecation"}) // Permission already included in YouTube. + /** + * Calling extension code must ensure the target app has the + * ACCESS_NETWORK_STATE app manifest permission. + */ + @SuppressWarnings({"deprecation", "MissingPermission"}) public static NetworkType getNetworkType() { Context networkContext = getContext(); if (networkContext == null) { @@ -852,6 +857,20 @@ public class Utils { return getResourceColor(colorString); } + /** + * Converts dip value to actual device pixels. + * + * @param dip The density-independent pixels value + * @return The device pixel value + */ + public static int dipToPixels(float dip) { + return (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dip, + Resources.getSystem().getDisplayMetrics() + ); + } + public static int clamp(int value, int lower, int upper) { return Math.max(lower, Math.min(value, upper)); } diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java new file mode 100644 index 000000000..e9f24ea61 --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerPreference.java @@ -0,0 +1,442 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.getResourceIdentifier; + +import android.app.AlertDialog; +import android.content.Context; +import android.graphics.Color; +import android.graphics.Typeface; +import android.os.Build; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.text.Editable; +import android.text.InputType; +import android.text.SpannableString; +import android.text.Spanned; +import android.text.TextWatcher; +import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.ColorInt; + +import java.util.Locale; +import java.util.regex.Pattern; + +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.StringSetting; + +/** + * A custom preference for selecting a color via a hexadecimal code or a color picker dialog. + * Extends {@link EditTextPreference} to display a colored dot in the widget area, + * reflecting the currently selected color. The dot is dimmed when the preference is disabled. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class ColorPickerPreference extends EditTextPreference { + + /** + * 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]"); + + /** + * Alpha for dimming when the preference is disabled. + */ + private static final float DISABLED_ALPHA = 0.5f; // 50% + + /** + * View displaying a colored dot in the widget area. + */ + private View widgetColorDot; + + /** + * Current color in RGB format (without alpha). + */ + @ColorInt + private int currentColor; + + /** + * Associated setting for storing the color value. + */ + private StringSetting colorSetting; + + /** + * Dialog TextWatcher for the EditText to monitor color input changes. + */ + private TextWatcher colorTextWatcher; + + /** + * Dialog TextView displaying a colored dot for the selected color preview in the dialog. + */ + private TextView dialogColorPreview; + + /** + * Dialog color picker view. + */ + private ColorPickerView dialogColorPickerView; + + /** + * Removes non valid hex characters, converts to all uppercase, + * and adds # character to the start if not present. + */ + public static String cleanupColorCodeString(String colorString) { + // Remove non-hex chars, convert to uppercase, and ensure correct length + String result = "#" + PATTERN_NOT_HEX.matcher(colorString) + .replaceAll("").toUpperCase(Locale.ROOT); + + if (result.length() < COLOR_STRING_LENGTH) { + return result; + } + + return result.substring(0, COLOR_STRING_LENGTH); + } + + /** + * @param color RGB color, without an alpha channel. + * @return #RRGGBB hex color string + */ + public static String getColorString(@ColorInt int color) { + String colorString = String.format("#%06X", color); + if ((color & 0xFF000000) != 0) { + // Likely a bug somewhere. + Logger.printException(() -> "getColorString: color has alpha channel: " + colorString); + } + return colorString; + } + + /** + * Creates a Spanned object for a colored dot using SpannableString. + * + * @param color The RGB color (without alpha). + * @return A Spanned object with the colored dot. + */ + public static Spanned getColorDot(@ColorInt int color) { + SpannableString spannable = new SpannableString(COLOR_DOT_STRING); + 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); + return spannable; + } + + public ColorPickerPreference(Context context) { + super(context); + init(); + } + + public ColorPickerPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + /** + * Initializes the preference by setting up the EditText, loading the color, and set the widget layout. + */ + private void init() { + colorSetting = (StringSetting) Setting.getSettingFromPath(getKey()); + if (colorSetting == null) { + Logger.printException(() -> "Could not find color setting for: " + getKey()); + } + + EditText editText = getEditText(); + editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + editText.setAutofillHints((String) null); + } + + // Set the widget layout to a custom layout containing the colored dot. + setWidgetLayoutResource(getResourceIdentifier("revanced_color_dot_widget", "layout")); + } + + /** + * 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 + public final void setText(String colorString) { + try { + Logger.printDebug(() -> "setText: " + colorString); + super.setText(colorString); + + currentColor = Color.parseColor(colorString) & 0x00FFFFFF; + if (colorSetting != null) { + colorSetting.save(getColorString(currentColor)); + } + updateColorPreview(); + updateWidgetColorDot(); + } catch (IllegalArgumentException ex) { + // This code is reached if the user pastes settings json with an invalid color + // since this preference is updated with the new setting text. + Logger.printDebug(() -> "Parse color error: " + colorString, ex); + Utils.showToastShort(str("revanced_settings_color_invalid")); + setText(colorSetting.resetToDefault()); + } catch (Exception ex) { + Logger.printException(() -> "setText failure: " + colorString, ex); + } + } + + @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); + } + + /** + * Creates a layout with a color preview and EditText for hex color input. + * + * @param context The context for creating the layout. + * @return A LinearLayout containing the color preview and EditText. + */ + private LinearLayout createDialogLayout(Context context) { + LinearLayout layout = new LinearLayout(context); + layout.setOrientation(LinearLayout.VERTICAL); + layout.setPadding(70, 0, 70, 0); + + // Inflate color picker. + View colorPicker = LayoutInflater.from(context).inflate( + getResourceIdentifier("revanced_color_picker", "layout"), null); + dialogColorPickerView = colorPicker.findViewById( + getResourceIdentifier("color_picker_view", "id")); + dialogColorPickerView.setColor(currentColor); + layout.addView(colorPicker); + + // Horizontal layout for preview and EditText. + LinearLayout inputLayout = new LinearLayout(context); + inputLayout.setOrientation(LinearLayout.HORIZONTAL); + inputLayout.setPadding(0, 20, 0, 0); + + dialogColorPreview = new TextView(context); + inputLayout.addView(dialogColorPreview); + updateColorPreview(); + + EditText editText = getEditText(); + ViewParent parent = editText.getParent(); + if (parent instanceof ViewGroup parentViewGroup) { + parentViewGroup.removeView(editText); + } + editText.setLayoutParams(new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT + )); + String currentColorString = getColorString(currentColor); + editText.setText(currentColorString); + editText.setSelection(currentColorString.length()); + editText.setTypeface(Typeface.MONOSPACE); + colorTextWatcher = createColorTextWatcher(dialogColorPickerView); + editText.addTextChangedListener(colorTextWatcher); + inputLayout.addView(editText); + + // Add a dummy view to take up remaining horizontal space, + // otherwise it will show an oversize underlined text view. + View paddingView = new View(context); + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + 0, + LinearLayout.LayoutParams.MATCH_PARENT, + 1f + ); + paddingView.setLayoutParams(params); + inputLayout.addView(paddingView); + + layout.addView(inputLayout); + + // Set up color picker listener with debouncing. + // Add listener last to prevent callbacks from set calls above. + dialogColorPickerView.setOnColorChangedListener(color -> { + // Check if it actually changed, since this callback + // can be caused by updates in afterTextChanged(). + if (currentColor == color) { + return; + } + + String updatedColorString = getColorString(color); + Logger.printDebug(() -> "onColorChanged: " + updatedColorString); + currentColor = color; + editText.setText(updatedColorString); + editText.setSelection(updatedColorString.length()); + + updateColorPreview(); + updateWidgetColorDot(); + }); + + return layout; + } + + /** + * 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. + * + * @return A TextWatcher that updates the color preview on valid input. + */ + private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) { + return 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 sanitizedColorString = cleanupColorCodeString(colorString); + if (!sanitizedColorString.equals(colorString)) { + edit.replace(0, colorString.length(), sanitizedColorString); + return; + } + + if (sanitizedColorString.length() != COLOR_STRING_LENGTH) { + // User is still typing out the color. + return; + } + + final int newColor = Color.parseColor(colorString); + if (currentColor != newColor) { + Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString); + currentColor = newColor; + updateColorPreview(); + updateWidgetColorDot(); + colorPickerView.setColor(newColor); + } + } catch (Exception ex) { + // Should never be reached since input is validated before using. + Logger.printException(() -> "afterTextChanged failure", ex); + } + } + }; + } + + /** + * Prepares the dialog builder with a custom view and reset button. + * + * @param builder The AlertDialog.Builder to configure. + */ + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + Utils.setEditTextDialogTheme(builder); + LinearLayout dialogLayout = createDialogLayout(builder.getContext()); + builder.setView(dialogLayout); + final int originalColor = currentColor; + + builder.setNeutralButton(str("revanced_settings_reset_color"), null); + + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + try { + String colorString = getEditText().getText().toString(); + + if (colorString.length() != COLOR_STRING_LENGTH) { + Utils.showToastShort(str("revanced_settings_color_invalid")); + setText(getColorString(originalColor)); + return; + } + + setText(colorString); + } catch (Exception ex) { + // Should never happen due to a bad color string, + // since the text is validated and fixed while the user types. + Logger.printException(() -> "setPositiveButton failure", ex); + } + }); + + builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> { + try { + // Restore the original color. + setText(getColorString(originalColor)); + } catch (Exception ex) { + Logger.printException(() -> "setNegativeButton failure", ex); + } + }); + } + + @Override + protected void showDialog(Bundle state) { + super.showDialog(state); + + AlertDialog dialog = (AlertDialog) getDialog(); + dialog.setCanceledOnTouchOutside(false); + + // Do not close dialog when reset is pressed. + Button button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + button.setOnClickListener(view -> { + 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(() -> "setOnClickListener failure", ex); + } + }); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (colorTextWatcher != null) { + getEditText().removeTextChangedListener(colorTextWatcher); + colorTextWatcher = null; + } + + dialogColorPreview = null; + dialogColorPickerView = null; + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + updateWidgetColorDot(); + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java new file mode 100644 index 000000000..cbec3349c --- /dev/null +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ColorPickerView.java @@ -0,0 +1,500 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.Utils.dipToPixels; +import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ComposeShader; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.RectF; +import android.graphics.Shader; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.ColorInt; + +import app.revanced.extension.shared.Logger; +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. + * This implementation is density-independent and responsive across different screen sizes and DPIs. + * + *

+ * This view displays two main components for color selection: + *

+ * + *

+ * The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar and the + * saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles). + * + *

+ * 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. + */ +public class ColorPickerView extends View { + + /** + * Interface definition for a callback to be invoked when the selected color changes. + */ + public interface OnColorChangedListener { + /** + * 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); + } + + /** Expanded touch area for the hue bar to increase the touch-sensitive area. */ + public static final float TOUCH_EXPANSION = dipToPixels(20f); + + private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24); + private static final float VIEW_PADDING = dipToPixels(16); + private static final float HUE_BAR_WIDTH = dipToPixels(12); + private static final float HUE_CORNER_RADIUS = dipToPixels(6); + private static final float SELECTOR_RADIUS = dipToPixels(12); + private static final float SELECTOR_STROKE_WIDTH = 8; + /** + * Hue 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. + */ + private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2; + /** Thin dark outline stroke width for the selector rings. */ + private static final float SELECTOR_EDGE_STROKE_WIDTH = 1; + public static final float SELECTOR_EDGE_RADIUS = + SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2; + + /** Selector outline inner color. */ + @ColorInt + private static final int SELECTOR_OUTLINE_COLOR = Color.WHITE; + + /** Dark edge color for the selector rings. */ + @ColorInt + private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF"); + + private static final int[] HUE_COLORS = new int[361]; + static { + for (int i = 0; i < 361; i++) { + HUE_COLORS[i] = Color.HSVToColor(new float[]{i, 1, 1}); + } + } + + /** Hue bar. */ + private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + /** Saturation-value selector. */ + private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + /** Draggable selector. */ + private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + { + selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH); + } + + /** Bounds of the hue bar. */ + private final RectF hueRect = new RectF(); + /** Bounds of the saturation-value selector. */ + private final RectF saturationValueRect = new RectF(); + + /** HSV color calculations to avoid allocations during drawing. */ + private final float[] hsvArray = {1, 1, 1}; + + /** Current hue value (0-360). */ + private float hue = 0f; + /** Current saturation value (0-1). */ + private float saturation = 1f; + /** Current value (brightness) value (0-1). */ + private float value = 1f; + + /** The currently selected color in RGB format with no alpha channel. */ + @ColorInt + private int selectedColor; + + private OnColorChangedListener colorChangedListener; + + /** Track if we're currently dragging the hue or saturation handle. */ + private boolean isDraggingHue; + private boolean isDraggingSaturation; + + public ColorPickerView(Context context) { + super(context); + } + + public ColorPickerView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8 + + final int minWidth = Utils.dipToPixels(250); + final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO); + + int width = resolveSize(minWidth, widthMeasureSpec); + int height = resolveSize(minHeight, heightMeasureSpec); + + // Ensure minimum dimensions for usability + width = Math.max(width, minWidth); + height = Math.max(height, minHeight); + + // Adjust height to maintain desired aspect ratio if possible + final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO); + if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) { + height = desiredHeight; + } + + setMeasuredDimension(width, height); + } + + /** + * Called when the size of the view changes. + * 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 + protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { + super.onSizeChanged(width, height, oldWidth, oldHeight); + + // Calculate bounds with hue bar on the right + final float effectiveWidth = width - (2 * VIEW_PADDING); + final float selectorWidth = effectiveWidth - HUE_BAR_WIDTH - MARGIN_BETWEEN_AREAS; + + // Adjust rectangles to account for padding and density-independent dimensions + saturationValueRect.set( + VIEW_PADDING, + VIEW_PADDING, + VIEW_PADDING + selectorWidth, + height - VIEW_PADDING + ); + + hueRect.set( + width - VIEW_PADDING - HUE_BAR_WIDTH, + VIEW_PADDING, + width - VIEW_PADDING, + height - VIEW_PADDING + ); + + // Update the shaders. + updateHueShader(); + updateSaturationValueShader(); + } + + /** + * Updates the hue full spectrum (0-360 degrees). + */ + private void updateHueShader() { + LinearGradient hueShader = new LinearGradient( + hueRect.left, hueRect.top, + hueRect.left, hueRect.bottom, + HUE_COLORS, + null, + Shader.TileMode.CLAMP + ); + + huePaint.setShader(hueShader); + } + + /** + * Updates the shader for the saturation-value selector based on the currently selected hue. + * This method creates a combined shader that blends a saturation gradient with a value gradient. + */ + private void updateSaturationValueShader() { + // Create a saturation-value gradient based on the current hue. + // Calculate the start color (white with the selected hue) for the saturation gradient. + final int startColor = Color.HSVToColor(new float[]{hue, 0f, 1f}); + + // Calculate the middle color (fully saturated color with the selected hue) for the saturation gradient. + final int midColor = Color.HSVToColor(new float[]{hue, 1f, 1f}); + + // Create a linear gradient for the saturation from startColor to midColor (horizontal). + LinearGradient satShader = new LinearGradient( + saturationValueRect.left, saturationValueRect.top, + saturationValueRect.right, saturationValueRect.top, + startColor, + midColor, + Shader.TileMode.CLAMP + ); + + // Create a linear gradient for the value (brightness) from white to black (vertical). + //noinspection ExtractMethodRecommender + LinearGradient valShader = new LinearGradient( + saturationValueRect.left, saturationValueRect.top, + saturationValueRect.left, saturationValueRect.bottom, + Color.WHITE, + Color.BLACK, + Shader.TileMode.CLAMP + ); + + // Combine the saturation and value shaders using PorterDuff.Mode.MULTIPLY to create the final color. + ComposeShader combinedShader = new ComposeShader(satShader, valShader, PorterDuff.Mode.MULTIPLY); + + // Set the combined shader for the saturation-value paint. + saturationValuePaint.setShader(combinedShader); + } + + /** + * Draws the color picker view on the canvas. + * 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 + protected void onDraw(Canvas canvas) { + // Draw the saturation-value selector rectangle. + canvas.drawRect(saturationValueRect, saturationValuePaint); + + // Draw the hue bar. + canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint); + + final float hueSelectorX = hueRect.centerX(); + final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height(); + + final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width(); + final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height(); + + // Draw the saturation and hue selector handle filled with the selected color. + hsvArray[0] = hue; + final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray); + selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE); + + selectorPaint.setColor(hueHandleColor); + canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint); + + selectorPaint.setColor(selectedColor | 0xFF000000); + canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint); + + // Draw white outlines for the handles. + selectorPaint.setColor(SELECTOR_OUTLINE_COLOR); + selectorPaint.setStyle(Paint.Style.STROKE); + selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH); + canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint); + canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint); + + // Draw thin dark outlines for the handles at the outer edge of the white outline. + selectorPaint.setColor(SELECTOR_EDGE_COLOR); + selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH); + canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint); + canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint); + } + + /** + * Handles touch events on the view. + * 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. + *

+ * 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. + * @return True if the event was handled, false otherwise. + */ + @SuppressLint("ClickableViewAccessibility") // performClick is not overridden, but not needed in this case. + @Override + public boolean onTouchEvent(MotionEvent event) { + try { + final float x = event.getX(); + final float y = event.getY(); + final int action = event.getAction(); + Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y); + + // Define touch expansion for the hue bar. + RectF expandedHueRect = new RectF( + hueRect.left - TOUCH_EXPANSION, + hueRect.top, + hueRect.right + TOUCH_EXPANSION, + hueRect.bottom + ); + + switch (action) { + case MotionEvent.ACTION_DOWN: + // Calculate current handle positions. + final float hueSelectorX = hueRect.centerX(); + final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height(); + + final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width(); + final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height(); + + // Create hit areas for both handles. + RectF hueHitRect = new RectF( + hueSelectorX - SELECTOR_RADIUS, + hueSelectorY - SELECTOR_RADIUS, + hueSelectorX + SELECTOR_RADIUS, + hueSelectorY + SELECTOR_RADIUS + ); + RectF satValHitRect = new RectF( + satSelectorX - SELECTOR_RADIUS, + valSelectorY - SELECTOR_RADIUS, + satSelectorX + SELECTOR_RADIUS, + valSelectorY + SELECTOR_RADIUS + ); + + // Check if the touch started on a handle or within the expanded hue bar area. + if (hueHitRect.contains(x, y)) { + isDraggingHue = true; + updateHueFromTouch(y); + } else if (satValHitRect.contains(x, y)) { + isDraggingSaturation = true; + updateSaturationValueFromTouch(x, y); + } else if (expandedHueRect.contains(x, y)) { + // Handle touch within the expanded hue bar area. + isDraggingHue = true; + updateHueFromTouch(y); + } else if (saturationValueRect.contains(x, y)) { + isDraggingSaturation = true; + updateSaturationValueFromTouch(x, y); + } + break; + + case MotionEvent.ACTION_MOVE: + // Continue updating values even if touch moves outside the view. + if (isDraggingHue) { + updateHueFromTouch(y); + } else if (isDraggingSaturation) { + updateSaturationValueFromTouch(x, y); + } + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + isDraggingHue = false; + isDraggingSaturation = false; + break; + } + } catch (Exception ex) { + Logger.printException(() -> "onTouchEvent failure", ex); + } + + return true; + } + + /** + * Updates the hue value based on touch position, clamping to valid range. + * + * @param y The y-coordinate of the touch position. + */ + private void updateHueFromTouch(float y) { + // Clamp y to the hue rectangle bounds. + final float clampedY = Utils.clamp(y, hueRect.top, hueRect.bottom); + final float updatedHue = ((clampedY - hueRect.top) / hueRect.height()) * 360f; + if (hue == updatedHue) { + return; + } + + hue = updatedHue; + updateSaturationValueShader(); + updateSelectedColor(); + } + + /** + * Updates saturation and value based on touch position, clamping to valid range. + * + * @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) { + // Clamp x and y to the saturation-value rectangle bounds. + final float clampedX = Utils.clamp(x, saturationValueRect.left, saturationValueRect.right); + final float clampedY = Utils.clamp(y, saturationValueRect.top, saturationValueRect.bottom); + + final float updatedSaturation = (clampedX - saturationValueRect.left) / saturationValueRect.width(); + final float updatedValue = 1 - ((clampedY - saturationValueRect.top) / saturationValueRect.height()); + + if (saturation == updatedSaturation && value == updatedValue) { + return; + } + saturation = updatedSaturation; + value = updatedValue; + updateSelectedColor(); + } + + /** + * Updates the selected color and notifies listeners. + */ + private void updateSelectedColor() { + final int updatedColor = Color.HSVToColor(0, new float[]{hue, saturation, value}); + + if (selectedColor != updatedColor) { + selectedColor = updatedColor; + + if (colorChangedListener != null) { + colorChangedListener.onColorChanged(updatedColor); + } + } + + // Must always redraw, otherwise if saturation is pure grey or black + // then the hue slider cannot be changed. + invalidate(); + } + + /** + * Sets the currently selected color. + * + * @param color The color to set in either ARGB or RGB format. + */ + public void setColor(@ColorInt int color) { + color &= 0x00FFFFFF; + if (selectedColor == color) { + return; + } + + // Update the selected color. + selectedColor = color; + Logger.printDebug(() -> "setColor: " + getColorString(selectedColor)); + + // Convert the ARGB color to HSV values. + float[] hsv = new float[3]; + Color.colorToHSV(color, hsv); + + // Update the hue, saturation, and value. + hue = hsv[0]; + saturation = hsv[1]; + value = hsv[2]; + + // Update the saturation-value shader based on the new hue. + updateSaturationValueShader(); + + // Notify the listener if it's set. + if (colorChangedListener != null) { + colorChangedListener.onColorChanged(selectedColor); + } + + // Invalidate the view to trigger a redraw. + invalidate(); + } + + /** + * Gets the currently selected color. + * + * @return The selected color in RGB format with no alpha channel. + */ + @ColorInt + public int getColor() { + return selectedColor; + } + + /** + * Sets the listener to be notified when the selected color changes. + * + * @param listener The listener to set. + */ + public void setOnColorChangedListener(OnColorChangedListener listener) { + colorChangedListener = listener; + } +} diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java index ba2facccb..161711737 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java @@ -8,7 +8,6 @@ import android.app.Dialog; import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; -import android.content.res.Configuration; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; @@ -54,7 +53,7 @@ public class ReVancedAboutPreference extends Preference { } protected boolean isDarkModeEnabled() { - return Utils.isDarkModeEnabled(getContext()); + return Utils.isDarkModeEnabled(); } /** diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/Utils.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/Utils.java index 63f4b0127..d1d0f089f 100644 --- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/Utils.java +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/Utils.java @@ -2,7 +2,6 @@ package app.revanced.extension.tiktok; import static app.revanced.extension.shared.Utils.isDarkModeEnabled; -import android.content.Context; import android.graphics.Color; import android.view.View; import android.widget.TextView; @@ -43,8 +42,8 @@ public class Utils { private static final @ColorInt int TEXT_LIGHT_MODE_SUMMARY = Color.argb(255, 80, 80, 80); - public static void setTitleAndSummaryColor(Context context, View view) { - final boolean darkModeEnabled = isDarkModeEnabled(context); + public static void setTitleAndSummaryColor(View view) { + final boolean darkModeEnabled = isDarkModeEnabled(); TextView title = view.findViewById(android.R.id.title); title.setTextColor(darkModeEnabled diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/DownloadPathPreference.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/DownloadPathPreference.java index fb4e27121..f387c68f5 100644 --- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/DownloadPathPreference.java +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/DownloadPathPreference.java @@ -101,7 +101,7 @@ public class DownloadPathPreference extends DialogPreference { protected void onBindView(View view) { super.onBindView(view); - Utils.setTitleAndSummaryColor(getContext(), view); + Utils.setTitleAndSummaryColor(view); } @Override diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/InputTextPreference.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/InputTextPreference.java index d2df31462..a4785c955 100644 --- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/InputTextPreference.java +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/InputTextPreference.java @@ -22,6 +22,6 @@ public class InputTextPreference extends EditTextPreference { protected void onBindView(View view) { super.onBindView(view); - Utils.setTitleAndSummaryColor(getContext(), view); + Utils.setTitleAndSummaryColor(view); } } diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/RangeValuePreference.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/RangeValuePreference.java index 012d334f0..a13438a90 100644 --- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/RangeValuePreference.java +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/RangeValuePreference.java @@ -127,7 +127,7 @@ public class RangeValuePreference extends DialogPreference { protected void onBindView(View view) { super.onBindView(view); - Utils.setTitleAndSummaryColor(getContext(), view); + Utils.setTitleAndSummaryColor(view); } @Override diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedTikTokAboutPreference.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedTikTokAboutPreference.java index 408bab6c0..7348ccc70 100644 --- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedTikTokAboutPreference.java +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedTikTokAboutPreference.java @@ -48,6 +48,6 @@ public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference { protected void onBindView(View view) { super.onBindView(view); - Utils.setTitleAndSummaryColor(getContext(), view); + Utils.setTitleAndSummaryColor(view); } } diff --git a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/TogglePreference.java b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/TogglePreference.java index f0e8085f0..7a78d504d 100644 --- a/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/TogglePreference.java +++ b/extensions/tiktok/src/main/java/app/revanced/extension/tiktok/settings/preference/TogglePreference.java @@ -22,6 +22,6 @@ public class TogglePreference extends SwitchPreference { protected void onBindView(View view) { super.onBindView(view); - Utils.setTitleAndSummaryColor(getContext(), view); + Utils.setTitleAndSummaryColor(view); } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationButtonsPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationButtonsPatch.java index d2e1d5f8b..7021cc6f9 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationButtonsPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/NavigationButtonsPatch.java @@ -95,7 +95,7 @@ public final class NavigationButtonsPatch { return false; } - return Utils.isDarkModeEnabled(Utils.getContext()) + return Utils.isDarkModeEnabled() ? !DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK : !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT; } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java index 90f18a6c2..2b07a2f73 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java @@ -1,7 +1,5 @@ package app.revanced.extension.youtube.patches; -import android.content.res.Resources; -import android.util.TypedValue; import android.view.View; import app.revanced.extension.shared.Logger; @@ -33,8 +31,7 @@ public final class WideSearchbarPatch { final int paddingRight = searchBarView.getPaddingRight(); final int paddingTop = searchBarView.getPaddingTop(); final int paddingBottom = searchBarView.getPaddingBottom(); - final int paddingStart = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, - 8, Resources.getSystem().getDisplayMetrics()); + final int paddingStart = Utils.dipToPixels(8); if (Utils.isRightToLeftLocale()) { searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/ProgressBarDrawable.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/ProgressBarDrawable.java index bf0284f79..93d53c9a5 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/ProgressBarDrawable.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/ProgressBarDrawable.java @@ -20,13 +20,16 @@ import app.revanced.extension.youtube.settings.Settings; public class ProgressBarDrawable extends Drawable { private final Paint paint = new Paint(); + { + paint.setColor(SeekbarColorPatch.getSeekbarColor()); + } @Override public void draw(@NonNull Canvas canvas) { if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) { return; } - paint.setColor(SeekbarColorPatch.getSeekbarColor()); + canvas.drawRect(getBounds(), paint); } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java index 4d036509e..1489ffd51 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java @@ -179,7 +179,7 @@ public final class SeekbarColorPatch { //noinspection ConstantConditions if (false) { // Set true to force slow animation for development. final int longAnimation = Utils.getResourceIdentifier( - Utils.isDarkModeEnabled(Utils.getContext()) + Utils.isDarkModeEnabled() ? "startup_animation_5s_30fps_dark" : "startup_animation_5s_30fps_light", "raw"); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java index a0730c055..2a3c5a07c 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java @@ -21,8 +21,6 @@ import android.text.Spanned; import android.text.style.ForegroundColorSpan; import android.text.style.ImageSpan; import android.text.style.ReplacementSpan; -import android.util.DisplayMetrics; -import android.util.TypedValue; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; @@ -120,16 +118,13 @@ public class ReturnYouTubeDislike { private static final ShapeDrawable leftSeparatorShape; static { - DisplayMetrics dp = Objects.requireNonNull(Utils.getContext()).getResources().getDisplayMetrics(); - leftSeparatorBounds = new Rect(0, 0, - (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp), - (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dp)); - final int middleSeparatorSize = - (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp); + Utils.dipToPixels(1.2f), + Utils.dipToPixels(14f)); + final int middleSeparatorSize = Utils.dipToPixels(3.7f); middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize); - leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.4f, dp); + leftSeparatorShapePaddingPixels = Utils.dipToPixels(8.4f); leftSeparatorShape = new ShapeDrawable(new RectShape()); leftSeparatorShape.setBounds(leftSeparatorBounds); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java index 5a14ca39c..a6335a5e3 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java @@ -6,7 +6,6 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.preference.PreferenceFragment; -import android.util.TypedValue; import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toolbar; @@ -119,8 +118,7 @@ public class LicenseActivityHook { toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable()); toolbar.setTitle(getResourceIdentifier("revanced_settings_title", "string")); - final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, - Utils.getContext().getResources().getDisplayMetrics()); + final int margin = Utils.dipToPixels(16); toolbar.setTitleMarginStart(margin); toolbar.setTitleMarginEnd(margin); TextView toolbarTextView = Utils.getChildView(toolbar, false, diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java index 916378bf5..1257bb190 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java @@ -17,7 +17,6 @@ import android.preference.SwitchPreference; import android.text.SpannableStringBuilder; import android.text.TextUtils; import android.text.style.BackgroundColorSpan; -import android.util.TypedValue; import android.view.ViewGroup; import android.view.WindowInsets; import android.widget.TextView; @@ -245,9 +244,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment { toolbar.setNavigationIcon(getBackButtonDrawable()); toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss()); - final int margin = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics() - ); + final int margin = Utils.dipToPixels(16); toolbar.setTitleMargin(margin, 0, margin, 0); TextView toolbarTextView = Utils.getChildView(toolbar, diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java index 3f48930e3..22259d571 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java @@ -5,7 +5,6 @@ import static app.revanced.extension.shared.StringRef.str; import android.graphics.Canvas; import android.graphics.Rect; import android.text.TextUtils; -import android.util.TypedValue; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -727,15 +726,11 @@ public class SegmentPlaybackController { } } - private static int highlightSegmentTimeBarScreenWidth = -1; // actual pixel width to use - private static int getHighlightSegmentTimeBarScreenWidth() { - if (highlightSegmentTimeBarScreenWidth == -1) { - highlightSegmentTimeBarScreenWidth = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH, - Objects.requireNonNull(Utils.getContext()).getResources().getDisplayMetrics()); - } - return highlightSegmentTimeBarScreenWidth; - } + /** + * Actual screen pixel width to use for the highlight segment time bar. + */ + private static final int highlightSegmentTimeBarScreenWidth + = Utils.dipToPixels(HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH); /** * Injection point. @@ -757,7 +752,7 @@ public class SegmentPlaybackController { final float left = leftPadding + segment.start * videoMillisecondsToPixels; final float right; if (segment.category == SegmentCategory.HIGHLIGHT) { - right = left + getHighlightSegmentTimeBarScreenWidth(); + right = left + highlightSegmentTimeBarScreenWidth; } else { right = leftPadding + segment.end * videoMillisecondsToPixels; } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java index ec1b1fca5..ae6900330 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java @@ -1,6 +1,7 @@ package app.revanced.extension.youtube.sponsorblock.objects; 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 android.graphics.Color; @@ -9,7 +10,9 @@ import android.text.Spannable; import android.text.SpannableString; import android.text.TextUtils; import android.text.style.ForegroundColorSpan; +import android.text.style.RelativeSizeSpan; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -134,7 +137,8 @@ public enum SegmentCategory { updateEnabledCategories(); } - public static int applyOpacityToColor(int color, float opacity) { + @ColorInt + public static int applyOpacityToColor(@ColorInt int color, float opacity) { if (opacity < 0 || opacity > 1.0f) { throw new IllegalArgumentException("Invalid opacity: " + opacity); } @@ -165,29 +169,28 @@ public enum SegmentCategory { /** * Skipped segment toast, if the skip occurred in the first quarter of the video */ - @NonNull public final StringRef skippedToastBeginning; /** * Skipped segment toast, if the skip occurred in the middle half of the video */ - @NonNull public final StringRef skippedToastMiddle; /** * Skipped segment toast, if the skip occurred in the last quarter of the video */ - @NonNull public final StringRef skippedToastEnd; - @NonNull public final Paint paint; + /** + * Category color with opacity applied. + */ + @ColorInt private int color; /** * Value must be changed using {@link #setBehaviour(CategoryBehaviour)}. * Caller must also {@link #updateEnabledCategories()}. */ - @NonNull public CategoryBehaviour behaviour = CategoryBehaviour.IGNORE; SegmentCategory(String keyValue, StringRef title, StringRef description, @@ -247,7 +250,7 @@ public enum SegmentCategory { } } - public void setBehaviour(@NonNull CategoryBehaviour behaviour) { + public void setBehaviour(CategoryBehaviour behaviour) { this.behaviour = Objects.requireNonNull(behaviour); this.behaviorSetting.save(behaviour.reVancedKeyValue); } @@ -273,6 +276,10 @@ public enum SegmentCategory { return opacitySetting.get(); } + public float getOpacityDefault() { + return opacitySetting.defaultValue; + } + public void resetColorAndOpacity() { setColor(colorSetting.defaultValue); setOpacity(opacitySetting.defaultValue); @@ -291,10 +298,19 @@ public enum SegmentCategory { /** * @return Integer color of #RRGGBB format. */ + @ColorInt public int getColorNoOpacity() { return color & 0x00FFFFFF; } + /** + * @return Integer color of #RRGGBB format. + */ + @ColorInt + public int getColorNoOpacityDefault() { + return Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF; + } + /** * @return Hex color string of #RRGGBB format with no opacity level. */ @@ -302,22 +318,27 @@ public enum SegmentCategory { return String.format(Locale.US, "#%06X", getColorNoOpacity()); } - private static SpannableString getCategoryColorDotSpan(String text, int color) { - SpannableString dotSpan = new SpannableString('⬤' + text); + 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(int color) { - return getCategoryColorDotSpan("", color); + 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(int categoryColor) { + public SpannableString getTitleWithColorDot(@ColorInt int categoryColor) { return getCategoryColorDotSpan(" " + title, categoryColor); } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java index 36204319c..8c2a98757 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java @@ -1,35 +1,46 @@ 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.settings.preference.ColorPickerPreference.getColorString; import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; 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.view.LayoutInflater; +import android.view.View; +import android.widget.Button; import android.widget.EditText; import android.widget.GridLayout; +import android.widget.LinearLayout; import android.widget.TextView; +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; - private TextView colorDotView; - private EditText colorEditText; - private EditText opacityEditText; + /** - * #RRGGBB + * RGB format (no alpha). */ + @ColorInt private int categoryColor; /** * [0, 1] @@ -37,6 +48,11 @@ public class SegmentCategoryListPreference extends ListPreference { private float categoryOpacity; private int selectedDialogEntryIndex; + private TextView dialogColorDotView; + private EditText dialogColorEditText; + private EditText dialogOpacityEditText; + private ColorPickerView dialogColorPickerView; + public SegmentCategoryListPreference(Context context, SegmentCategory category) { super(context); this.category = Objects.requireNonNull(category); @@ -67,8 +83,20 @@ public class SegmentCategoryListPreference extends ListPreference { categoryOpacity = category.getOpacity(); Context context = builder.getContext(); + LinearLayout mainLayout = new LinearLayout(context); + mainLayout.setOrientation(LinearLayout.VERTICAL); + mainLayout.setPadding(70, 0, 70, 0); + + // Inflate the color picker view. + View colorPickerContainer = LayoutInflater.from(context) + .inflate(getResourceIdentifier("revanced_color_picker", "layout"), null); + dialogColorPickerView = colorPickerContainer.findViewById( + getResourceIdentifier("color_picker_view", "id")); + dialogColorPickerView.setColor(categoryColor); + mainLayout.addView(colorPickerContainer); + + // Grid layout for color and opacity inputs. GridLayout gridLayout = new GridLayout(context); - gridLayout.setPadding(70, 0, 150, 0); // Padding for the entire layout. gridLayout.setColumnCount(3); gridLayout.setRowCount(2); @@ -84,19 +112,22 @@ public class SegmentCategoryListPreference extends ListPreference { gridParams.rowSpec = GridLayout.spec(0); // First row. gridParams.columnSpec = GridLayout.spec(1); // Second column. gridParams.setMargins(0, 0, 10, 0); - colorDotView = new TextView(context); - colorDotView.setLayoutParams(gridParams); - gridLayout.addView(colorDotView); + 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. - colorEditText = new EditText(context); - colorEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS); - colorEditText.setTextLocale(Locale.US); - colorEditText.setText(category.getColorString()); - colorEditText.addTextChangedListener(new TextWatcher() { + 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) { } @@ -109,28 +140,30 @@ public class SegmentCategoryListPreference extends ListPreference { public void afterTextChanged(Editable edit) { try { String colorString = edit.toString(); - final int colorStringLength = colorString.length(); + String normalizedColorString = ColorPickerPreference.cleanupColorCodeString(colorString); - if (!colorString.startsWith("#")) { - edit.insert(0, "#"); // Recursively calls back into this method. + if (!normalizedColorString.equals(colorString)) { + edit.replace(0, colorString.length(), normalizedColorString); return; } - final int maxColorStringLength = 7; // #RRGGBB - if (colorStringLength > maxColorStringLength) { - edit.delete(maxColorStringLength, colorStringLength); + if (normalizedColorString.length() != ColorPickerPreference.COLOR_STRING_LENGTH) { + // User is still typing out the color. return; } - categoryColor = Color.parseColor(colorString); - updateCategoryColorDot(); - } catch (IllegalArgumentException ex) { - // Ignore. + // 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); } } }); - colorEditText.setLayoutParams(gridParams); - gridLayout.addView(colorEditText); + dialogColorEditText.setLayoutParams(gridParams); + gridLayout.addView(dialogColorEditText); gridParams = new GridLayout.LayoutParams(); gridParams.rowSpec = GridLayout.spec(1); // Second row. @@ -143,9 +176,13 @@ public class SegmentCategoryListPreference extends ListPreference { gridParams = new GridLayout.LayoutParams(); gridParams.rowSpec = GridLayout.spec(1); // Second row. gridParams.columnSpec = GridLayout.spec(2); // Third column. - opacityEditText = new EditText(context); - opacityEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); - opacityEditText.addTextChangedListener(new TextWatcher() { + 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) { } @@ -183,31 +220,40 @@ public class SegmentCategoryListPreference extends ListPreference { } updateCategoryColorDot(); - } catch (NumberFormatException ex) { + } catch (Exception ex) { // Should never happen. - Logger.printException(() -> "Could not parse opacity string", ex); + Logger.printException(() -> "opacityEditText afterTextChanged failure", ex); } } }); - opacityEditText.setLayoutParams(gridParams); - gridLayout.addView(opacityEditText); + dialogOpacityEditText.setLayoutParams(gridParams); + gridLayout.addView(dialogOpacityEditText); updateOpacityText(); - builder.setView(gridLayout); + mainLayout.addView(gridLayout); + + // 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()); + }); + + builder.setView(mainLayout); builder.setTitle(category.title.toString()); builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { onClick(dialog, DialogInterface.BUTTON_POSITIVE); }); - builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> { - try { - category.resetColorAndOpacity(); - updateUI(); - Utils.showToastShort(str("revanced_sb_color_reset")); - } catch (Exception ex) { - Logger.printException(() -> "setNeutralButton failure", ex); - } - }); + builder.setNeutralButton(str("revanced_settings_reset_color"), null); builder.setNegativeButton(android.R.string.cancel, null); selectedDialogEntryIndex = findIndexOfValue(getValue()); @@ -218,6 +264,25 @@ public class SegmentCategoryListPreference extends ListPreference { } } + @Override + protected void showDialog(Bundle state) { + super.showDialog(state); + + // Do not close dialog when reset is pressed. + Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL); + button.setOnClickListener(view -> { + try { + // Setting view color causes callback to update the UI. + dialogColorPickerView.setColor(category.getColorNoOpacityDefault()); + + categoryOpacity = category.getOpacityDefault(); + updateOpacityText(); + } catch (Exception ex) { + Logger.printException(() -> "setOnClickListener failure", ex); + } + }); + } + @Override protected void onDialogClosed(boolean positiveResult) { try { @@ -230,43 +295,42 @@ public class SegmentCategoryListPreference extends ListPreference { } try { - String colorString = colorEditText.getText().toString(); - if (!colorString.equals(category.getColorString()) || categoryOpacity != category.getOpacity()) { - category.setColor(colorString); - category.setOpacity(categoryOpacity); - Utils.showToastShort(str("revanced_sb_color_changed")); - } + category.setColor(dialogColorEditText.getText().toString()); + category.setOpacity(categoryOpacity); } catch (IllegalArgumentException ex) { - Utils.showToastShort(str("revanced_sb_color_invalid")); + Utils.showToastShort(str("revanced_settings_color_invalid")); } updateUI(); } } catch (Exception ex) { Logger.printException(() -> "onDialogClosed failure", ex); + } finally { + dialogColorDotView = null; + dialogColorEditText = null; + dialogOpacityEditText = null; + dialogColorPickerView = null; } } - private void applyOpacityToCategoryColor() { - categoryColor = applyOpacityToColor(categoryColor, categoryOpacity); + @ColorInt + private int applyOpacityToCategoryColor() { + return applyOpacityToColor(categoryColor, categoryOpacity); } public void updateUI() { categoryColor = category.getColorNoOpacity(); categoryOpacity = category.getOpacity(); - applyOpacityToCategoryColor(); - setTitle(category.getTitleWithColorDot(categoryColor)); + setTitle(category.getTitleWithColorDot(applyOpacityToCategoryColor())); } private void updateCategoryColorDot() { - applyOpacityToCategoryColor(); - - colorDotView.setText(SegmentCategory.getCategoryColorDot(categoryColor)); + dialogColorDotView.setText(SegmentCategory.getCategoryColorDot(applyOpacityToCategoryColor())); } private void updateOpacityText() { - opacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity)); + dialogOpacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity)); } @Override @@ -277,4 +341,4 @@ public class SegmentCategoryListPreference extends ListPreference { // This is required otherwise the ReVanced preference fragment // sets all ListPreference summaries to show the current selection. } -} \ No newline at end of file +} diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java index 4ed3bf238..1cf2e0444 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockPreferenceGroup.java @@ -421,7 +421,7 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup { .setTitle(apiUrl.getTitle()) .setView(editText) .setNegativeButton(android.R.string.cancel, null) - .setNeutralButton(str("revanced_sb_reset"), urlChangeListener) + .setNeutralButton(str("revanced_settings_reset"), urlChangeListener) .setPositiveButton(android.R.string.ok, urlChangeListener) .show(); return true; diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt index 2c55658fb..6c14a93b6 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt @@ -48,7 +48,9 @@ private val swipeControlsResourcePatch = resourcePatch { summaryKey = null, ), TextPreference("revanced_swipe_overlay_background_opacity", inputType = InputType.NUMBER), - TextPreference("revanced_swipe_overlay_progress_color", inputType = InputType.TEXT_CAP_CHARACTERS), + TextPreference("revanced_swipe_overlay_progress_color", + tag = "app.revanced.extension.shared.settings.preference.ColorPickerPreference", + inputType = InputType.TEXT_CAP_CHARACTERS), TextPreference("revanced_swipe_text_overlay_size", inputType = InputType.NUMBER), TextPreference("revanced_swipe_overlay_timeout", inputType = InputType.NUMBER), TextPreference("revanced_swipe_threshold", inputType = InputType.NUMBER), diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt index 73e84d659..514eae710 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt @@ -89,14 +89,15 @@ val themePatch = bytecodePatch( execute { val preferences = mutableSetOf( SwitchPreference("revanced_seekbar_custom_color"), - TextPreference("revanced_seekbar_custom_color_primary", inputType = InputType.TEXT_CAP_CHARACTERS), + TextPreference("revanced_seekbar_custom_color_primary", + tag = "app.revanced.extension.shared.settings.preference.ColorPickerPreference", + inputType = InputType.TEXT_CAP_CHARACTERS), ) if (is_19_25_or_greater) { - preferences += TextPreference( - "revanced_seekbar_custom_color_accent", - inputType = InputType.TEXT_CAP_CHARACTERS - ) + preferences += TextPreference("revanced_seekbar_custom_color_accent", + tag = "app.revanced.extension.shared.settings.preference.ColorPickerPreference", + inputType = InputType.TEXT_CAP_CHARACTERS) } PreferenceScreen.SEEKBAR.addPreferences( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt index 06a28e7f3..1cd74f52c 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt @@ -74,6 +74,7 @@ private val settingsResourcePatch = resourcePatch { arrayOf( ResourceGroup("drawable", + "revanced_settings_circle_background.xml", "revanced_settings_cursor.xml", "revanced_settings_icon.xml", "revanced_settings_screen_00_about.xml", @@ -91,6 +92,8 @@ private val settingsResourcePatch = resourcePatch { "revanced_settings_screen_12_video.xml", ), ResourceGroup("layout", + "revanced_color_dot_widget.xml", + "revanced_color_picker.xml", "revanced_preference_with_icon_no_search_result.xml", "revanced_search_suggestion_item.xml", "revanced_settings_with_toolbar.xml"), diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index ede128926..d59675088 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -35,6 +35,8 @@ Second \"item\" text" ReVanced Do you wish to proceed? Reset + Reset color + Invalid color Refresh and restart Restart Import @@ -1161,11 +1163,6 @@ Ready to submit?" %s seconds Opacity: Color: - Color changed - Color reset - Invalid color code - Reset color - Reset About sponsor.ajay.app Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms diff --git a/patches/src/main/resources/settings/drawable/revanced_settings_circle_background.xml b/patches/src/main/resources/settings/drawable/revanced_settings_circle_background.xml new file mode 100644 index 000000000..d0c8a7ae4 --- /dev/null +++ b/patches/src/main/resources/settings/drawable/revanced_settings_circle_background.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/patches/src/main/resources/settings/layout/revanced_color_dot_widget.xml b/patches/src/main/resources/settings/layout/revanced_color_dot_widget.xml new file mode 100644 index 000000000..ea61afb8d --- /dev/null +++ b/patches/src/main/resources/settings/layout/revanced_color_dot_widget.xml @@ -0,0 +1,18 @@ + + + + diff --git a/patches/src/main/resources/settings/layout/revanced_color_picker.xml b/patches/src/main/resources/settings/layout/revanced_color_picker.xml new file mode 100644 index 000000000..0c8a3db85 --- /dev/null +++ b/patches/src/main/resources/settings/layout/revanced_color_picker.xml @@ -0,0 +1,11 @@ + + + + +