mirror of
https://github.com/revanced/revanced-integrations.git
synced 2025-01-05 17:45:49 +01:00
feat: swipe-controls
rewrite (#64)
* rewrite swipe controls without deep hooks + merge changes + refactor class names * fix gesture detection behaviour * add option to disable press-to-swipe * add config options related to swipe * restore default device brightness when exiting fullscreen player fixes https://github.com/revanced/revanced-patches/issues/128 * set to default brightness after reaching 0% * block swipe-to-dismiss when not using press-to-swipe * fix: TouchThiefLayout potentially attaches multiple times * remove last references to 'fenster' name * move updatePlayerType hook into its own patch * refactor 'swipe-controls' patch * make feedback text backgrond semi-transparent * update swipe-controls overlay * fix swipe-controls leaking host activity context * fix saved screen brightness resetting between videos * fix crash on re-enter activity * make overlay more configurable * add settings to revanced_prefs.xml
This commit is contained in:
parent
376ffc0844
commit
fcabebf3a7
@ -1,33 +0,0 @@
|
||||
package app.revanced.integrations.fenster
|
||||
|
||||
import app.revanced.integrations.settings.SettingsEnum
|
||||
|
||||
/**
|
||||
* controls fenster feature enablement
|
||||
*/
|
||||
object FensterEnablement {
|
||||
|
||||
/**
|
||||
* should fenster be enabled? (global setting)
|
||||
*/
|
||||
val shouldEnableFenster: Boolean
|
||||
get() {
|
||||
return shouldEnableFensterVolumeControl || shouldEnableFensterBrightnessControl
|
||||
}
|
||||
|
||||
/**
|
||||
* should swipe controls for volume be enabled?
|
||||
*/
|
||||
val shouldEnableFensterVolumeControl: Boolean
|
||||
get() {
|
||||
return SettingsEnum.ENABLE_SWIPE_VOLUME_BOOLEAN.boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* should swipe controls for volume be enabled?
|
||||
*/
|
||||
val shouldEnableFensterBrightnessControl: Boolean
|
||||
get() {
|
||||
return SettingsEnum.ENABLE_SWIPE_BRIGHTNESS_BOOLEAN.boolean
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package app.revanced.integrations.fenster
|
||||
|
||||
/**
|
||||
* WatchWhile player types
|
||||
*/
|
||||
@Suppress("unused")
|
||||
enum class WatchWhilePlayerType {
|
||||
NONE,
|
||||
HIDDEN,
|
||||
WATCH_WHILE_MINIMIZED,
|
||||
WATCH_WHILE_MAXIMIZED,
|
||||
WATCH_WHILE_FULLSCREEN,
|
||||
WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN,
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED,
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED,
|
||||
WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED,
|
||||
INLINE_MINIMAL,
|
||||
VIRTUAL_REALITY_FULLSCREEN,
|
||||
WATCH_WHILE_PICTURE_IN_PICTURE;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun safeParseFromString(name: String): WatchWhilePlayerType? {
|
||||
return values().firstOrNull { it.name == name }
|
||||
}
|
||||
}
|
||||
}
|
@ -1,243 +0,0 @@
|
||||
package app.revanced.integrations.fenster.controllers
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewGroup
|
||||
import app.revanced.integrations.fenster.FensterEnablement
|
||||
import app.revanced.integrations.fenster.util.ScrollDistanceHelper
|
||||
import app.revanced.integrations.fenster.util.SwipeControlZone
|
||||
import app.revanced.integrations.fenster.util.applyDimension
|
||||
import app.revanced.integrations.fenster.util.getSwipeControlZone
|
||||
import app.revanced.integrations.utils.LogHelper
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* main controller class for 'FensterV2' swipe controls
|
||||
*/
|
||||
class FensterController {
|
||||
|
||||
/**
|
||||
* are the swipe controls currently enabled?
|
||||
*/
|
||||
var isEnabled: Boolean
|
||||
get() = _isEnabled
|
||||
set(value) {
|
||||
_isEnabled = value && FensterEnablement.shouldEnableFenster
|
||||
overlayController?.setOverlayVisible(_isEnabled)
|
||||
LogHelper.debug(this.javaClass, "FensterController.isEnabled set to $_isEnabled")
|
||||
}
|
||||
private var _isEnabled = false
|
||||
|
||||
/**
|
||||
* the activity that hosts the controller
|
||||
*/
|
||||
private var hostActivity: Activity? = null
|
||||
private var audioController: AudioVolumeController? = null
|
||||
private var screenController: ScreenBrightnessController? = null
|
||||
private var overlayController: FensterOverlayController? = null
|
||||
|
||||
private var gestureListener: FensterGestureListener? = null
|
||||
private var gestureDetector: GestureDetector? = null
|
||||
|
||||
/**
|
||||
* Initializes the controller.
|
||||
* this function *may* be called after [initializeOverlay], but must be called before [onTouchEvent]
|
||||
*
|
||||
* @param host the activity that hosts the controller. this must be the same activity that the view hook for [onTouchEvent] is on
|
||||
*/
|
||||
fun initializeController(host: Activity) {
|
||||
if (hostActivity != null) {
|
||||
if (host == hostActivity) {
|
||||
// function was called twice, ignore the call
|
||||
LogHelper.debug(
|
||||
this.javaClass,
|
||||
"initializeController was called twice, ignoring secondary call"
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
LogHelper.debug(this.javaClass, "initializing FensterV2 controllers")
|
||||
hostActivity = host
|
||||
audioController = if (FensterEnablement.shouldEnableFensterVolumeControl)
|
||||
AudioVolumeController(host) else null
|
||||
screenController = if (FensterEnablement.shouldEnableFensterBrightnessControl)
|
||||
ScreenBrightnessController(host) else null
|
||||
|
||||
gestureListener = FensterGestureListener(host)
|
||||
gestureDetector = GestureDetector(host, gestureListener)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the user feedback overlay, adding it as a child to the provided parent.
|
||||
* this function *may* not be called, but in that case you'll have no user feedback
|
||||
*
|
||||
* @param parent parent view group that the overlay is added to
|
||||
*/
|
||||
fun initializeOverlay(parent: ViewGroup) {
|
||||
LogHelper.debug(this.javaClass, "initializing FensterV2 overlay")
|
||||
|
||||
// create and add overlay
|
||||
overlayController = FensterOverlayController(parent.context)
|
||||
parent.addView(overlayController!!.overlayRootView, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process touch events from the view hook.
|
||||
* the hooked view *must* be a child of the activity used for [initializeController]
|
||||
*
|
||||
* @param event the motion event to process
|
||||
* @return was the event consumed by the controller?
|
||||
*/
|
||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
// if disabled, we shall not consume any events
|
||||
if (!isEnabled) return false
|
||||
|
||||
// if important components are not present, there is no point in processing the event here
|
||||
if (hostActivity == null || gestureDetector == null || gestureListener == null) {
|
||||
return false
|
||||
}
|
||||
|
||||
// send event to gesture detector
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
gestureListener?.onUp(event)
|
||||
}
|
||||
val consumed = gestureDetector?.onTouchEvent(event) ?: false
|
||||
|
||||
// if the event was inside a control zone, we always consume the event
|
||||
val swipeZone = event.getSwipeControlZone(hostActivity!!)
|
||||
var inControlZone = false
|
||||
if (audioController != null) {
|
||||
inControlZone = inControlZone || swipeZone == SwipeControlZone.VOLUME_CONTROL
|
||||
}
|
||||
if (screenController != null) {
|
||||
inControlZone = inControlZone || swipeZone == SwipeControlZone.BRIGHTNESS_CONTROL
|
||||
}
|
||||
|
||||
return consumed || inControlZone
|
||||
}
|
||||
|
||||
/**
|
||||
* primary gesture listener that handles the following behaviour:
|
||||
*
|
||||
* - Volume & Brightness swipe controls:
|
||||
* when swiping on the right or left side of the screen, the volume or brightness is adjusted accordingly.
|
||||
* swipe controls are only unlocked after a long- press in the corresponding screen half
|
||||
*
|
||||
* - Fling- to- mute:
|
||||
* when quickly flinging down, the volume is instantly muted
|
||||
*/
|
||||
inner class FensterGestureListener(
|
||||
private val context: Context
|
||||
) : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
|
||||
private var inSwipeSession = true
|
||||
|
||||
/**
|
||||
* scroller for volume adjustment
|
||||
*/
|
||||
private val volumeScroller = ScrollDistanceHelper(
|
||||
10.applyDimension(
|
||||
context,
|
||||
TypedValue.COMPLEX_UNIT_DIP
|
||||
)
|
||||
) { _, _, direction ->
|
||||
audioController?.apply {
|
||||
volume += direction
|
||||
overlayController?.showNewVolume((volume * 100.0) / maxVolume)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* scroller for screen brightness adjustment
|
||||
*/
|
||||
private val brightnessScroller = ScrollDistanceHelper(
|
||||
1.applyDimension(
|
||||
context,
|
||||
TypedValue.COMPLEX_UNIT_DIP
|
||||
)
|
||||
) { _, _, direction ->
|
||||
screenController?.apply {
|
||||
screenBrightness += direction
|
||||
overlayController?.showNewBrightness(screenBrightness)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* custom handler for ACTION_UP event, because GestureDetector doesn't offer that :|
|
||||
*
|
||||
* @param e the motion event
|
||||
*/
|
||||
fun onUp(e: MotionEvent) {
|
||||
LogHelper.debug(this.javaClass, "onUp(${e.x}, ${e.y}, ${e.action})")
|
||||
inSwipeSession = true
|
||||
volumeScroller.reset()
|
||||
brightnessScroller.reset()
|
||||
}
|
||||
|
||||
|
||||
override fun onScroll(
|
||||
eFrom: MotionEvent?,
|
||||
eTo: MotionEvent?,
|
||||
disX: Float,
|
||||
disY: Float
|
||||
): Boolean {
|
||||
if (eFrom == null || eTo == null) return false
|
||||
LogHelper.debug(
|
||||
this.javaClass,
|
||||
"onScroll(from: [${eFrom.x}, ${eFrom.y}, ${eFrom.action}], to: [${eTo.x}, ${eTo.y}, ${eTo.action}], d: [$disX, $disY])"
|
||||
)
|
||||
|
||||
// ignore if scroll not in scroll session
|
||||
if (!inSwipeSession) return false
|
||||
|
||||
// do the adjustment
|
||||
when (eFrom.getSwipeControlZone(context)) {
|
||||
SwipeControlZone.VOLUME_CONTROL -> {
|
||||
volumeScroller.add(disY.toDouble())
|
||||
}
|
||||
SwipeControlZone.BRIGHTNESS_CONTROL -> {
|
||||
brightnessScroller.add(disY.toDouble())
|
||||
}
|
||||
SwipeControlZone.NONE -> {}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onFling(
|
||||
eFrom: MotionEvent?,
|
||||
eTo: MotionEvent?,
|
||||
velX: Float,
|
||||
velY: Float
|
||||
): Boolean {
|
||||
if (eFrom == null || eTo == null) return false
|
||||
LogHelper.debug(
|
||||
this.javaClass,
|
||||
"onFling(from: [${eFrom.x}, ${eFrom.y}, ${eFrom.action}], to: [${eTo.x}, ${eTo.y}, ${eTo.action}], v: [$velX, $velY])"
|
||||
)
|
||||
|
||||
// filter out flings that are not very vertical
|
||||
if (abs(velY) < abs(velX * 2)) return false
|
||||
|
||||
// check if either of the events was in the volume zone
|
||||
if ((eFrom.getSwipeControlZone(context) == SwipeControlZone.VOLUME_CONTROL)
|
||||
|| (eTo.getSwipeControlZone(context) == SwipeControlZone.VOLUME_CONTROL)
|
||||
) {
|
||||
// if the fling was very aggressive, trigger instant- mute
|
||||
if (velY > 5000) {
|
||||
audioController?.apply {
|
||||
volume = 0
|
||||
overlayController?.notifyFlingToMutePerformed()
|
||||
overlayController?.showNewVolume((volume * 100.0) / maxVolume)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,130 +0,0 @@
|
||||
package app.revanced.integrations.fenster.controllers
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import app.revanced.integrations.fenster.util.applyDimension
|
||||
import kotlin.math.round
|
||||
|
||||
/**
|
||||
* controller for the fenster overlay
|
||||
*
|
||||
* @param context the context to create the overlay in
|
||||
*/
|
||||
class FensterOverlayController(
|
||||
context: Context
|
||||
) {
|
||||
|
||||
/**
|
||||
* the main overlay view
|
||||
*/
|
||||
val overlayRootView: RelativeLayout
|
||||
private val feedbackTextView: TextView
|
||||
|
||||
init {
|
||||
// create root container
|
||||
overlayRootView = RelativeLayout(context).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
isClickable = false
|
||||
isFocusable = false
|
||||
z = 1000f
|
||||
//elevation = 1000f
|
||||
}
|
||||
|
||||
// add other views
|
||||
val feedbackTextViewPadding = 2.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
feedbackTextView = TextView(context).apply {
|
||||
layoutParams = RelativeLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE)
|
||||
setPadding(
|
||||
feedbackTextViewPadding,
|
||||
feedbackTextViewPadding,
|
||||
feedbackTextViewPadding,
|
||||
feedbackTextViewPadding
|
||||
)
|
||||
}
|
||||
setBackgroundColor(Color.BLACK)
|
||||
setTextColor(Color.WHITE)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 20f)
|
||||
visibility = View.GONE
|
||||
}
|
||||
overlayRootView.addView(feedbackTextView)
|
||||
}
|
||||
|
||||
private val feedbackHideHandler = Handler(Looper.getMainLooper())
|
||||
private val feedbackHideCallback = Runnable {
|
||||
feedbackTextView.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* set the overlay visibility
|
||||
*
|
||||
* @param visible should the overlay be visible?
|
||||
*/
|
||||
fun setOverlayVisible(visible: Boolean) {
|
||||
overlayRootView.visibility = if (visible) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* show the new volume level on the overlay
|
||||
*
|
||||
* @param volume the new volume level, in percent (range 0.0 - 100.0)
|
||||
*/
|
||||
fun showNewVolume(volume: Double) {
|
||||
feedbackTextView.text = "Volume ${round(volume).toInt()}%"
|
||||
showFeedbackView()
|
||||
}
|
||||
|
||||
/**
|
||||
* show the new screen brightness on the overlay
|
||||
*
|
||||
* @param brightness the new screen brightness, in percent (range 0.0 - 100.0)
|
||||
*/
|
||||
fun showNewBrightness(brightness: Double) {
|
||||
feedbackTextView.text = "Brightness ${round(brightness).toInt()}%"
|
||||
showFeedbackView()
|
||||
}
|
||||
|
||||
/**
|
||||
* notify the user that a new swipe- session has started
|
||||
*/
|
||||
fun notifyEnterSwipeSession() {
|
||||
overlayRootView.performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS,
|
||||
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* notify the user that fling-to-mute was triggered
|
||||
*/
|
||||
fun notifyFlingToMutePerformed() {
|
||||
overlayRootView.performHapticFeedback(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) HapticFeedbackConstants.REJECT else HapticFeedbackConstants.LONG_PRESS,
|
||||
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* show the feedback view for a given time
|
||||
*/
|
||||
private fun showFeedbackView() {
|
||||
feedbackTextView.visibility = View.VISIBLE
|
||||
feedbackHideHandler.removeCallbacks(feedbackHideCallback)
|
||||
feedbackHideHandler.postDelayed(feedbackHideCallback, 500)
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package app.revanced.integrations.fenster.controllers
|
||||
|
||||
import android.app.Activity
|
||||
import app.revanced.integrations.fenster.util.clamp
|
||||
|
||||
/**
|
||||
* controller to adjust the screen brightness level
|
||||
*
|
||||
* @param host the host activity of which the brightness is adjusted
|
||||
*/
|
||||
class ScreenBrightnessController(
|
||||
private val host: Activity
|
||||
) {
|
||||
|
||||
/**
|
||||
* the current screen brightness in percent, ranging from 0.0 to 100.0
|
||||
*/
|
||||
var screenBrightness: Double
|
||||
get() {
|
||||
return host.window.attributes.screenBrightness * 100.0
|
||||
}
|
||||
set(value) {
|
||||
val attr = host.window.attributes
|
||||
attr.screenBrightness = (value.toFloat() / 100f).clamp(0f, 1f)
|
||||
host.window.attributes = attr
|
||||
}
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
package app.revanced.integrations.fenster.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
|
||||
/**
|
||||
* zones for swipe controls
|
||||
*/
|
||||
enum class SwipeControlZone {
|
||||
/**
|
||||
* not in any zone, should do nothing
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* in volume zone, adjust volume
|
||||
*/
|
||||
VOLUME_CONTROL,
|
||||
|
||||
/**
|
||||
* in brightness zone, adjust brightness
|
||||
*/
|
||||
BRIGHTNESS_CONTROL;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the control zone in which this motion event is
|
||||
*
|
||||
* @return the swipe control zone
|
||||
*/
|
||||
@Suppress("UnnecessaryVariable", "LocalVariableName")
|
||||
fun MotionEvent.getSwipeControlZone(context: Context): SwipeControlZone {
|
||||
// get screen size
|
||||
val screenWidth = device.getMotionRange(MotionEvent.AXIS_X).range
|
||||
val screenHeight = device.getMotionRange(MotionEvent.AXIS_Y).range
|
||||
|
||||
// check in what detection zone the event is in
|
||||
val _40dp = 40.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP).toFloat()
|
||||
val _80dp = 80.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP).toFloat()
|
||||
val _200dp = 200.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP).toFloat()
|
||||
|
||||
// Y- Axis:
|
||||
// -------- 0
|
||||
// ^
|
||||
// dead | 40dp
|
||||
// v
|
||||
// -------- yDeadTop
|
||||
// ^
|
||||
// swipe |
|
||||
// v
|
||||
// -------- yDeadBtm
|
||||
// ^
|
||||
// dead | 80dp
|
||||
// v
|
||||
// -------- screenHeight
|
||||
val yDeadTop = _40dp
|
||||
val yDeadBtm = screenHeight - _80dp
|
||||
|
||||
// X- Axis:
|
||||
// 0 xBrigStart xBrigEnd xVolStart xVolEnd screenWidth
|
||||
// | | | | | |
|
||||
// | 40dp | 200dp | | 200dp | 40dp |
|
||||
// | <------> | <------> | <------> | <------> | <------> |
|
||||
// | dead | brightness | dead | volume | dead |
|
||||
val xBrightStart = _40dp
|
||||
val xBrightEnd = xBrightStart + _200dp
|
||||
val xVolEnd = screenWidth - _40dp
|
||||
val xVolStart = xVolEnd - _200dp
|
||||
|
||||
// test detection zone
|
||||
if (y in yDeadTop..yDeadBtm) {
|
||||
return when (x) {
|
||||
in xBrightStart..xBrightEnd -> SwipeControlZone.BRIGHTNESS_CONTROL
|
||||
in xVolStart..xVolEnd -> SwipeControlZone.VOLUME_CONTROL
|
||||
else -> SwipeControlZone.NONE
|
||||
}
|
||||
}
|
||||
|
||||
// not in bounds
|
||||
return SwipeControlZone.NONE
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import app.revanced.integrations.fenster.WatchWhilePlayerType;
|
||||
import app.revanced.integrations.fenster.controllers.FensterController;
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
|
||||
/**
|
||||
* Hook receiver class for 'FensterV2' video swipe controls.
|
||||
*
|
||||
* @usedBy app.revanced.patches.youtube.interaction.fenster.patch.FensterPatch
|
||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class FensterSwipePatch {
|
||||
|
||||
/**
|
||||
* main fenster controller instance
|
||||
*/
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static final FensterController FENSTER = new FensterController();
|
||||
|
||||
/**
|
||||
* Hook into the main activity lifecycle
|
||||
*
|
||||
* @param thisRef reference to the WatchWhileActivity instance
|
||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;->WatchWhileActivity_onStartHookEX(Ljava/lang/Object;)V
|
||||
*/
|
||||
public static void WatchWhileActivity_onStartHookEX(Object thisRef) {
|
||||
if (thisRef == null) return;
|
||||
if (thisRef instanceof Activity) {
|
||||
FENSTER.initializeController((Activity) thisRef);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* hook into the player overlays lifecycle
|
||||
*
|
||||
* @param thisRef reference to the PlayerOverlays instance
|
||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;->YouTubePlayerOverlaysLayout_onFinishInflateHookEX(Ljava/lang/Object;)V
|
||||
*/
|
||||
public static void YouTubePlayerOverlaysLayout_onFinishInflateHookEX(Object thisRef) {
|
||||
if (thisRef == null) return;
|
||||
if (thisRef instanceof ViewGroup) {
|
||||
FENSTER.initializeOverlay((ViewGroup) thisRef);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into updatePlayerLayout() method
|
||||
*
|
||||
* @param type the new player type
|
||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;->YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(Ljava/lang/Object;)V
|
||||
*/
|
||||
public static void YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(Object type) {
|
||||
if (type == null) return;
|
||||
|
||||
// disable processing events if not watching fullscreen video
|
||||
WatchWhilePlayerType playerType = WatchWhilePlayerType.safeParseFromString(type.toString());
|
||||
FENSTER.setEnabled(playerType == WatchWhilePlayerType.WATCH_WHILE_FULLSCREEN);
|
||||
LogHelper.debug(FensterSwipePatch.class, "WatchWhile player type was updated to " + playerType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into NextGenWatchLayout.onTouchEvent
|
||||
*
|
||||
* @param thisRef reference to NextGenWatchLayout instance
|
||||
* @param motionEvent event parameter
|
||||
* @return was the event consumed by the hook?
|
||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;->NextGenWatchLayout_onTouchEventHookEX(Ljava/lang/Object;Ljava/lang/Object;)Z
|
||||
*/
|
||||
public static boolean NextGenWatchLayout_onTouchEventHookEX(Object thisRef, Object motionEvent) {
|
||||
if (motionEvent == null) return false;
|
||||
if (motionEvent instanceof MotionEvent) {
|
||||
return FENSTER.onTouchEvent((MotionEvent) motionEvent);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into NextGenWatchLayout.onInterceptTouchEvent
|
||||
*
|
||||
* @param thisRef reference to NextGenWatchLayout instance
|
||||
* @param motionEvent event parameter
|
||||
* @return was the event consumed by the hook?
|
||||
* @smali Lapp/revanced/integrations/patches/FensterSwipePatch;->NextGenWatchLayout_onInterceptTouchEventHookEX(Ljava/lang/Object;Ljava/lang/Object;)Z
|
||||
*/
|
||||
public static boolean NextGenWatchLayout_onInterceptTouchEventHookEX(Object thisRef, Object motionEvent) {
|
||||
if (motionEvent == null) return false;
|
||||
if (motionEvent instanceof MotionEvent) {
|
||||
return FENSTER.onTouchEvent((MotionEvent) motionEvent);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.utils.LogHelper;
|
||||
import app.revanced.integrations.utils.PlayerType;
|
||||
|
||||
/**
|
||||
* Hook receiver class for 'player-type-hook' patch
|
||||
*
|
||||
* @usedBy app.revanced.patches.youtube.misc.playertype.patch.PlayerTypeHookPatch
|
||||
* @smali Lapp/revanced/integrations/patches/PlayerTypeHookPatch;
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class PlayerTypeHookPatch {
|
||||
/**
|
||||
* Hook into YouTubePlayerOverlaysLayout.updatePlayerLayout() method
|
||||
*
|
||||
* @param type the new player type
|
||||
* @smali YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(Ljava/lang/Object;)V
|
||||
*/
|
||||
public static void YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX(@Nullable Object type) {
|
||||
if (type == null) return;
|
||||
|
||||
// update current player type
|
||||
final PlayerType newType = PlayerType.safeParseFromString(type.toString());
|
||||
if (newType != null) {
|
||||
PlayerType.setCurrent(newType);
|
||||
LogHelper.debug(PlayerTypeHookPatch.class, "YouTubePlayerOverlaysLayout player type was updated to " + newType);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package app.revanced.integrations.patches;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.integrations.swipecontrols.views.SwipeControlsHostLayout;
|
||||
|
||||
/**
|
||||
* Hook receiver class for 'swipe-controls' patch
|
||||
*
|
||||
* @usedBy app.revanced.patches.youtube.interaction.swipecontrols.patch.SwipeControlsPatch
|
||||
* @smali Lapp/revanced/integrations/patches/SwipeControlsPatch;
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class SwipeControlsPatch {
|
||||
/**
|
||||
* Hook into the main activity lifecycle
|
||||
* (using onStart here, but really anything up until onResume should be fine)
|
||||
*
|
||||
* @param thisRef reference to the WatchWhileActivity instance
|
||||
* @smali WatchWhileActivity_onStartHookEX(Ljava / lang / Object ;)V
|
||||
*/
|
||||
public static void WatchWhileActivity_onStartHookEX(@Nullable Object thisRef) {
|
||||
if (thisRef == null) return;
|
||||
if (thisRef instanceof Activity) {
|
||||
SwipeControlsHostLayout.attachTo((Activity) thisRef, false);
|
||||
}
|
||||
}
|
||||
}
|
@ -66,6 +66,12 @@ public enum SettingsEnum {
|
||||
//Swipe controls
|
||||
ENABLE_SWIPE_BRIGHTNESS_BOOLEAN("revanced_enable_swipe_brightness", true),
|
||||
ENABLE_SWIPE_VOLUME_BOOLEAN("revanced_enable_swipe_volume", true),
|
||||
ENABLE_PRESS_TO_SWIPE_BOOLEAN("revanced_enable_press_to_swipe", false),
|
||||
ENABLE_SWIPE_HAPTIC_FEEDBACK_BOOLEAN("revanced_enable_swipe_haptic_feedback", true),
|
||||
SWIPE_OVERLAY_TIMEOUT_LONG("revanced_swipe_overlay_timeout", 500L),
|
||||
SWIPE_OVERLAY_TEXT_SIZE_FLOAT("revanced_swipe_overlay_text_size", 22f),
|
||||
SWIPE_OVERLAY_BACKGROUND_ALPHA_INTEGER("revanced_swipe_overlay_background_alpha", 127),
|
||||
SWIPE_MAGNITUDE_THRESHOLD_FLOAT("revanced_swipe_magnitude_threshold", 30f),
|
||||
|
||||
//Buffer Settings
|
||||
MAX_BUFFER_INTEGER("revanced_pref_max_buffer_ms", 120000),
|
||||
|
@ -0,0 +1,89 @@
|
||||
package app.revanced.integrations.swipecontrols
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import app.revanced.integrations.settings.SettingsEnum
|
||||
import app.revanced.integrations.utils.PlayerType
|
||||
|
||||
/**
|
||||
* provider for configuration for volume and brightness swipe controls
|
||||
*
|
||||
* @param context the context to create in
|
||||
*/
|
||||
class SwipeControlsConfigurationProvider(
|
||||
private val context: Context
|
||||
) {
|
||||
//region swipe enable
|
||||
/**
|
||||
* should swipe controls be enabled? (global setting
|
||||
*/
|
||||
val enableSwipeControls: Boolean
|
||||
get() = isFullscreenVideo && (enableVolumeControls || enableBrightnessControl)
|
||||
|
||||
/**
|
||||
* should swipe controls for volume be enabled?
|
||||
*/
|
||||
val enableVolumeControls: Boolean
|
||||
get() = SettingsEnum.ENABLE_SWIPE_VOLUME_BOOLEAN.boolean
|
||||
|
||||
/**
|
||||
* should swipe controls for volume be enabled?
|
||||
*/
|
||||
val enableBrightnessControl: Boolean
|
||||
get() = SettingsEnum.ENABLE_SWIPE_BRIGHTNESS_BOOLEAN.boolean
|
||||
|
||||
/**
|
||||
* is the video player currently in fullscreen mode?
|
||||
*/
|
||||
private val isFullscreenVideo: Boolean
|
||||
get() = PlayerType.current == PlayerType.WATCH_WHILE_FULLSCREEN
|
||||
//endregion
|
||||
|
||||
//region gesture adjustments
|
||||
/**
|
||||
* should press-to-swipe be enabled?
|
||||
*/
|
||||
val shouldEnablePressToSwipe: Boolean
|
||||
get() = SettingsEnum.ENABLE_PRESS_TO_SWIPE_BOOLEAN.boolean
|
||||
|
||||
/**
|
||||
* threshold for swipe detection
|
||||
* this may be called rapidly in onScroll, so we have to load it once and then leave it constant
|
||||
*/
|
||||
val swipeMagnitudeThreshold: Float = SettingsEnum.SWIPE_MAGNITUDE_THRESHOLD_FLOAT.float
|
||||
//endregion
|
||||
|
||||
//region overlay adjustments
|
||||
|
||||
/**
|
||||
* should the overlay enable haptic feedback?
|
||||
*/
|
||||
val shouldEnableHapticFeedback: Boolean
|
||||
get() = SettingsEnum.ENABLE_SWIPE_HAPTIC_FEEDBACK_BOOLEAN.boolean
|
||||
|
||||
/**
|
||||
* how long the overlay should be shown on changes
|
||||
*/
|
||||
val overlayShowTimeoutMillis: Long
|
||||
get() = SettingsEnum.SWIPE_OVERLAY_TIMEOUT_LONG.long
|
||||
|
||||
/**
|
||||
* text size for the overlay, in sp
|
||||
*/
|
||||
val overlayTextSize: Float
|
||||
get() = SettingsEnum.SWIPE_OVERLAY_TEXT_SIZE_FLOAT.float
|
||||
|
||||
/**
|
||||
* get the background color for text on the overlay, as a color int
|
||||
*/
|
||||
val overlayTextBackgroundColor: Int
|
||||
get() = Color.argb(SettingsEnum.SWIPE_OVERLAY_BACKGROUND_ALPHA_INTEGER.int, 0, 0, 0)
|
||||
|
||||
/**
|
||||
* get the foreground color for text on the overlay, as a color int
|
||||
*/
|
||||
val overlayForegroundColor: Int
|
||||
get() = Color.WHITE
|
||||
|
||||
//endregion
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
package app.revanced.integrations.fenster.controllers
|
||||
package app.revanced.integrations.swipecontrols.controller
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import app.revanced.integrations.fenster.util.clamp
|
||||
import app.revanced.integrations.swipecontrols.misc.clamp
|
||||
import app.revanced.integrations.utils.LogHelper
|
||||
import kotlin.properties.Delegates
|
||||
|
@ -0,0 +1,65 @@
|
||||
package app.revanced.integrations.swipecontrols.controller
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.WindowManager
|
||||
import app.revanced.integrations.swipecontrols.misc.clamp
|
||||
|
||||
/**
|
||||
* controller to adjust the screen brightness level
|
||||
*
|
||||
* @param host the host activity of which the brightness is adjusted
|
||||
*/
|
||||
class ScreenBrightnessController(
|
||||
private val host: Activity
|
||||
) {
|
||||
/**
|
||||
* screen brightness saved by [save]
|
||||
*/
|
||||
private var savedScreenBrightness: Float? = null
|
||||
|
||||
/**
|
||||
* the current screen brightness in percent, ranging from 0.0 to 100.0
|
||||
*/
|
||||
var screenBrightness: Double
|
||||
get() = rawScreenBrightness * 100.0
|
||||
set(value) {
|
||||
rawScreenBrightness = (value.toFloat() / 100f).clamp(0f, 1f)
|
||||
}
|
||||
|
||||
/**
|
||||
* restore the screen brightness to the default device brightness
|
||||
*/
|
||||
fun restoreDefaultBrightness() {
|
||||
rawScreenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
||||
}
|
||||
|
||||
/**
|
||||
* save the current screen brightness, to be brought back using [restore]
|
||||
*/
|
||||
fun save() {
|
||||
if(savedScreenBrightness == null) {
|
||||
savedScreenBrightness = rawScreenBrightness
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* restore the screen brightness saved using [save]
|
||||
*/
|
||||
fun restore() {
|
||||
savedScreenBrightness?.let {
|
||||
rawScreenBrightness = it
|
||||
}
|
||||
savedScreenBrightness = null
|
||||
}
|
||||
|
||||
/**
|
||||
* wrapper for the raw screen brightness in [WindowManager.LayoutParams.screenBrightness]
|
||||
*/
|
||||
private var rawScreenBrightness: Float
|
||||
get() = host.window.attributes.screenBrightness
|
||||
set(value) {
|
||||
val attr = host.window.attributes
|
||||
attr.screenBrightness = value
|
||||
host.window.attributes = attr
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package app.revanced.integrations.swipecontrols.controller.gesture
|
||||
|
||||
import android.content.Context
|
||||
import android.view.MotionEvent
|
||||
import app.revanced.integrations.swipecontrols.views.SwipeControlsHostLayout
|
||||
|
||||
/**
|
||||
* [SwipeGestureController], but with press-to-swipe disabled because a lot of people dislike the feature.
|
||||
* If you want to change something, try to do it in [SwipeGestureController] so that both configurations can benefit from it
|
||||
*/
|
||||
class NoPtSSwipeGestureController(context: Context, controller: SwipeControlsHostLayout) :
|
||||
SwipeGestureController(context, controller) {
|
||||
|
||||
/**
|
||||
* to disable press-to-swipe, we have to become press-to-swipe
|
||||
*/
|
||||
override var inSwipeSession
|
||||
get() = true
|
||||
set(_) {}
|
||||
|
||||
override fun onLongPress(e: MotionEvent?) {
|
||||
if (e == null) return
|
||||
|
||||
// send GestureDetector a ACTION_CANCEL event so it will handle further events
|
||||
// if this is left out, swipe-to-dismiss is triggered when scrolling down
|
||||
e.action = MotionEvent.ACTION_CANCEL
|
||||
detector.onTouchEvent(e)
|
||||
}
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
package app.revanced.integrations.swipecontrols.controller.gesture
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import app.revanced.integrations.swipecontrols.views.SwipeControlsHostLayout
|
||||
import app.revanced.integrations.swipecontrols.misc.ScrollDistanceHelper
|
||||
import app.revanced.integrations.swipecontrols.misc.applyDimension
|
||||
import app.revanced.integrations.swipecontrols.misc.contains
|
||||
import app.revanced.integrations.swipecontrols.misc.toPoint
|
||||
import app.revanced.integrations.utils.LogHelper
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* base gesture controller for volume and brightness swipe controls controls, with press-to-swipe enabled
|
||||
* for the controller without press-to-swipe, see [NoPtSSwipeGestureController]
|
||||
*
|
||||
* @param context the context to create in
|
||||
* @param controller reference to main controller instance
|
||||
*/
|
||||
@Suppress("LeakingThis")
|
||||
open class SwipeGestureController(
|
||||
context: Context,
|
||||
private val controller: SwipeControlsHostLayout
|
||||
) :
|
||||
GestureDetector.SimpleOnGestureListener(),
|
||||
SwipeControlsHostLayout.TouchEventListener {
|
||||
|
||||
/**
|
||||
* the main gesture detector that powers everything
|
||||
*/
|
||||
protected open val detector = GestureDetector(context, this)
|
||||
|
||||
/**
|
||||
* to enable swipe controls, users must first long- press. this flags monitors that long- press
|
||||
* NOTE: if you dislike press-to-swipe, and want it disabled, have a look at [NoPtSSwipeGestureController]. it does exactly that
|
||||
*/
|
||||
protected open var inSwipeSession = true
|
||||
|
||||
/**
|
||||
* currently in- progress swipe
|
||||
*/
|
||||
protected open var currentSwipe: SwipeDirection = SwipeDirection.NONE
|
||||
|
||||
/**
|
||||
* were downstream event cancelled already? used by [onScroll]
|
||||
*/
|
||||
protected open var didCancelDownstream = false
|
||||
|
||||
/**
|
||||
* should [onTouchEvent] force- intercept all touch events?
|
||||
*/
|
||||
protected open val shouldForceInterceptEvents: Boolean
|
||||
get() = currentSwipe == SwipeDirection.VERTICAL && inSwipeSession
|
||||
|
||||
/**
|
||||
* scroller for volume adjustment
|
||||
*/
|
||||
protected open val volumeScroller = ScrollDistanceHelper(
|
||||
10.applyDimension(
|
||||
context,
|
||||
TypedValue.COMPLEX_UNIT_DIP
|
||||
)
|
||||
) { _, _, direction ->
|
||||
controller.audio?.apply {
|
||||
volume += direction
|
||||
controller.overlay.onVolumeChanged(volume, maxVolume)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* scroller for screen brightness adjustment
|
||||
*/
|
||||
protected open val brightnessScroller = ScrollDistanceHelper(
|
||||
1.applyDimension(
|
||||
context,
|
||||
TypedValue.COMPLEX_UNIT_DIP
|
||||
)
|
||||
) { _, _, direction ->
|
||||
controller.screen?.apply {
|
||||
if (screenBrightness > 0 || direction > 0) {
|
||||
screenBrightness += direction
|
||||
} else {
|
||||
restoreDefaultBrightness()
|
||||
}
|
||||
|
||||
controller.overlay.onBrightnessChanged(screenBrightness)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTouchEvent(motionEvent: MotionEvent): Boolean {
|
||||
if (!controller.config.enableSwipeControls) {
|
||||
return false
|
||||
}
|
||||
if (motionEvent.action == MotionEvent.ACTION_UP) {
|
||||
onUp(motionEvent)
|
||||
}
|
||||
|
||||
return detector.onTouchEvent(motionEvent) or shouldForceInterceptEvents
|
||||
}
|
||||
|
||||
/**
|
||||
* custom handler for ACTION_UP event, because GestureDetector doesn't offer that :|
|
||||
* basically just resets all flags to non- swiping values
|
||||
*
|
||||
* @param e the motion event
|
||||
*/
|
||||
open fun onUp(e: MotionEvent) {
|
||||
LogHelper.debug(this.javaClass, "onUp(${e.x}, ${e.y}, ${e.action})")
|
||||
inSwipeSession = false
|
||||
currentSwipe = SwipeDirection.NONE
|
||||
didCancelDownstream = false
|
||||
volumeScroller.reset()
|
||||
brightnessScroller.reset()
|
||||
}
|
||||
|
||||
override fun onLongPress(e: MotionEvent?) {
|
||||
if (e == null) return
|
||||
LogHelper.debug(this.javaClass, "onLongPress(${e.x}, ${e.y}, ${e.action})")
|
||||
|
||||
// enter swipe session with feedback
|
||||
inSwipeSession = true
|
||||
controller.overlay.onEnterSwipeSession()
|
||||
|
||||
// send GestureDetector a ACTION_CANCEL event so it will handle further events
|
||||
e.action = MotionEvent.ACTION_CANCEL
|
||||
detector.onTouchEvent(e)
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
eFrom: MotionEvent?,
|
||||
eTo: MotionEvent?,
|
||||
disX: Float,
|
||||
disY: Float
|
||||
): Boolean {
|
||||
if (eFrom == null || eTo == null) return false
|
||||
LogHelper.debug(
|
||||
this.javaClass,
|
||||
"onScroll(from: [${eFrom.x}, ${eFrom.y}, ${eFrom.action}], to: [${eTo.x}, ${eTo.y}, ${eTo.action}], d: [$disX, $disY])"
|
||||
)
|
||||
|
||||
return when (currentSwipe) {
|
||||
// no swipe direction was detected yet, try to detect one
|
||||
// if the user did not swipe far enough, we cannot detect what direction they swiped
|
||||
// so we wait until a greater distance was swiped
|
||||
// NOTE: sqrt() can be high- cost, so using squared magnitudes here
|
||||
SwipeDirection.NONE -> {
|
||||
val deltaX = abs(eTo.x - eFrom.x)
|
||||
val deltaY = abs(eTo.y - eFrom.y)
|
||||
val swipeMagnitudeSquared = deltaX.pow(2) + deltaY.pow(2)
|
||||
if (swipeMagnitudeSquared > controller.config.swipeMagnitudeThreshold.pow(2)) {
|
||||
currentSwipe = if (deltaY > deltaX) {
|
||||
SwipeDirection.VERTICAL
|
||||
} else {
|
||||
SwipeDirection.HORIZONTAL
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// horizontal swipe, we should leave this one for downstream to handle
|
||||
SwipeDirection.HORIZONTAL -> false
|
||||
|
||||
// vertical swipe, could be for us
|
||||
SwipeDirection.VERTICAL -> {
|
||||
if (!inSwipeSession) {
|
||||
// not in swipe session, let downstream handle this one
|
||||
return false
|
||||
}
|
||||
|
||||
// vertical & in swipe session, handle this one:
|
||||
// first, send ACTION_CANCEL to downstream to let them known they should stop tracking events
|
||||
if (!didCancelDownstream) {
|
||||
val eCancel = MotionEvent.obtain(eFrom)
|
||||
eCancel.action = MotionEvent.ACTION_CANCEL
|
||||
controller.dispatchDownstreamTouchEvent(eCancel)
|
||||
eCancel.recycle()
|
||||
didCancelDownstream = true
|
||||
}
|
||||
|
||||
// then, process the event
|
||||
when (eFrom.toPoint()) {
|
||||
in controller.volumeZone -> volumeScroller.add(disY.toDouble())
|
||||
in controller.brightnessZone -> brightnessScroller.add(disY.toDouble())
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* direction of a swipe
|
||||
*/
|
||||
enum class SwipeDirection {
|
||||
/**
|
||||
* swipe has no direction or no swipe
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* swipe along the X- Axes
|
||||
*/
|
||||
HORIZONTAL,
|
||||
|
||||
/**
|
||||
* swipe along the Y- Axes
|
||||
*/
|
||||
VERTICAL
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package app.revanced.integrations.swipecontrols.misc
|
||||
|
||||
import android.view.MotionEvent
|
||||
|
||||
/**
|
||||
* a simple 2D point class
|
||||
*/
|
||||
data class Point(
|
||||
val x: Int,
|
||||
val y: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* convert the motion event coordinates to a point
|
||||
*/
|
||||
fun MotionEvent.toPoint(): Point =
|
||||
Point(x.toInt(), y.toInt())
|
@ -0,0 +1,23 @@
|
||||
package app.revanced.integrations.swipecontrols.misc
|
||||
|
||||
/**
|
||||
* a simple rectangle class
|
||||
*/
|
||||
data class Rectangle(
|
||||
val x: Int,
|
||||
val y: Int,
|
||||
val width: Int,
|
||||
val height: Int
|
||||
) {
|
||||
val left = x
|
||||
val right = x + width
|
||||
val top = y
|
||||
val bottom = y + height
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* is the point within this rectangle?
|
||||
*/
|
||||
operator fun Rectangle.contains(p: Point): Boolean =
|
||||
p.x in left..right && p.y in top..bottom
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.integrations.fenster.util
|
||||
package app.revanced.integrations.swipecontrols.misc
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.sign
|
@ -0,0 +1,26 @@
|
||||
package app.revanced.integrations.swipecontrols.misc
|
||||
|
||||
/**
|
||||
* Interface for all overlays for swipe controls
|
||||
*/
|
||||
interface SwipeControlsOverlay {
|
||||
/**
|
||||
* called when the currently set volume level was changed
|
||||
*
|
||||
* @param newVolume the new volume level
|
||||
* @param maximumVolume the maximum volume index
|
||||
*/
|
||||
fun onVolumeChanged(newVolume: Int, maximumVolume: Int)
|
||||
|
||||
/**
|
||||
* called when the currently set screen brightness was changed
|
||||
*
|
||||
* @param brightness the new screen brightness, in percent (range 0.0 - 100.0)
|
||||
*/
|
||||
fun onBrightnessChanged(brightness: Double)
|
||||
|
||||
/**
|
||||
* called when a new swipe- session has started
|
||||
*/
|
||||
fun onEnterSwipeSession()
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package app.revanced.integrations.fenster.util
|
||||
package app.revanced.integrations.swipecontrols.misc
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
@ -0,0 +1,73 @@
|
||||
package app.revanced.integrations.swipecontrols.misc
|
||||
|
||||
import android.content.Context
|
||||
import android.util.TypedValue
|
||||
|
||||
//TODO reimplement this, again with 1/3rd for the zone size
|
||||
// because in shorts, the screen is way less wide than this code expects!
|
||||
/**
|
||||
* Y- Axis:
|
||||
* -------- 0
|
||||
* ^
|
||||
* dead | 40dp
|
||||
* v
|
||||
* -------- yDeadTop
|
||||
* ^
|
||||
* swipe |
|
||||
* v
|
||||
* -------- yDeadBtm
|
||||
* ^
|
||||
* dead | 80dp
|
||||
* v
|
||||
* -------- screenHeight
|
||||
*
|
||||
* X- Axis:
|
||||
* 0 xBrigStart xBrigEnd xVolStart xVolEnd screenWidth
|
||||
* | | | | | |
|
||||
* | 40dp | 200dp | | 200dp | 40dp |
|
||||
* | <------> | <------> | <------> | <------> | <------> |
|
||||
* | dead | brightness | dead | volume | dead |
|
||||
*/
|
||||
@Suppress("LocalVariableName")
|
||||
object SwipeZonesHelper {
|
||||
|
||||
/**
|
||||
* get the zone for volume control
|
||||
*
|
||||
* @param context the current context
|
||||
* @param screenRect the screen rectangle in the current orientation
|
||||
* @return the rectangle for the control zone
|
||||
*/
|
||||
fun getVolumeControlZone(context: Context, screenRect: Rectangle): Rectangle {
|
||||
val _40dp = 40.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
val _80dp = 80.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
val _200dp = 200.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
|
||||
return Rectangle(
|
||||
screenRect.right - _40dp - _200dp,
|
||||
screenRect.top + _40dp,
|
||||
_200dp,
|
||||
screenRect.height - _40dp - _80dp
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* get the zone for brightness control
|
||||
*
|
||||
* @param context the current context
|
||||
* @param screenRect the screen rectangle in the current orientation
|
||||
* @return the rectangle for the control zone
|
||||
*/
|
||||
fun getBrightnessControlZone(context: Context, screenRect: Rectangle): Rectangle {
|
||||
val _40dp = 40.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
val _80dp = 80.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
val _200dp = 200.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
|
||||
return Rectangle(
|
||||
screenRect.left + _40dp,
|
||||
screenRect.top + _40dp,
|
||||
_200dp,
|
||||
screenRect.height - _40dp - _80dp
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
package app.revanced.integrations.swipecontrols.views
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.graphics.Color
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import app.revanced.integrations.swipecontrols.SwipeControlsConfigurationProvider
|
||||
import app.revanced.integrations.swipecontrols.controller.AudioVolumeController
|
||||
import app.revanced.integrations.swipecontrols.controller.ScreenBrightnessController
|
||||
import app.revanced.integrations.swipecontrols.controller.gesture.NoPtSSwipeGestureController
|
||||
import app.revanced.integrations.swipecontrols.controller.gesture.SwipeGestureController
|
||||
import app.revanced.integrations.swipecontrols.misc.Rectangle
|
||||
import app.revanced.integrations.swipecontrols.misc.SwipeControlsOverlay
|
||||
import app.revanced.integrations.swipecontrols.misc.SwipeZonesHelper
|
||||
import app.revanced.integrations.utils.LogHelper
|
||||
import app.revanced.integrations.utils.PlayerType
|
||||
|
||||
/**
|
||||
* The main controller for volume and brightness swipe controls
|
||||
*
|
||||
* @param hostActivity the activity that should host the controller
|
||||
* @param debugTouchableZone show a overlay on all zones covered by this layout
|
||||
*/
|
||||
@SuppressLint("ViewConstructor")
|
||||
class SwipeControlsHostLayout(
|
||||
private val hostActivity: Activity,
|
||||
private val mainContentChild: View,
|
||||
debugTouchableZone: Boolean = false
|
||||
) : FrameLayout(hostActivity) {
|
||||
init {
|
||||
isFocusable = false
|
||||
isClickable = false
|
||||
|
||||
if (debugTouchableZone) {
|
||||
val zoneOverlay = View(context).apply {
|
||||
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
||||
setBackgroundColor(Color.argb(50, 0, 255, 0))
|
||||
z = 9999f
|
||||
}
|
||||
addView(zoneOverlay)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* current instance of [AudioVolumeController]
|
||||
*/
|
||||
val audio: AudioVolumeController?
|
||||
|
||||
/**
|
||||
* current instance of [ScreenBrightnessController]
|
||||
*/
|
||||
val screen: ScreenBrightnessController?
|
||||
|
||||
/**
|
||||
* current instance of [SwipeControlsConfigurationProvider]
|
||||
*/
|
||||
val config: SwipeControlsConfigurationProvider
|
||||
|
||||
/**
|
||||
* current instance of [SwipeControlsOverlayLayout]
|
||||
*/
|
||||
val overlay: SwipeControlsOverlay
|
||||
|
||||
/**
|
||||
* main gesture controller
|
||||
*/
|
||||
private val gesture: SwipeGestureController
|
||||
|
||||
init {
|
||||
// create controllers
|
||||
LogHelper.info(this.javaClass, "initializing swipe controls controllers")
|
||||
config = SwipeControlsConfigurationProvider(hostActivity)
|
||||
gesture = createGestureController()
|
||||
audio = createAudioController()
|
||||
screen = createScreenController()
|
||||
|
||||
// create overlay
|
||||
SwipeControlsOverlayLayout(hostActivity).let {
|
||||
overlay = it
|
||||
addView(it)
|
||||
}
|
||||
|
||||
// listen for changes in the player type
|
||||
PlayerType.onChange += this::onPlayerTypeChanged
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
return if (ev != null && gesture.onTouchEvent(ev)) true else {
|
||||
super.dispatchTouchEvent(ev)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addView(child: View?, index: Int, params: ViewGroup.LayoutParams?) {
|
||||
// main content is always at index 0, all other are inserted after
|
||||
if (child == mainContentChild) {
|
||||
super.addView(child, 0, params)
|
||||
} else {
|
||||
super.addView(child, childCount, params)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* called when the player type changes
|
||||
*
|
||||
* @param type the new player type
|
||||
*/
|
||||
private fun onPlayerTypeChanged(type: PlayerType) {
|
||||
when (type) {
|
||||
PlayerType.WATCH_WHILE_FULLSCREEN -> screen?.restore()
|
||||
else -> {
|
||||
screen?.save()
|
||||
screen?.restoreDefaultBrightness()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* dispatch a touch event to downstream views
|
||||
*
|
||||
* @param event the event to dispatch
|
||||
* @return was the event consumed?
|
||||
*/
|
||||
fun dispatchDownstreamTouchEvent(event: MotionEvent) =
|
||||
super.dispatchTouchEvent(event)
|
||||
|
||||
/**
|
||||
* create the audio volume controller
|
||||
*/
|
||||
private fun createAudioController() =
|
||||
if (config.enableVolumeControls)
|
||||
AudioVolumeController(context) else null
|
||||
|
||||
/**
|
||||
* create the screen brightness controller instance
|
||||
*/
|
||||
private fun createScreenController() =
|
||||
if (config.enableBrightnessControl)
|
||||
ScreenBrightnessController(hostActivity) else null
|
||||
|
||||
/**
|
||||
* create the gesture controller based on settings
|
||||
*/
|
||||
private fun createGestureController() =
|
||||
if (config.shouldEnablePressToSwipe)
|
||||
SwipeGestureController(hostActivity, this)
|
||||
else NoPtSSwipeGestureController(hostActivity, this)
|
||||
|
||||
/**
|
||||
* the current screen rectangle
|
||||
*/
|
||||
private val screenRect: Rectangle
|
||||
get() = Rectangle(x.toInt(), y.toInt(), width, height)
|
||||
|
||||
/**
|
||||
* the rectangle of the volume control zone
|
||||
*/
|
||||
val volumeZone: Rectangle
|
||||
get() = SwipeZonesHelper.getVolumeControlZone(hostActivity, screenRect)
|
||||
|
||||
/**
|
||||
* the rectangle of the screen brightness control zone
|
||||
*/
|
||||
val brightnessZone: Rectangle
|
||||
get() = SwipeZonesHelper.getBrightnessControlZone(hostActivity, screenRect)
|
||||
|
||||
|
||||
interface TouchEventListener {
|
||||
/**
|
||||
* touch event callback
|
||||
*
|
||||
* @param motionEvent the motion event that was received
|
||||
* @return intercept the event? if true, child views will not receive the event
|
||||
*/
|
||||
fun onTouchEvent(motionEvent: MotionEvent): Boolean
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* attach a [SwipeControlsHostLayout] to the activity
|
||||
*
|
||||
* @param debugTouchableZone show a overlay on all zones covered by this layout
|
||||
* @return the attached instance
|
||||
*/
|
||||
@JvmStatic
|
||||
fun Activity.attachTo(debugTouchableZone: Boolean = false): SwipeControlsHostLayout {
|
||||
// get targets
|
||||
val contentView: ViewGroup = window.decorView.findViewById(android.R.id.content)!!
|
||||
var content = contentView.getChildAt(0)
|
||||
|
||||
// detach previously attached swipe host first
|
||||
if (content is SwipeControlsHostLayout) {
|
||||
contentView.removeView(content)
|
||||
content.removeAllViews()
|
||||
content = content.mainContentChild
|
||||
}
|
||||
|
||||
// create swipe host
|
||||
val swipeHost = SwipeControlsHostLayout(this, content, debugTouchableZone).apply {
|
||||
layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
}
|
||||
|
||||
// insert the swipe host as parent to the actual content
|
||||
contentView.removeView(content)
|
||||
contentView.addView(swipeHost)
|
||||
swipeHost.addView(content)
|
||||
return swipeHost
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
package app.revanced.integrations.swipecontrols.views
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import app.revanced.integrations.swipecontrols.SwipeControlsConfigurationProvider
|
||||
import app.revanced.integrations.swipecontrols.misc.SwipeControlsOverlay
|
||||
import app.revanced.integrations.swipecontrols.misc.applyDimension
|
||||
import app.revanced.integrations.utils.ReVancedUtils
|
||||
import kotlin.math.round
|
||||
|
||||
/**
|
||||
* main overlay layout for volume and brightness swipe controls
|
||||
*
|
||||
* @param context context to create in
|
||||
*/
|
||||
class SwipeControlsOverlayLayout(
|
||||
context: Context,
|
||||
private val config: SwipeControlsConfigurationProvider
|
||||
) : RelativeLayout(context), SwipeControlsOverlay {
|
||||
/**
|
||||
* DO NOT use this, for tools only
|
||||
*/
|
||||
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context))
|
||||
|
||||
private val feedbackTextView: TextView
|
||||
private val autoBrightnessIcon: Drawable
|
||||
private val manualBrightnessIcon: Drawable
|
||||
private val mutedVolumeIcon: Drawable
|
||||
private val normalVolumeIcon: Drawable
|
||||
|
||||
private fun getDrawable(name: String, width: Int, height: Int): Drawable {
|
||||
return resources.getDrawable(
|
||||
ReVancedUtils.getResourceIdByName(context, "drawable", name),
|
||||
context.theme
|
||||
).apply {
|
||||
setTint(config.overlayForegroundColor)
|
||||
setBounds(
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
// init views
|
||||
val feedbackTextViewPadding = 2.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
feedbackTextView = TextView(context).apply {
|
||||
layoutParams = LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
addRule(CENTER_IN_PARENT, TRUE)
|
||||
setPadding(
|
||||
feedbackTextViewPadding,
|
||||
feedbackTextViewPadding,
|
||||
feedbackTextViewPadding,
|
||||
feedbackTextViewPadding
|
||||
)
|
||||
}
|
||||
background = GradientDrawable().apply {
|
||||
cornerRadius = 8f
|
||||
setColor(config.overlayTextBackgroundColor)
|
||||
}
|
||||
setTextColor(config.overlayForegroundColor)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, config.overlayTextSize)
|
||||
compoundDrawablePadding = compoundIconPadding
|
||||
visibility = GONE
|
||||
}
|
||||
addView(feedbackTextView)
|
||||
|
||||
// get icons scaled, assuming square icons
|
||||
val iconHeight = round(feedbackTextView.lineHeight * .8).toInt()
|
||||
autoBrightnessIcon = getDrawable("ic_sc_brightness_auto", iconHeight, iconHeight)
|
||||
manualBrightnessIcon = getDrawable("ic_sc_brightness_manual", iconHeight, iconHeight)
|
||||
mutedVolumeIcon = getDrawable("ic_sc_volume_mute", iconHeight, iconHeight)
|
||||
normalVolumeIcon = getDrawable("ic_sc_volume_normal", iconHeight, iconHeight)
|
||||
}
|
||||
|
||||
private val feedbackHideHandler = Handler(Looper.getMainLooper())
|
||||
private val feedbackHideCallback = Runnable {
|
||||
feedbackTextView.visibility = View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
* show the feedback view for a given time
|
||||
*
|
||||
* @param message the message to show
|
||||
* @param icon the icon to use
|
||||
*/
|
||||
private fun showFeedbackView(message: String, icon: Drawable) {
|
||||
feedbackHideHandler.removeCallbacks(feedbackHideCallback)
|
||||
feedbackHideHandler.postDelayed(feedbackHideCallback, config.overlayShowTimeoutMillis)
|
||||
feedbackTextView.apply {
|
||||
text = message
|
||||
setCompoundDrawablesRelative(
|
||||
icon,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
)
|
||||
visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
|
||||
showFeedbackView(
|
||||
"$newVolume",
|
||||
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBrightnessChanged(brightness: Double) {
|
||||
if (brightness > 0) {
|
||||
showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon)
|
||||
} else {
|
||||
showFeedbackView("AUTO", autoBrightnessIcon)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEnterSwipeSession() {
|
||||
if (config.shouldEnableHapticFeedback) {
|
||||
performHapticFeedback(
|
||||
HapticFeedbackConstants.LONG_PRESS,
|
||||
HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
22
app/src/main/java/app/revanced/integrations/utils/Event.kt
Normal file
22
app/src/main/java/app/revanced/integrations/utils/Event.kt
Normal file
@ -0,0 +1,22 @@
|
||||
package app.revanced.integrations.utils
|
||||
|
||||
/**
|
||||
* generic event provider class
|
||||
*/
|
||||
class Event<T> {
|
||||
private val eventListeners = mutableSetOf<(T) -> Unit>()
|
||||
|
||||
operator fun plusAssign(observer: (T) -> Unit) {
|
||||
eventListeners.add(observer)
|
||||
}
|
||||
|
||||
operator fun minusAssign(observer: (T) -> Unit) {
|
||||
eventListeners.remove(observer)
|
||||
}
|
||||
|
||||
operator fun invoke(value: T) {
|
||||
for (observer in eventListeners)
|
||||
observer.invoke(value)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
package app.revanced.integrations.utils
|
||||
|
||||
/**
|
||||
* WatchWhile player type
|
||||
*/
|
||||
@Suppress("unused")
|
||||
enum class PlayerType {
|
||||
NONE,
|
||||
HIDDEN,
|
||||
WATCH_WHILE_MINIMIZED,
|
||||
WATCH_WHILE_MAXIMIZED,
|
||||
WATCH_WHILE_FULLSCREEN,
|
||||
WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN,
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED,
|
||||
WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED,
|
||||
WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED,
|
||||
INLINE_MINIMAL,
|
||||
VIRTUAL_REALITY_FULLSCREEN,
|
||||
WATCH_WHILE_PICTURE_IN_PICTURE;
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* safely parse from a string
|
||||
*
|
||||
* @param name the name to find
|
||||
* @return the enum constant, or null if not found
|
||||
*/
|
||||
@JvmStatic
|
||||
fun safeParseFromString(name: String): PlayerType? {
|
||||
return values().firstOrNull { it.name == name }
|
||||
}
|
||||
|
||||
/**
|
||||
* the current player type, as reported by [app.revanced.integrations.patches.PlayerTypeHookPatch.YouTubePlayerOverlaysLayout_updatePlayerTypeHookEX]
|
||||
*/
|
||||
@JvmStatic
|
||||
var current
|
||||
get() = currentPlayerType
|
||||
set(value) {
|
||||
currentPlayerType = value
|
||||
onChange(currentPlayerType)
|
||||
}
|
||||
private var currentPlayerType = NONE
|
||||
|
||||
/**
|
||||
* player type change listener
|
||||
*/
|
||||
val onChange = Event<PlayerType>()
|
||||
}
|
||||
}
|
@ -51,6 +51,16 @@ public class ReVancedUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public static Integer getResourceIdByName(Context context, String type, String name) {
|
||||
try {
|
||||
Resources res = context.getResources();
|
||||
return res.getIdentifier(name, type, context.getPackageName());
|
||||
} catch (Throwable exception) {
|
||||
LogHelper.printException(ReVancedUtils.class, "Resource not found.", exception);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void setPlayerType(PlayerType type) {
|
||||
env = type;
|
||||
}
|
||||
|
5
app/src/main/res/drawable/ic_sc_brightness_auto.xml
Normal file
5
app/src/main/res/drawable/ic_sc_brightness_auto.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M10.85,12.65h2.3L12,9l-1.15,3.65zM20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69L23.31,12 20,8.69zM14.3,16l-0.7,-2h-3.2l-0.7,2H7.8L11,7h2l3.2,9h-1.9z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_sc_brightness_manual.xml
Normal file
5
app/src/main/res/drawable/ic_sc_brightness_manual.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,15.31L23.31,12 20,8.69V4h-4.69L12,0.69 8.69,4H4v4.69L0.69,12 4,15.31V20h4.69L12,23.31 15.31,20H20v-4.69zM12,18V6c3.31,0 6,2.69 6,6s-2.69,6 -6,6z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_sc_volume_mute.xml
Normal file
5
app/src/main/res/drawable/ic_sc_volume_mute.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v2.21l2.45,2.45c0.03,-0.2 0.05,-0.41 0.05,-0.63zM19,12c0,0.94 -0.2,1.82 -0.54,2.64l1.51,1.51C20.63,14.91 21,13.5 21,12c0,-4.28 -2.99,-7.86 -7,-8.77v2.06c2.89,0.86 5,3.54 5,6.71zM4.27,3L3,4.27 7.73,9L3,9v6h4l5,5v-6.73l4.25,4.25c-0.67,0.52 -1.42,0.93 -2.25,1.18v2.06c1.38,-0.31 2.63,-0.95 3.69,-1.81L19.73,21 21,19.73l-9,-9L4.27,3zM12,4L9.91,6.09 12,8.18L12,4z"/>
|
||||
</vector>
|
5
app/src/main/res/drawable/ic_sc_volume_normal.xml
Normal file
5
app/src/main/res/drawable/ic_sc_volume_normal.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M3,9v6h4l5,5L12,4L7,9L3,9zM16.5,12c0,-1.77 -1.02,-3.29 -2.5,-4.03v8.05c1.48,-0.73 2.5,-2.25 2.5,-4.02zM14,3.23v2.06c2.89,0.86 5,3.54 5,6.71s-2.11,5.85 -5,6.71v2.06c4.01,-0.91 7,-4.49 7,-8.77s-2.99,-7.86 -7,-8.77z"/>
|
||||
</vector>
|
@ -93,6 +93,20 @@
|
||||
<string name="revanced_xfenster_volume_summary_off">Swipe controls for volume are disabled</string>
|
||||
<string name="revanced_xfenster_volume_summary_on">Swipe controls for volume are enabled</string>
|
||||
<string name="revanced_xfenster_volume_title">Swipe controls for Volume</string>
|
||||
<string name="revanced_swipe_pts_title">Press-to-Swipe</string>
|
||||
<string name="revanced_swipe_pts_summary_off">Swipe controls are always active</string>
|
||||
<string name="revanced_swipe_pts_summary_on">Swipe controls require a long-press before activating</string>
|
||||
<string name="revanced_swipe_pts_haptic_title">Vibrate on Press-to-Swipe</string>
|
||||
<string name="revanced_swipe_pts_haptic_summary_on">You\'ll get haptic feedback when activating Press-to-Swipe</string>
|
||||
<string name="revanced_swipe_pts_haptic_summary_off">You won\'t get haptic feedback when activating Press-to-Swipe</string>
|
||||
<string name="revanced_swipe_overlay_timeout_title">Overlay Timeout</string>
|
||||
<string name="revanced_swipe_overlay_timeout_summary">How long the overlay is shown after changes (ms)</string>
|
||||
<string name="revanced_swipe_overlay_text_size_title">Overlay Text Size</string>
|
||||
<string name="revanced_swipe_overlay_text_size_summary">Text size on the overlay</string>
|
||||
<string name="revanced_swipe_overlay_bg_alpha_title">Overlay Background Transparency</string>
|
||||
<string name="revanced_swipe_overlay_bg_alpha_summary">Transparency value of the overlay background (0–255)</string>
|
||||
<string name="revanced_swipe_magnitude_threshold_title">Swipe Magnitude Threshold</string>
|
||||
<string name="revanced_swipe_magnitude_threshold_summary">Minimum magnitude before a swipe is detected</string>
|
||||
<string name="revanced_website_summary">Tap to open our website</string>
|
||||
<string name="revanced_website_title">ReVanced website</string>
|
||||
<string name="revanced_home_ads_summary_off">Home ADS are hidden</string>
|
||||
|
@ -57,11 +57,12 @@
|
||||
<PreferenceScreen android:title="@string/revanced_xfenster_title" android:key="xfenster_screen" android:summary="@string/revanced_xfenster_screen_summary">
|
||||
<SwitchPreference android:title="@string/revanced_xfenster_brightness_title" android:key="revanced_enable_swipe_brightness" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_brightness_summary_on" android:summaryOff="@string/revanced_xfenster_brightness_summary_off" />
|
||||
<SwitchPreference android:title="@string/revanced_xfenster_volume_title" android:key="revanced_enable_swipe_volume" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_volume_summary_on" android:summaryOff="@string/revanced_xfenster_volume_summary_off" />
|
||||
<SwitchPreference android:title="@string/revanced_xfenster_tablet_title" android:key="revanced_swipe_tablet_mode" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_tablet_summary_on" android:summaryOff="@string/revanced_xfenster_tablet_summary_off" />
|
||||
<EditTextPreference android:numeric="integer" android:title="@string/revanced_swipe_threshold_title" android:key="revanced_swipe_threshold" android:summary="@string/revanced_swipe_threshold_summary" android:defaultValue="0" />
|
||||
<EditTextPreference android:numeric="integer" android:title="@string/revanced_swipe_padding_top_title" android:key="revanced_swipe_padding_top" android:summary="@string/revanced_swipe_padding_top_summary" android:defaultValue="20" />
|
||||
<SwitchPreference android:title="@string/revanced_xfenster_brightness_title" android:key="pref_xfenster_brightness" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_brightness_summary_on" android:summaryOff="@string/revanced_xfenster_brightness_summary_off" />
|
||||
<SwitchPreference android:title="@string/revanced_xfenster_volume_title" android:key="pref_xfenster_volume" android:defaultValue="false" android:summaryOn="@string/revanced_xfenster_volume_summary_on" android:summaryOff="@string/revanced_xfenster_volume_summary_off" />
|
||||
<SwitchPreference android:title="@string/revanced_swipe_pts_title" android:key="revanced_enable_press_to_swipe" android:defaultValue="false" android:summaryOn="@string/revanced_swipe_pts_summary_on" android:summaryOff="@string/revanced_swipe_pts_summary_off" />
|
||||
<SwitchPreference android:title="@string/revanced_swipe_pts_haptic_title" android:key="revanced_enable_swipe_haptic_feedback" android:defaultValue="false" android:summaryOn="@string/revanced_swipe_pts_haptic_summary_on" android:summaryOff="@string/revanced_swipe_pts_summary_off" />
|
||||
<EditTextPreference android:numeric="integer" android:title="@string/revanced_swipe_overlay_timeout_title" android:key="revanced_swipe_overlay_timeout" android:summary="@string/revanced_swipe_overlay_timeout_summary" android:defaultValue="0" />
|
||||
<EditTextPreference android:numeric="decimal" android:title="@string/revanced_swipe_overlay_text_size_title" android:key="revanced_swipe_overlay_text_size" android:summary="@string/revanced_swipe_overlay_text_size_summary" android:defaultValue="0" />
|
||||
<EditTextPreference android:numeric="integer" android:title="@string/revanced_swipe_overlay_bg_alpha_title" android:key="revanced_swipe_overlay_background_alpha" android:summary="@string/revanced_swipe_overlay_bg_alpha_summary" android:defaultValue="0" />
|
||||
<EditTextPreference android:numeric="decimal" android:title="@string/revanced_swipe_magnitude_threshold_title" android:key="revanced_swipe_magnitude_threshold" android:summary="@string/revanced_swipe_magnitude_threshold_summary" android:defaultValue="0" />
|
||||
</PreferenceScreen>
|
||||
<PreferenceScreen android:title="@string/revanced_buffer_title" android:key="buffer_screen">
|
||||
<EditTextPreference android:numeric="integer" android:title="@string/revanced_maximum_buffer_title" android:key="revanced_pref_max_buffer_ms" android:summary="@string/revanced_maximum_buffer_summary" android:defaultValue="120000" />
|
||||
|
Loading…
Reference in New Issue
Block a user