From ebee07ec3aba6fd3adbd8e0af30390e197879d89 Mon Sep 17 00:00:00 2001 From: MarcaD <152095496+MarcaDian@users.noreply.github.com> Date: Wed, 23 Apr 2025 14:30:41 +0300 Subject: [PATCH] feat(YouTube - Swipe controls): Add option for vertical progress bar (#4811) --- .../extension/shared/settings/Setting.java | 5 +- .../CustomPlayerOverlayOpacityPatch.java | 3 +- .../youtube/patches/MiniplayerPatch.java | 3 +- .../speed/CustomPlaybackSpeedPatch.java | 4 +- .../extension/youtube/settings/Settings.java | 7 +- .../SwipeControlsConfigurationProvider.kt | 162 ++++-- .../SwipeControlsHostActivity.kt | 4 +- .../views/SwipeControlsOverlayLayout.kt | 466 ++++++++++++++---- .../swipecontrols/SwipeControlsPatch.kt | 9 +- .../resources/addresources/values/arrays.xml | 21 + .../resources/addresources/values/strings.xml | 21 +- 11 files changed, 536 insertions(+), 169 deletions(-) diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java index 2f6405d6b..8294fe423 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -342,9 +342,12 @@ public abstract class Setting { /** * Identical to calling {@link #save(Object)} using {@link #defaultValue}. + * + * @return The newly saved default value. */ - public void resetToDefault() { + public T resetToDefault() { save(defaultValue); + return defaultValue; } /** diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CustomPlayerOverlayOpacityPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CustomPlayerOverlayOpacityPatch.java index 4f13deaac..50133579d 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CustomPlayerOverlayOpacityPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/CustomPlayerOverlayOpacityPatch.java @@ -17,8 +17,7 @@ public class CustomPlayerOverlayOpacityPatch { if (opacity < 0 || opacity > 100) { Utils.showToastLong(str("revanced_player_overlay_opacity_invalid_toast")); - Settings.PLAYER_OVERLAY_OPACITY.resetToDefault(); - opacity = Settings.PLAYER_OVERLAY_OPACITY.defaultValue; + opacity = Settings.PLAYER_OVERLAY_OPACITY.resetToDefault(); } PLAYER_OVERLAY_OPACITY_LEVEL = (opacity * 255) / 100; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java index 1b41e44c6..7a7571162 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java @@ -162,8 +162,7 @@ public final class MiniplayerPatch { if (opacity < 0 || opacity > 100) { Utils.showToastLong(str("revanced_miniplayer_opacity_invalid_toast")); - Settings.MINIPLAYER_OPACITY.resetToDefault(); - opacity = Settings.MINIPLAYER_OPACITY.defaultValue; + opacity = Settings.MINIPLAYER_OPACITY.resetToDefault(); } OPACITY_LEVEL = (opacity * 255) / 100; diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java index ae2a09f92..8cde513bd 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java @@ -54,12 +54,12 @@ public class CustomPlaybackSpeedPatch { static { final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get(); + if (holdSpeed > 0 && holdSpeed <= PLAYBACK_SPEED_MAXIMUM) { TAP_AND_HOLD_SPEED = holdSpeed; } else { showInvalidCustomSpeedToast(); - Settings.SPEED_TAP_AND_HOLD.resetToDefault(); - TAP_AND_HOLD_SPEED = Settings.SPEED_TAP_AND_HOLD.get(); + TAP_AND_HOLD_SPEED = Settings.SPEED_TAP_AND_HOLD.resetToDefault(); } loadCustomSpeeds(); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java index c49c6995e..0c48df66f 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -25,6 +25,7 @@ import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehavi import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP; import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY; import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE; +import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle; import android.graphics.Color; @@ -323,12 +324,14 @@ public class Settings extends BaseSettings { public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_threshold", 30, true, parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); public static final IntegerSetting SWIPE_VOLUME_SENSITIVITY = new IntegerSetting("revanced_swipe_volume_sensitivity", 1, true, parent(SWIPE_VOLUME)); - public static final BooleanSetting SWIPE_SHOW_CIRCULAR_OVERLAY = new BooleanSetting("revanced_swipe_show_circular_overlay", FALSE, true, + public static final EnumSetting SWIPE_OVERLAY_STYLE = new EnumSetting<>("revanced_swipe_overlay_style", SwipeOverlayStyle.HORIZONTAL,true, parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); - public static final BooleanSetting SWIPE_OVERLAY_MINIMAL_STYLE = new BooleanSetting("revanced_swipe_overlay_minimal_style", FALSE, true, + public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_text_overlay_size", 14, true, parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); public static final IntegerSetting SWIPE_OVERLAY_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true, parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); + public static final StringSetting SWIPE_OVERLAY_PROGRESS_COLOR = new StringSetting("revanced_swipe_overlay_progress_color", "#FFFFFF", true, + parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); public static final BooleanSetting SWIPE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_swipe_save_and_restore_brightness", TRUE, true, parent(SWIPE_BRIGHTNESS)); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt index 697926244..e8f181936 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt @@ -1,95 +1,98 @@ package app.revanced.extension.youtube.swipecontrols +import android.annotation.SuppressLint import android.graphics.Color +import app.revanced.extension.shared.Logger import app.revanced.extension.shared.StringRef.str import app.revanced.extension.shared.Utils import app.revanced.extension.youtube.settings.Settings import app.revanced.extension.youtube.shared.PlayerType /** - * provider for configuration for volume and brightness swipe controls + * Provides configuration settings for volume and brightness swipe controls in the YouTube player. + * Manages enabling/disabling gestures, overlay appearance, and behavior preferences. */ class SwipeControlsConfigurationProvider { -//region swipe enable + //region swipe enable /** - * should swipe controls be enabled? (global setting) + * Indicates whether swipe controls are enabled globally. + * Returns true if either volume or brightness controls are enabled and the video is in fullscreen mode. */ val enableSwipeControls: Boolean get() = (enableVolumeControls || enableBrightnessControl) && isFullscreenVideo /** - * should swipe controls for volume be enabled? + * Indicates whether swipe controls for adjusting volume are enabled. */ val enableVolumeControls = Settings.SWIPE_VOLUME.get() /** - * should swipe controls for volume be enabled? + * Indicates whether swipe controls for adjusting brightness are enabled. */ val enableBrightnessControl = Settings.SWIPE_BRIGHTNESS.get() /** - * is the video player currently in fullscreen mode? + * Checks if the video player is currently in fullscreen mode. */ private val isFullscreenVideo: Boolean get() = PlayerType.current == PlayerType.WATCH_WHILE_FULLSCREEN -//endregion + //endregion -//region keys enable + //region keys enable /** - * should volume key controls be overwritten? (global setting) + * Indicates whether volume key controls should be overridden by swipe controls. + * Returns true if volume controls are enabled and the video is in fullscreen mode. */ val overwriteVolumeKeyControls: Boolean get() = enableVolumeControls && isFullscreenVideo -//endregion + //endregion -//region gesture adjustments + //region gesture adjustments /** - * should press-to-swipe be enabled? + * Indicates whether press-to-swipe mode is enabled, requiring a press before swiping to activate controls. */ val shouldEnablePressToSwipe: Boolean get() = Settings.SWIPE_PRESS_TO_ENGAGE.get() /** - * threshold for swipe detection - * this may be called rapidly in onScroll, so we have to load it once and then leave it constant + * The threshold for detecting swipe gestures, in pixels. + * Loaded once to ensure consistent behavior during rapid scroll events. */ val swipeMagnitudeThreshold: Int get() = Settings.SWIPE_MAGNITUDE_THRESHOLD.get() /** - * How much volume will change by single swipe. - * If it is set to 0, it will reset to the default value because 0 would disable swiping. - * */ + * The sensitivity of volume swipe gestures, determining how much volume changes per swipe. + * Resets to default if set to 0, as it would disable swiping. + */ val volumeSwipeSensitivity: Int get() { val sensitivity = Settings.SWIPE_VOLUME_SENSITIVITY.get() if (sensitivity < 1) { - Settings.SWIPE_VOLUME_SENSITIVITY.resetToDefault() - - return Settings.SWIPE_VOLUME_SENSITIVITY.get() + return Settings.SWIPE_VOLUME_SENSITIVITY.resetToDefault() } return sensitivity } -//endregion + //endregion -//region overlay adjustments + //region overlay adjustments /** - * should the overlay enable haptic feedback? + * Indicates whether haptic feedback should be enabled for swipe control interactions. */ val shouldEnableHapticFeedback: Boolean get() = Settings.SWIPE_HAPTIC_FEEDBACK.get() /** - * how long the overlay should be shown on changes + * The duration in milliseconds that the overlay should remain visible after a change. */ val overlayShowTimeoutMillis: Long get() = Settings.SWIPE_OVERLAY_TIMEOUT.get() /** - * Gets the opacity value (0-100%) is converted to an alpha value (0-255) for transparency. - * If the opacity value is out of range, it resets to the default and displays a warning message. + * The background opacity of the overlay, converted from a percentage (0-100) to an alpha value (0-255). + * Resets to default and shows a toast if the value is out of range. */ val overlayBackgroundOpacity: Int get() { @@ -97,8 +100,7 @@ class SwipeControlsConfigurationProvider { if (opacity < 0 || opacity > 100) { Utils.showToastLong(str("revanced_swipe_overlay_background_opacity_invalid_toast")) - Settings.SWIPE_OVERLAY_OPACITY.resetToDefault() - opacity = Settings.SWIPE_OVERLAY_OPACITY.get() + opacity = Settings.SWIPE_OVERLAY_OPACITY.resetToDefault() } opacity = opacity * 255 / 100 @@ -106,55 +108,125 @@ class SwipeControlsConfigurationProvider { } /** - * The color of the progress overlay. + * The color of the progress bar in the overlay. + * Resets to default and shows a toast if the color string is invalid or empty. */ val overlayProgressColor: Int - get() = 0xBFFFFFFF.toInt() + get() { + try { + @SuppressLint("UseKtx") + val color = Color.parseColor(Settings.SWIPE_OVERLAY_PROGRESS_COLOR.get()) + return (0xBF000000.toInt() or (color and 0xFFFFFF)) + } catch (ex: IllegalArgumentException) { + Logger.printDebug({ "Could not parse color" }, ex) + Utils.showToastLong(str("revanced_swipe_overlay_progress_color_invalid_toast")) + Settings.SWIPE_OVERLAY_PROGRESS_COLOR.resetToDefault() + return overlayProgressColor // Recursively return. + } + } /** - * The color used for the background of the progress overlay fill. + * The background color used for the filled portion of the progress bar in the overlay. */ val overlayFillBackgroundPaint: Int get() = 0x80D3D3D3.toInt() /** - * The color used for the text and icons in the overlay. + * The color used for text and icons in the overlay. */ val overlayTextColor: Int get() = Color.WHITE /** - * A flag that determines if the overlay should only show the icon. + * The text size in the overlay, in density-independent pixels (dp). + * Must be between 1 and 30 dp; resets to default and shows a toast if invalid. */ - val overlayShowOverlayMinimalStyle: Boolean - get() = Settings.SWIPE_OVERLAY_MINIMAL_STYLE.get() + val overlayTextSize: Int + get() { + val size = Settings.SWIPE_OVERLAY_TEXT_SIZE.get() + if (size < 1 || size > 30) { + Utils.showToastLong(str("revanced_swipe_text_overlay_size_invalid_toast")) + return Settings.SWIPE_OVERLAY_TEXT_SIZE.resetToDefault() + } + return size + } /** - * A flag that determines if the progress bar should be circular. + * Defines the style of the swipe controls overlay, determining its layout and appearance. + * + * @property isMinimal Indicates whether the style is minimalistic, omitting detailed progress indicators. + * @property isHorizontalMinimalCenter Indicates whether the style is a minimal horizontal bar centered vertically. + * @property isCircular Indicates whether the style uses a circular progress bar. + * @property isVertical Indicates whether the style uses a vertical progress bar. */ - val isCircularProgressBar: Boolean - get() = Settings.SWIPE_SHOW_CIRCULAR_OVERLAY.get() -//endregion + @Suppress("unused") + enum class SwipeOverlayStyle( + val isMinimal: Boolean = false, + val isHorizontalMinimalCenter: Boolean = false, + val isCircular: Boolean = false, + val isVertical: Boolean = false + ) { + /** + * A full horizontal progress bar with detailed indicators. + */ + HORIZONTAL, -//region behaviour + /** + * A minimal horizontal progress bar positioned at the top. + */ + HORIZONTAL_MINIMAL_TOP(isMinimal = true), + + /** + * A minimal horizontal progress bar centered vertically. + */ + HORIZONTAL_MINIMAL_CENTER(isMinimal = true, isHorizontalMinimalCenter = true), + + /** + * A full circular progress bar with detailed indicators. + */ + CIRCULAR(isCircular = true), + + /** + * A minimal circular progress bar. + */ + CIRCULAR_MINIMAL(isMinimal = true, isCircular = true), + + /** + * A full vertical progress bar with detailed indicators. + */ + VERTICAL(isVertical = true), + + /** + * A minimal vertical progress bar. + */ + VERTICAL_MINIMAL(isMinimal = true, isVertical = true) + } /** - * should the brightness be saved and restored when exiting or entering fullscreen + * The current style of the overlay, determining its layout and appearance. + */ + val overlayStyle: SwipeOverlayStyle + get() = Settings.SWIPE_OVERLAY_STYLE.get() + //endregion + + //region behaviour + /** + * Indicates whether the brightness level should be saved and restored when entering or exiting fullscreen mode. */ val shouldSaveAndRestoreBrightness: Boolean get() = Settings.SWIPE_SAVE_AND_RESTORE_BRIGHTNESS.get() /** - * should auto-brightness be enabled at the lowest value of the brightness gesture + * Indicates whether auto-brightness should be enabled when the brightness gesture reaches its lowest value. */ val shouldLowestValueEnableAutoBrightness: Boolean get() = Settings.SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS.get() /** - * variable that stores the brightness gesture value in the settings + * The saved brightness value for the swipe gesture, used to restore brightness in fullscreen mode. */ var savedScreenBrightnessValue: Float get() = Settings.SWIPE_BRIGHTNESS_VALUE.get() set(value) = Settings.SWIPE_BRIGHTNESS_VALUE.save(value) -//endregion -} + //endregion +} \ No newline at end of file diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt index 10b70ed95..7fafd83a7 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt @@ -23,9 +23,7 @@ import java.lang.ref.WeakReference /** * The main controller for volume and brightness swipe controls. - * note that the superclass is overwritten to the superclass of the MainActivity at patch time - * - * @smali Lapp/revanced/extension/swipecontrols/SwipeControlsHostActivity; + * note that the superclass is overwritten to the superclass of the MainActivity at patch time. */ class SwipeControlsHostActivity : Activity() { /** diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt index 8df6aeeee..6a9044e85 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt @@ -1,8 +1,11 @@ package app.revanced.extension.youtube.swipecontrols.views +import android.annotation.SuppressLint import android.content.Context +import android.content.res.Resources import android.graphics.Canvas import android.graphics.Paint +import android.graphics.Rect import android.graphics.RectF import android.graphics.drawable.Drawable import android.os.Handler @@ -11,14 +14,23 @@ import android.util.AttributeSet import android.view.HapticFeedbackConstants import android.view.View import android.widget.RelativeLayout +import app.revanced.extension.shared.StringRef.str import app.revanced.extension.shared.Utils import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider import app.revanced.extension.youtube.swipecontrols.misc.SwipeControlsOverlay import kotlin.math.min +import kotlin.math.max import kotlin.math.round /** - * Main overlay layout for displaying volume and brightness level with both circular and horizontal progress bars. + * Convert dp to pixels based on system display density. + */ +fun Float.toDisplayPixels(): Float { + return this * Resources.getSystem().displayMetrics.density +} + +/** + * Main overlay layout for displaying volume and brightness level with circular, horizontal and vertical progress bars. */ class SwipeControlsOverlayLayout( context: Context, @@ -51,18 +63,21 @@ class SwipeControlsOverlayLayout( // Initialize progress bars private val circularProgressView: CircularProgressView private val horizontalProgressView: HorizontalProgressView + private val verticalBrightnessProgressView: VerticalProgressView + private val verticalVolumeProgressView: VerticalProgressView init { // Initialize circular progress bar circularProgressView = CircularProgressView( context, config.overlayBackgroundOpacity, - config.overlayShowOverlayMinimalStyle, + config.overlayStyle.isMinimal, config.overlayProgressColor, config.overlayFillBackgroundPaint, - config.overlayTextColor + config.overlayTextColor, + config.overlayTextSize ).apply { - layoutParams = LayoutParams(300, 300).apply { + layoutParams = LayoutParams(100f.toDisplayPixels().toInt(), 100f.toDisplayPixels().toInt()).apply { addRule(CENTER_IN_PARENT, TRUE) } visibility = GONE // Initially hidden @@ -71,22 +86,65 @@ class SwipeControlsOverlayLayout( // Initialize horizontal progress bar val screenWidth = resources.displayMetrics.widthPixels - val layoutWidth = (screenWidth * 2 / 3).toInt() // 2/3 of screen width + val layoutWidth = (screenWidth * 4 / 5).toInt() // Cap at ~360dp horizontalProgressView = HorizontalProgressView( context, config.overlayBackgroundOpacity, - config.overlayShowOverlayMinimalStyle, + config.overlayStyle.isMinimal, config.overlayProgressColor, config.overlayFillBackgroundPaint, - config.overlayTextColor + config.overlayTextColor, + config.overlayTextSize ).apply { - layoutParams = LayoutParams(layoutWidth, 100).apply { + layoutParams = LayoutParams(layoutWidth, 32f.toDisplayPixels().toInt()).apply { addRule(CENTER_HORIZONTAL) - topMargin = 40 // Top margin + if (config.overlayStyle.isHorizontalMinimalCenter) { + addRule(CENTER_VERTICAL) + } else { + topMargin = 20f.toDisplayPixels().toInt() + } } visibility = GONE // Initially hidden } addView(horizontalProgressView) + + // Initialize vertical progress bar for brightness (right side) + verticalBrightnessProgressView = VerticalProgressView( + context, + config.overlayBackgroundOpacity, + config.overlayStyle.isMinimal, + config.overlayProgressColor, + config.overlayFillBackgroundPaint, + config.overlayTextColor, + config.overlayTextSize + ).apply { + layoutParams = LayoutParams(40f.toDisplayPixels().toInt(), 150f.toDisplayPixels().toInt()).apply { + addRule(ALIGN_PARENT_RIGHT) + rightMargin = 40f.toDisplayPixels().toInt() + addRule(CENTER_VERTICAL) + } + visibility = GONE // Initially hidden + } + addView(verticalBrightnessProgressView) + + // Initialize vertical progress bar for volume (left side) + verticalVolumeProgressView = VerticalProgressView( + context, + config.overlayBackgroundOpacity, + config.overlayStyle.isMinimal, + config.overlayProgressColor, + config.overlayFillBackgroundPaint, + config.overlayTextColor, + config.overlayTextSize + ).apply { + layoutParams = LayoutParams(40f.toDisplayPixels().toInt(), 150f.toDisplayPixels().toInt()).apply { + addRule(ALIGN_PARENT_LEFT) + leftMargin = 40f.toDisplayPixels().toInt() + addRule(CENTER_VERTICAL) + } + visibility = GONE // Initially hidden + } + addView(verticalVolumeProgressView) } // Handler and callback for hiding progress bars @@ -94,6 +152,8 @@ class SwipeControlsOverlayLayout( private val feedbackHideCallback = Runnable { circularProgressView.visibility = GONE horizontalProgressView.visibility = GONE + verticalBrightnessProgressView.visibility = GONE + verticalVolumeProgressView.visibility = GONE } /** @@ -103,7 +163,11 @@ class SwipeControlsOverlayLayout( feedbackHideHandler.removeCallbacks(feedbackHideCallback) feedbackHideHandler.postDelayed(feedbackHideCallback, config.overlayShowTimeoutMillis) - val viewToShow = if (config.isCircularProgressBar) circularProgressView else horizontalProgressView + val viewToShow = when { + config.overlayStyle.isCircular -> circularProgressView + config.overlayStyle.isVertical -> if (isBrightness) verticalBrightnessProgressView else verticalVolumeProgressView + else -> horizontalProgressView + } viewToShow.apply { setProgress(progress, max, value, isBrightness) this.icon = icon @@ -126,7 +190,9 @@ class SwipeControlsOverlayLayout( // Handle brightness change override fun onBrightnessChanged(brightness: Double) { if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) { - showFeedbackView("Auto", 0, 100, autoBrightnessIcon, isBrightness = true) + val displayText = if (config.overlayStyle.isVertical) "А" + else str("revanced_swipe_lowest_value_enable_auto_brightness_overlay_text") + showFeedbackView(displayText, 0, 100, autoBrightnessIcon, isBrightness = true) } else { val brightnessValue = round(brightness).toInt() val icon = when { @@ -135,7 +201,8 @@ class SwipeControlsOverlayLayout( brightnessValue < 75 -> highBrightnessIcon else -> fullBrightnessIcon } - showFeedbackView("$brightnessValue%", brightnessValue, 100, icon, isBrightness = true) + val displayText = if (config.overlayStyle.isVertical) "$brightnessValue" else "$brightnessValue%" + showFeedbackView(displayText, brightnessValue, 100, icon, isBrightness = true) } } @@ -156,11 +223,12 @@ class SwipeControlsOverlayLayout( */ abstract class AbstractProgressView( context: Context, - protected val overlayBackgroundOpacity: Int, - protected val overlayShowOverlayMinimalStyle: Boolean, - protected val overlayProgressColor: Int, - protected val overlayFillBackgroundPaint: Int, - protected val overlayTextColor: Int, + overlayBackgroundOpacity: Int, + protected val isMinimalStyle: Boolean, + overlayProgressColor: Int, + overlayFillBackgroundPaint: Int, + private val overlayTextColor: Int, + protected val overlayTextSize: Int, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : View(context, attrs, defStyleAttr) { @@ -174,26 +242,25 @@ abstract class AbstractProgressView( } // Initialize paints - public val backgroundPaint = createPaint(overlayBackgroundOpacity, style = Paint.Style.FILL) - public val progressPaint = createPaint(overlayProgressColor, style = Paint.Style.STROKE, strokeCap = Paint.Cap.ROUND, strokeWidth = 20f) - public val fillBackgroundPaint = createPaint(overlayFillBackgroundPaint, style = Paint.Style.FILL) - public val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + val backgroundPaint = createPaint(overlayBackgroundOpacity, style = Paint.Style.FILL) + val progressPaint = createPaint(overlayProgressColor, style = Paint.Style.STROKE, strokeCap = Paint.Cap.ROUND, strokeWidth = 6f.toDisplayPixels()) + val fillBackgroundPaint = createPaint(overlayFillBackgroundPaint, style = Paint.Style.FILL) + val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = overlayTextColor textAlign = Paint.Align.CENTER - textSize = 40f // Can adjust based on need + textSize = overlayTextSize.toFloat().toDisplayPixels() } + // Rect for text measurement + protected val textBounds = Rect() + protected var progress = 0 protected var maxProgress = 100 protected var displayText: String = "0" protected var isBrightness = true - public var icon: Drawable? = null + var icon: Drawable? = null - init { - // Stroke widths are now set in createPaint for progressPaint and fillBackgroundPaint - } - - fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) { + open fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) { progress = value maxProgress = max displayText = text @@ -201,6 +268,11 @@ abstract class AbstractProgressView( invalidate() } + protected fun measureTextWidth(text: String, paint: Paint): Int { + paint.getTextBounds(text, 0, text.length, textBounds) + return textBounds.width() + } + override fun onDraw(canvas: Canvas) { // Base class implementation can be empty } @@ -209,34 +281,36 @@ abstract class AbstractProgressView( /** * Custom view for rendering a circular progress indicator with icons and text. */ +@SuppressLint("ViewConstructor") class CircularProgressView( context: Context, overlayBackgroundOpacity: Int, - overlayShowOverlayMinimalStyle: Boolean, + isMinimalStyle: Boolean, overlayProgressColor: Int, overlayFillBackgroundPaint: Int, overlayTextColor: Int, + overlayTextSize: Int, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AbstractProgressView( context, overlayBackgroundOpacity, - overlayShowOverlayMinimalStyle, + isMinimalStyle, overlayProgressColor, overlayFillBackgroundPaint, overlayTextColor, + overlayTextSize, attrs, defStyleAttr ) { private val rectF = RectF() init { - textPaint.textSize = 40f // Override default text size for circular view - progressPaint.strokeWidth = 20f - fillBackgroundPaint.strokeWidth = 20f - progressPaint.strokeCap = Paint.Cap.ROUND + progressPaint.strokeWidth = 6f.toDisplayPixels() + fillBackgroundPaint.strokeWidth = 6f.toDisplayPixels() + progressPaint.strokeCap = Paint.Cap.ROUND fillBackgroundPaint.strokeCap = Paint.Cap.BUTT - progressPaint.style = Paint.Style.STROKE + progressPaint.style = Paint.Style.STROKE fillBackgroundPaint.style = Paint.Style.STROKE } @@ -244,7 +318,8 @@ class CircularProgressView( super.onDraw(canvas) val size = min(width, height).toFloat() - rectF.set(20f, 20f, size - 20f, size - 20f) + val inset = 6f.toDisplayPixels() + rectF.set(inset, inset, size - inset, size - inset) canvas.drawOval(rectF, fillBackgroundPaint) // Draw the outer ring. canvas.drawCircle(width / 2f, height / 2f, size / 3, backgroundPaint) // Draw the inner circle. @@ -255,124 +330,307 @@ class CircularProgressView( // Draw the icon in the center. icon?.let { - val iconSize = if (overlayShowOverlayMinimalStyle) 100 else 80 + val iconSize = (if (isMinimalStyle) 36f else 24f).toDisplayPixels().toInt() val iconX = (width - iconSize) / 2 - val iconY = (height / 2) - if (overlayShowOverlayMinimalStyle) 50 else 80 + val iconY = if (isMinimalStyle) { + (height - iconSize) / 2 + } else { + (height / 2) - 24f.toDisplayPixels().toInt() + } it.setBounds(iconX, iconY, iconX + iconSize, iconY + iconSize) it.draw(canvas) } // If not a minimal style mode, draw the text inside the ring. - if (!overlayShowOverlayMinimalStyle) { - canvas.drawText(displayText, width / 2f, height / 2f + 60f, textPaint) + if (!isMinimalStyle) { + canvas.drawText(displayText, width / 2f, height / 2f + 20f.toDisplayPixels(), textPaint) } } + + override fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) { + super.setProgress(value, max, text, isBrightnessMode) + requestLayout() + } } /** * Custom view for rendering a rectangular progress bar with icons and text. */ +@SuppressLint("ViewConstructor") class HorizontalProgressView( context: Context, overlayBackgroundOpacity: Int, - overlayShowOverlayMinimalStyle: Boolean, + isMinimalStyle: Boolean, overlayProgressColor: Int, overlayFillBackgroundPaint: Int, overlayTextColor: Int, + overlayTextSize: Int, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AbstractProgressView( context, overlayBackgroundOpacity, - overlayShowOverlayMinimalStyle, + isMinimalStyle, overlayProgressColor, overlayFillBackgroundPaint, overlayTextColor, + overlayTextSize, attrs, defStyleAttr ) { - private val iconSize = 60f - private val padding = 40f + private val iconSize = 20f.toDisplayPixels() + private val padding = 12f.toDisplayPixels() + private var textWidth = 0f + private val progressBarHeight = 3f.toDisplayPixels() + private val progressBarWidth: Float = resources.displayMetrics.widthPixels / 4f init { - textPaint.textSize = 36f // Override default text size for horizontal view progressPaint.strokeWidth = 0f - progressPaint.strokeCap = Paint.Cap.BUTT - progressPaint.style = Paint.Style.FILL + progressPaint.strokeCap = Paint.Cap.BUTT + progressPaint.style = Paint.Style.FILL fillBackgroundPaint.style = Paint.Style.FILL } + /** + * Calculate required width based on content + * @return Required width to display all elements + */ + private fun calculateRequiredWidth(): Float { + textWidth = measureTextWidth(displayText, textPaint).toFloat() + + return if (!isMinimalStyle) { + padding + iconSize + padding + progressBarWidth + padding + textWidth + padding + } else { + padding + iconSize + padding + textWidth + padding + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val suggestedWidth = MeasureSpec.getSize(widthMeasureSpec) + val suggestedHeight = MeasureSpec.getSize(heightMeasureSpec) + + val height = suggestedHeight + val requiredWidth = calculateRequiredWidth().toInt() + val width = min(max(100, requiredWidth), suggestedWidth) + + setMeasuredDimension(width, height) + } + override fun onDraw(canvas: Canvas) { super.onDraw(canvas) - val width = width.toFloat() - val height = height.toFloat() + val viewWidth = width.toFloat() + val viewHeight = height.toFloat() + val viewHeightHalf = viewHeight / 2 - // Radius for rounded corners - val cornerRadius = min(width, height) / 2 + textWidth = measureTextWidth(displayText, textPaint).toFloat() - // Calculate the total width for the elements - val minimalElementWidth = 5 * padding + iconSize + val cornerRadius = viewHeightHalf - // Calculate the starting point (X) to center the elements - val minimalStartX = (width - minimalElementWidth) / 2 + val startX = padding + val iconEndX = startX + iconSize - // Draw the background - if (!overlayShowOverlayMinimalStyle) { - canvas.drawRoundRect(0f, 0f, width, height, cornerRadius, cornerRadius, backgroundPaint) - } else { - canvas.drawRoundRect(minimalStartX, 0f, minimalStartX + minimalElementWidth, height, cornerRadius, cornerRadius, backgroundPaint) - } + val textStartX = (viewWidth - 1.5f * padding - textWidth) - if (!overlayShowOverlayMinimalStyle) { - // Draw the fill background - val startX = 2 * padding + iconSize - val endX = width - 4 * padding - val fillWidth = endX - startX + canvas.drawRoundRect( + 0f, 0f, viewWidth, viewHeight, + cornerRadius, cornerRadius, backgroundPaint + ) - canvas.drawRoundRect( - startX, - height / 2 - 5f, - endX, - height / 2 + 5f, - 10f, 10f, - fillBackgroundPaint - ) - - // Draw the progress - val progressWidth = (progress.toFloat() / maxProgress) * fillWidth - canvas.drawRoundRect( - startX, - height / 2 - 5f, - startX + progressWidth, - height / 2 + 5f, - 10f, 10f, - progressPaint - ) - } - - // Draw the icon icon?.let { - val iconX = if (!overlayShowOverlayMinimalStyle) { - padding - } else { - padding + minimalStartX - } - val iconY = height / 2 - iconSize / 2 - it.setBounds(iconX.toInt(), iconY.toInt(), (iconX + iconSize).toInt(), (iconY + iconSize).toInt()) + val iconY = viewHeightHalf - iconSize / 2 + it.setBounds( + startX.toInt(), + iconY.toInt(), + (startX + iconSize).toInt(), + (iconY + iconSize).toInt() + ) it.draw(canvas) } - // Draw the text on the right - val textX = if (!overlayShowOverlayMinimalStyle) { - width - 2 * padding - } else { - minimalStartX + minimalElementWidth - 2 * padding - } - val textY = height / 2 + textPaint.textSize / 3 + val textY = viewHeightHalf + textPaint.textSize / 3 + textPaint.textAlign = Paint.Align.LEFT - // Draw the text - canvas.drawText(displayText, textX, textY, textPaint) + if (isMinimalStyle) { + canvas.drawText(displayText, textStartX, textY, textPaint) + } else { + val progressStartX = iconEndX + padding + val progressEndX = textStartX - padding + val progressWidth = progressEndX - progressStartX + + if (progressWidth > 50) { + val progressBarHeightHalf = progressBarHeight / 2.0f + val viewHeightHalfMinusProgressBarHeightHalf = viewHeightHalf - progressBarHeightHalf + val viewHeightHalfPlusProgressBarHeightHalf = viewHeightHalf + progressBarHeightHalf + + canvas.drawRoundRect( + progressStartX, + viewHeightHalfMinusProgressBarHeightHalf, + progressEndX, + viewHeightHalfPlusProgressBarHeightHalf, + progressBarHeightHalf, + progressBarHeightHalf, + fillBackgroundPaint + ) + + val progressValue = (progress.toFloat() / maxProgress) * progressWidth + canvas.drawRoundRect( + progressStartX, + viewHeightHalfMinusProgressBarHeightHalf, + progressStartX + progressValue, + viewHeightHalfPlusProgressBarHeightHalf, + progressBarHeightHalf, + progressBarHeightHalf, + progressPaint + ) + } + + canvas.drawText(displayText, textStartX, textY, textPaint) + } + } + + override fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) { + super.setProgress(value, max, text, isBrightnessMode) + requestLayout() } } + +/** + * Custom view for rendering a vertical progress bar with icons and text. + */ +@SuppressLint("ViewConstructor") +class VerticalProgressView( + context: Context, + overlayBackgroundOpacity: Int, + isMinimalStyle: Boolean, + overlayProgressColor: Int, + overlayFillBackgroundPaint: Int, + overlayTextColor: Int, + overlayTextSize: Int, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AbstractProgressView( + context, + overlayBackgroundOpacity, + isMinimalStyle, + overlayProgressColor, + overlayFillBackgroundPaint, + overlayTextColor, + overlayTextSize, + attrs, + defStyleAttr +) { + + private val iconSize = 20f.toDisplayPixels() + private val padding = 12f.toDisplayPixels() + private val progressBarWidth = 3f.toDisplayPixels() + private val progressBarHeight: Float = resources.displayMetrics.widthPixels / 3f + + init { + progressPaint.strokeWidth = 0f + progressPaint.strokeCap = Paint.Cap.BUTT + progressPaint.style = Paint.Style.FILL + fillBackgroundPaint.style = Paint.Style.FILL + } + + /** + * Calculate required height based on content + * @return Required height to display all elements + */ + private fun calculateRequiredHeight(): Float { + return if (!isMinimalStyle) { + padding + iconSize + padding + progressBarHeight + padding + textPaint.textSize + padding + } else { + padding + iconSize + padding + textPaint.textSize + padding + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + val suggestedWidth = MeasureSpec.getSize(widthMeasureSpec) + val suggestedHeight = MeasureSpec.getSize(heightMeasureSpec) + + val requiredHeight = calculateRequiredHeight().toInt() + val height = min(max(100, requiredHeight), suggestedHeight) + + setMeasuredDimension(suggestedWidth, height) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + val viewWidth = width.toFloat() + val viewHeight = height.toFloat() + val viewWidthHalf = viewWidth / 2 + val cornerRadius = viewWidthHalf + + val startY = padding + val iconEndY = startY + iconSize + + val textStartY = viewHeight - padding - textPaint.textSize / 2 + + canvas.drawRoundRect( + 0f, 0f, viewWidth, viewHeight, + cornerRadius, cornerRadius, backgroundPaint + ) + + icon?.let { + val iconX = viewWidthHalf - iconSize / 2 + it.setBounds( + iconX.toInt(), + startY.toInt(), + (iconX + iconSize).toInt(), + (startY + iconSize).toInt() + ) + it.draw(canvas) + } + + val textX = viewWidthHalf + textPaint.textAlign = Paint.Align.CENTER + + if (isMinimalStyle) { + canvas.drawText(displayText, textX, textStartY, textPaint) + } else { + val progressStartY = (iconEndY + padding).toFloat() + val progressEndY = textStartY - textPaint.textSize - padding + val progressHeight = progressEndY - progressStartY + + if (progressHeight > 50) { + val progressBarWidthHalf = progressBarWidth / 2 + val viewHeightHalfMinusProgressBarHeightHalf = viewWidthHalf - progressBarWidthHalf + val viewHeightHalfPlusProgressBarHeightHalf = viewWidthHalf + progressBarWidthHalf + + canvas.drawRoundRect( + viewHeightHalfMinusProgressBarHeightHalf, + progressStartY, + viewHeightHalfPlusProgressBarHeightHalf, + progressEndY, + progressBarWidthHalf, + progressBarWidthHalf, + fillBackgroundPaint + ) + + val progressValue = (progress.toFloat() / maxProgress) * progressHeight + canvas.drawRoundRect( + viewHeightHalfMinusProgressBarHeightHalf, + progressEndY - progressValue, + viewHeightHalfPlusProgressBarHeightHalf, + progressEndY, + progressBarWidthHalf, + progressBarWidthHalf, + progressPaint + ) + } + canvas.drawText(displayText, textX, textStartY, textPaint) + } + } + + override fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) { + super.setProgress(value, max, text, isBrightnessMode) + requestLayout() + } +} \ No newline at end of file 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 e987ec4ce..2c55658fb 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 @@ -6,6 +6,7 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMu import app.revanced.patches.all.misc.resources.addResources import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.InputType +import app.revanced.patches.shared.misc.settings.preference.ListPreference import app.revanced.patches.shared.misc.settings.preference.SwitchPreference import app.revanced.patches.shared.misc.settings.preference.TextPreference import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch @@ -42,9 +43,13 @@ private val swipeControlsResourcePatch = resourcePatch { SwitchPreference("revanced_swipe_haptic_feedback"), SwitchPreference("revanced_swipe_save_and_restore_brightness"), SwitchPreference("revanced_swipe_lowest_value_enable_auto_brightness"), - SwitchPreference("revanced_swipe_show_circular_overlay"), - SwitchPreference("revanced_swipe_overlay_minimal_style"), + ListPreference( + "revanced_swipe_overlay_style", + 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_text_overlay_size", inputType = InputType.NUMBER), TextPreference("revanced_swipe_overlay_timeout", inputType = InputType.NUMBER), TextPreference("revanced_swipe_threshold", inputType = InputType.NUMBER), TextPreference("revanced_swipe_volume_sensitivity", inputType = InputType.NUMBER), diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml index 1e11eb9a1..7889a05dd 100644 --- a/patches/src/main/resources/addresources/values/arrays.xml +++ b/patches/src/main/resources/addresources/values/arrays.xml @@ -135,6 +135,27 @@ IOS_UNPLUGGED + + + @string/revanced_swipe_overlay_style_entry_1 + @string/revanced_swipe_overlay_style_entry_2 + @string/revanced_swipe_overlay_style_entry_3 + @string/revanced_swipe_overlay_style_entry_4 + @string/revanced_swipe_overlay_style_entry_5 + @string/revanced_swipe_overlay_style_entry_6 + @string/revanced_swipe_overlay_style_entry_7 + + + + HORIZONTAL + HORIZONTAL_MINIMAL_TOP + HORIZONTAL_MINIMAL_CENTER + CIRCULAR + CIRCULAR_MINIMAL + VERTICAL + VERTICAL_MINIMAL + + @string/revanced_spoof_app_version_target_entry_1 diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 512d235db..cf670ed7e 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -520,21 +520,30 @@ Adjust volume by swiping vertically on the right side of the screen" Enable auto-brightness gesture Swiping down to the lowest value of the brightness gesture enable auto-brightness Swiping down to the lowest value does not enable auto-brightness + Auto Swipe overlay timeout The amount of milliseconds the overlay is visible Swipe overlay background opacity Opacity value between 0-100 Swipe opacity must be between 0-100 + Swipe overlay progress bar color + The color of the progress bar for volume and brightness controls + Invalid progress bar color + Swipe overlay text size + The text size for swipe overlay between 1-30 + The text size must be between 1-30 Swipe magnitude threshold The amount of threshold for swipe to occur Volume swipe sensitivity How much the volume changes per swipe - Show circular overlay - Circular overlay is shown - Horizontal overlay is shown - Enable minimal style - Minimal overlay style is enabled - Minimal overlay style is disabled + Swipe overlay style + Horizontal overlay + Horizontal overlay (minimal - top) + Horizontal overlay (minimal - center) + Circular overlay + Circular overlay (minimal) + Vertical overlay + Vertical overlay (minimal) Enable swipe to change videos Swiping in fullscreen mode will change to the next/previous video Swiping in fullscreen mode will not change to the next/previous video