From 7bdc32867aeefe011fdc432b52ac6165205eee9b Mon Sep 17 00:00:00 2001
From: MarcaD <152095496+MarcaDian@users.noreply.github.com>
Date: Sun, 3 Aug 2025 18:23:46 +0300
Subject: [PATCH] feat(YouTube): Add player button to change video quality
(#5435)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
---
.../app/revanced/extension/shared/Utils.java | 10 +
.../AdvancedVideoQualityMenuPatch.java | 24 +-
.../quality/RememberVideoQualityPatch.java | 242 +++++----
.../speed/CustomPlaybackSpeedPatch.java | 8 +-
.../extension/youtube/settings/Settings.java | 1 +
.../ReVancedPreferenceFragment.java | 6 +-
.../videoplayer/PlayerControlButton.java | 54 +-
.../videoplayer/VideoQualityDialogButton.java | 476 ++++++++++++++++++
.../innertube/model/media/VideoQuality.java | 10 +-
patches/api/patches.api | 4 +
.../hook/RecyclerViewTreeHookPatch.kt | 1 -
.../quality/AdvancedVideoQualityMenuPatch.kt | 3 +-
.../youtube/video/quality/Fingerprints.kt | 2 +-
.../quality/RememberVideoQualityPatch.kt | 34 +-
.../video/quality/VideoQualityPatch.kt | 2 +
.../button/VideoQualityDialogButtonPatch.kt | 64 +++
.../resources/addresources/values/strings.xml | 5 +
...evanced_video_quality_dialog_button_4k.xml | 9 +
...vanced_video_quality_dialog_button_fhd.xml | 9 +
...d_video_quality_dialog_button_fhd_plus.xml | 9 +
...evanced_video_quality_dialog_button_hd.xml | 9 +
...evanced_video_quality_dialog_button_ld.xml | 9 +
...vanced_video_quality_dialog_button_qhd.xml | 9 +
...evanced_video_quality_dialog_button_sd.xml | 9 +
...ed_video_quality_dialog_button_unknown.xml | 11 +
.../youtube_controls_bottom_ui_container.xml | 27 +
.../youtube_controls_bottom_ui_container.xml | 1 -
27 files changed, 896 insertions(+), 152 deletions(-)
create mode 100644 extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java
create mode 100644 patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatch.kt
create mode 100644 patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_4k.xml
create mode 100644 patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd.xml
create mode 100644 patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd_plus.xml
create mode 100644 patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_hd.xml
create mode 100644 patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_ld.xml
create mode 100644 patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_qhd.xml
create mode 100644 patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_sd.xml
create mode 100644 patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_unknown.xml
create mode 100644 patches/src/main/resources/qualitybutton/host/layout/youtube_controls_bottom_ui_container.xml
diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
index e84ac36a2..c015c0610 100644
--- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
+++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java
@@ -1460,6 +1460,16 @@ public class Utils {
return (int) (metrics.widthPixels * (percentage / 100.0f));
}
+ /**
+ * Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
+ */
+ @ColorInt
+ public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) {
+ return isDarkModeEnabled()
+ ? adjustColorBrightness(baseColor, darkThemeFactor)
+ : adjustColorBrightness(baseColor, lightThemeFactor);
+ }
+
/**
* Adjusts the brightness of a color by lightening or darkening it based on the given factor.
*
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/AdvancedVideoQualityMenuPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/AdvancedVideoQualityMenuPatch.java
index 6722c4cb7..89f221b3b 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/AdvancedVideoQualityMenuPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/AdvancedVideoQualityMenuPatch.java
@@ -18,7 +18,7 @@ import app.revanced.extension.youtube.settings.Settings;
public final class AdvancedVideoQualityMenuPatch {
/**
- * Injection point.
+ * Injection point. Regular videos.
*/
public static void onFlyoutMenuCreate(RecyclerView recyclerView) {
if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return;
@@ -61,22 +61,12 @@ public final class AdvancedVideoQualityMenuPatch {
});
}
-
- /**
- * Injection point.
- *
- * Used to force the creation of the advanced menu item for the Shorts quality flyout.
- */
- public static boolean forceAdvancedVideoQualityMenuCreation(boolean original) {
- return Settings.ADVANCED_VIDEO_QUALITY_MENU.get() || original;
- }
-
/**
* Injection point.
*
* Shorts video quality flyout.
*/
- public static void showAdvancedVideoQualityMenu(ListView listView) {
+ public static void addVideoQualityListMenuListener(ListView listView) {
if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return;
listView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
@@ -91,7 +81,6 @@ public final class AdvancedVideoQualityMenuPatch {
listView.setSoundEffectsEnabled(false);
final var qualityItemMenuPosition = 4;
listView.performItemClick(null, qualityItemMenuPosition, 0);
-
} catch (Exception ex) {
Logger.printException(() -> "showAdvancedVideoQualityMenu failure", ex);
}
@@ -102,4 +91,13 @@ public final class AdvancedVideoQualityMenuPatch {
}
});
}
+
+ /**
+ * Injection point.
+ *
+ * Used to force the creation of the advanced menu item for the Shorts quality flyout.
+ */
+ public static boolean forceAdvancedVideoQualityMenuCreation(boolean original) {
+ return Settings.ADVANCED_VIDEO_QUALITY_MENU.get() || original;
+ }
}
\ No newline at end of file
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java
index 59d76324e..dcbad858b 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java
@@ -17,6 +17,7 @@ import app.revanced.extension.shared.settings.IntegerSetting;
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
+import app.revanced.extension.youtube.videoplayer.VideoQualityDialogButton;
@SuppressWarnings("unused")
public class RememberVideoQualityPatch {
@@ -25,10 +26,20 @@ public class RememberVideoQualityPatch {
* Interface to use obfuscated methods.
*/
public interface VideoQualityMenuInterface {
- void patch_setMenuIndexFromQuality(VideoQuality quality);
+ void patch_setQuality(VideoQuality quality);
}
- private static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
+ /**
+ * Video resolution of the automatic quality option..
+ */
+ public static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
+
+ /**
+ * All quality names are the same for all languages.
+ * VideoQuality also has a resolution enum that can be used if needed.
+ */
+ public static final String VIDEO_QUALITY_1080P_PREMIUM_NAME = "1080p Premium";
+
private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI;
private static final IntegerSetting videoQualityMobile = Settings.VIDEO_QUALITY_DEFAULT_MOBILE;
private static final IntegerSetting shortsQualityWifi = Settings.SHORTS_QUALITY_DEFAULT_WIFI;
@@ -36,32 +47,56 @@ public class RememberVideoQualityPatch {
private static boolean qualityNeedsUpdating;
- /**
- * If the user selected a new quality from the flyout menu,
- * and {@link Settings#REMEMBER_VIDEO_QUALITY_LAST_SELECTED}
- * or {@link Settings#REMEMBER_SHORTS_QUALITY_LAST_SELECTED} is enabled.
- */
- private static boolean userChangedDefaultQuality;
-
- /**
- * Index of the video quality chosen by the user from the flyout menu.
- */
- private static int userSelectedQualityIndex;
-
/**
* The available qualities of the current video.
*/
@Nullable
- private static List videoQualities;
+ private static List currentQualities;
- private static boolean shouldRememberVideoQuality() {
- BooleanSetting preference = ShortsPlayerState.isOpen() ?
- Settings.REMEMBER_SHORTS_QUALITY_LAST_SELECTED
+ /**
+ * The current quality of the video playing.
+ * This is always the actual quality even if Automatic quality is active.
+ */
+ @Nullable
+ private static VideoQuality currentQuality;
+
+ /**
+ * The current VideoQualityMenuInterface, set during setVideoQuality.
+ */
+ @Nullable
+ private static VideoQualityMenuInterface currentMenuInterface;
+
+ @Nullable
+ public static List getCurrentQualities() {
+ return currentQualities;
+ }
+
+ @Nullable
+ public static VideoQuality getCurrentQuality() {
+ return currentQuality;
+ }
+
+ @Nullable
+ public static VideoQualityMenuInterface getCurrentMenuInterface() {
+ return currentMenuInterface;
+ }
+
+ public static boolean shouldRememberVideoQuality() {
+ BooleanSetting preference = ShortsPlayerState.isOpen()
+ ? Settings.REMEMBER_SHORTS_QUALITY_LAST_SELECTED
: Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED;
return preference.get();
}
- private static void changeDefaultQuality(int qualityResolution) {
+ public static int getDefaultQualityResolution() {
+ final boolean isShorts = ShortsPlayerState.isOpen();
+ IntegerSetting preference = Utils.getNetworkType() == NetworkType.MOBILE
+ ? (isShorts ? shortsQualityMobile : videoQualityMobile)
+ : (isShorts ? shortsQualityWifi : videoQualityWifi);
+ return preference.get();
+ }
+
+ public static void saveDefaultQuality(int qualityResolution) {
final boolean shortPlayerOpen = ShortsPlayerState.isOpen();
String networkTypeMessage;
IntegerSetting qualitySetting;
@@ -72,16 +107,24 @@ public class RememberVideoQualityPatch {
networkTypeMessage = str("revanced_remember_video_quality_wifi");
qualitySetting = shortPlayerOpen ? shortsQualityWifi : videoQualityWifi;
}
+
+ if (qualitySetting.get() == qualityResolution) {
+ // User clicked the same video quality as the current video,
+ // or changed between 1080p Premium and non-Premium.
+ return;
+ }
qualitySetting.save(qualityResolution);
- if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
+ if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) {
+ String qualityLabel = qualityResolution + "p";
Utils.showToastShort(str(
shortPlayerOpen
? "revanced_remember_video_quality_toast_shorts"
: "revanced_remember_video_quality_toast",
networkTypeMessage,
- (qualityResolution + "p"))
+ qualityLabel)
);
+ }
}
/**
@@ -93,111 +136,101 @@ public class RememberVideoQualityPatch {
public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) {
try {
Utils.verifyOnMainThread();
+ currentMenuInterface = menu;
- final boolean useShortsPreference = ShortsPlayerState.isOpen();
- final int preferredQuality = Utils.getNetworkType() == NetworkType.MOBILE
- ? (useShortsPreference ? shortsQualityMobile : videoQualityMobile).get()
- : (useShortsPreference ? shortsQualityWifi : videoQualityWifi).get();
+ final boolean availableQualitiesChanged = currentQualities == null
+ || currentQualities.size() != qualities.length;
+ if (availableQualitiesChanged) {
+ currentQualities = Arrays.asList(qualities);
+ Logger.printDebug(() -> "VideoQualities: " + currentQualities);
+ }
- if (!userChangedDefaultQuality && preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
+ VideoQuality updatedCurrentQuality = qualities[originalQualityIndex];
+ if (updatedCurrentQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE &&
+ (currentQuality == null
+ || !currentQuality.patch_getQualityName().equals(updatedCurrentQuality.patch_getQualityName()))) {
+ currentQuality = updatedCurrentQuality;
+ Logger.printDebug(() -> "Current quality changed to: " + updatedCurrentQuality);
+
+ VideoQualityDialogButton.updateButtonIcon(updatedCurrentQuality);
+ }
+
+ final int preferredQuality = getDefaultQualityResolution();
+ if (preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
return originalQualityIndex; // Nothing to do.
}
- if (videoQualities == null || videoQualities.size() != qualities.length) {
- videoQualities = Arrays.asList(qualities);
-
- // After changing videos the qualities can initially be for the prior video.
- // So if the qualities have changed an update is needed.
- qualityNeedsUpdating = true;
- Logger.printDebug(() -> "VideoQualities: " + videoQualities);
- }
-
- if (userChangedDefaultQuality) {
- userChangedDefaultQuality = false;
- VideoQuality quality = videoQualities.get(userSelectedQualityIndex);
- Logger.printDebug(() -> "User changed default quality to: " + quality);
- changeDefaultQuality(quality.patch_getResolution());
- return userSelectedQualityIndex;
- }
-
- if (!qualityNeedsUpdating) {
+ // After changing videos the qualities can initially be for the prior video.
+ // If the qualities have changed and the default is not auto then an update is needed.
+ if (!qualityNeedsUpdating && !availableQualitiesChanged) {
return originalQualityIndex;
}
qualityNeedsUpdating = false;
// Find the highest quality that is equal to or less than the preferred.
- VideoQuality qualityToUse = videoQualities.get(0); // First element is automatic mode.
- int qualityIndexToUse = 0;
int i = 0;
- for (VideoQuality quality : videoQualities) {
+ for (VideoQuality quality : qualities) {
final int qualityResolution = quality.patch_getResolution();
- if (qualityResolution > qualityToUse.patch_getResolution() && qualityResolution <= preferredQuality) {
- qualityToUse = quality;
- qualityIndexToUse = i;
- break;
+ if (qualityResolution != AUTOMATIC_VIDEO_QUALITY_VALUE && qualityResolution <= preferredQuality) {
+ final boolean qualityNeedsChange = (i != originalQualityIndex);
+ Logger.printDebug(() -> qualityNeedsChange
+ ? "Changing video quality from: " + updatedCurrentQuality + " to: " + quality
+ : "Video is already the preferred quality: " + quality
+ );
+
+ // On first load of a new regular video, if the video is already the
+ // desired quality then the quality flyout will show 'Auto' (ie: Auto (720p)).
+ //
+ // To prevent user confusion, set the video index even if the
+ // quality is already correct so the UI picker will not display "Auto".
+ //
+ // Only change Shorts quality if the quality actually needs to change,
+ // because the "auto" option is not shown in the flyout
+ // and setting the same quality again can cause the Short to restart.
+ if (qualityNeedsChange || !ShortsPlayerState.isOpen()) {
+ menu.patch_setQuality(qualities[i]);
+ return i;
+ }
+
+ return originalQualityIndex;
}
i++;
}
-
- // If the desired quality index is equal to the original index,
- // then the video is already set to the desired default quality.
- String qualityToUseName = qualityToUse.patch_getQualityName();
- if (qualityIndexToUse == originalQualityIndex) {
- Logger.printDebug(() -> "Video is already preferred quality: " + qualityToUseName);
- } else {
- Logger.printDebug(() -> "Changing video quality from: "
- + videoQualities.get(originalQualityIndex).patch_getQualityName()
- + " to: " + qualityToUseName);
- }
-
- // On first load of a new video, if the video is already the desired quality
- // then the quality flyout will show 'Auto' (ie: Auto (720p)).
- //
- // To prevent user confusion, set the video index even if the
- // quality is already correct so the UI picker will not display "Auto".
- menu.patch_setMenuIndexFromQuality(qualities[qualityIndexToUse]);
-
- return qualityIndexToUse;
} catch (Exception ex) {
Logger.printException(() -> "setVideoQuality failure", ex);
- return originalQualityIndex;
}
- }
-
- /**
- * Injection point. Fixes bad data used by YouTube.
- */
- public static int fixVideoQualityResolution(String name, int quality) {
- final int correctQuality = 480;
- if (name.equals("480p") && quality != correctQuality) {
- Logger.printDebug(() -> "Fixing bad data of " + name + " from: " + quality
- + " to: " + correctQuality);
- return correctQuality;
- }
-
- return quality;
+ return originalQualityIndex;
}
/**
* Injection point.
- * @param qualityIndex Element index of {@link #videoQualities}.
+ * @param userSelectedQualityIndex Element index of {@link #currentQualities}.
*/
- public static void userChangedQuality(int qualityIndex) {
- if (shouldRememberVideoQuality()) {
- userSelectedQualityIndex = qualityIndex;
- userChangedDefaultQuality = true;
+ public static void userChangedShortsQuality(int userSelectedQualityIndex) {
+ try {
+ if (shouldRememberVideoQuality()) {
+ if (currentQualities == null) {
+ Logger.printDebug(() -> "Cannot save default quality, qualities is null");
+ return;
+ }
+ VideoQuality quality = currentQualities.get(userSelectedQualityIndex);
+ saveDefaultQuality(quality.patch_getResolution());
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "userChangedShortsQuality failure", ex);
}
}
/**
- * Injection point.
+ * Injection point. Regular videos.
* @param videoResolution Human readable resolution: 480, 720, 1080.
*/
- public static void userChangedQualityInFlyout(int videoResolution) {
+ public static void userChangedQuality(int videoResolution) {
Utils.verifyOnMainThread();
- if (!shouldRememberVideoQuality()) return;
- changeDefaultQuality(videoResolution);
+ if (shouldRememberVideoQuality()) {
+ saveDefaultQuality(videoResolution);
+ }
}
/**
@@ -207,7 +240,24 @@ public class RememberVideoQualityPatch {
Utils.verifyOnMainThread();
Logger.printDebug(() -> "newVideoStarted");
+ currentQualities = null;
+ currentQuality = null;
+ currentMenuInterface = null;
qualityNeedsUpdating = true;
- videoQualities = null;
+
+ // Hide the quality button until playback starts and the qualities are available.
+ VideoQualityDialogButton.updateButtonIcon(null);
+ }
+
+ /**
+ * Injection point. Fixes bad data used by YouTube.
+ */
+ public static int fixVideoQualityResolution(String name, int quality) {
+ final int correctQuality = 480;
+ if (name.equals("480p") && quality != correctQuality) {
+ return correctQuality;
+ }
+
+ return quality;
}
}
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 88eae4a0d..e738be34d 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
@@ -671,11 +671,9 @@ public class CustomPlaybackSpeedPatch {
*/
public static int getAdjustedBackgroundColor(boolean isHandleBar) {
final int baseColor = Utils.getDialogBackgroundColor();
- float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
- float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
- return Utils.isDarkModeEnabled()
- ? Utils.adjustColorBrightness(baseColor, darkThemeFactor) // Lighten for dark theme.
- : Utils.adjustColorBrightness(baseColor, lightThemeFactor); // Darken for light theme.
+ final float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
+ final float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
+ return Utils.adjustColorBrightness(baseColor, lightThemeFactor, darkThemeFactor);
}
}
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 2f51b1347..d279287f0 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
@@ -172,6 +172,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_VIDEO_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE);
public static final BooleanSetting OPEN_VIDEOS_FULLSCREEN_PORTRAIT = new BooleanSetting("revanced_open_videos_fullscreen_portrait", FALSE);
public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE);
+ public static final BooleanSetting VIDEO_QUALITY_DIALOG_BUTTON = new BooleanSetting("revanced_video_quality_dialog_button", FALSE);
public static final IntegerSetting PLAYER_OVERLAY_OPACITY = new IntegerSetting("revanced_player_overlay_opacity", 100, true);
public static final BooleanSetting PLAYER_POPUP_PANELS = new BooleanSetting("revanced_hide_player_popup_panels", FALSE);
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
index 02f8db6cb..c4cb42313 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
@@ -333,10 +333,8 @@ class AbstractPreferenceSearchData {
return text;
}
- final int baseColor = Utils.getAppBackgroundColor();
- final int adjustedColor = Utils.isDarkModeEnabled()
- ? Utils.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme.
- : Utils.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
+ final int adjustedColor = Utils.adjustColorBrightness(Utils.getAppBackgroundColor(),
+ 0.95f, 1.20f);
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
SpannableStringBuilder spannable = new SpannableStringBuilder(text);
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java
index 6d3c32017..94c92b738 100644
--- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java
@@ -187,4 +187,56 @@ public class PlayerControlButton {
if (view != null) view.setVisibility(View.GONE);
isVisible = false;
}
-}
\ No newline at end of file
+
+ /**
+ * Sets the icon of the button.
+ * @param resourceId Drawable identifier, or zero to hide the icon.
+ */
+ public void setIcon(int resourceId) {
+ try {
+ View button = buttonRef.get();
+ if (button instanceof ImageView imageButton) {
+ imageButton.setImageResource(resourceId);
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "setIcon failure", ex);
+ }
+ }
+
+ /**
+ * Starts an animation on the button.
+ * @param animation The animation to apply.
+ */
+ public void startAnimation(Animation animation) {
+ try {
+ View button = buttonRef.get();
+ if (button != null) {
+ button.startAnimation(animation);
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "startAnimation failure", ex);
+ }
+ }
+
+ /**
+ * Clears any animation on the button.
+ */
+ public void clearAnimation() {
+ try {
+ View button = buttonRef.get();
+ if (button != null) {
+ button.clearAnimation();
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "clearAnimation failure", ex);
+ }
+ }
+
+ /**
+ * Returns the View associated with this button.
+ * @return The button View.
+ */
+ public View getView() {
+ return buttonRef.get();
+ }
+}
diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java
new file mode 100644
index 000000000..924f1ce7b
--- /dev/null
+++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java
@@ -0,0 +1,476 @@
+package app.revanced.extension.youtube.videoplayer;
+
+import static app.revanced.extension.shared.StringRef.str;
+import static app.revanced.extension.shared.Utils.dipToPixels;
+import static app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch.AUTOMATIC_VIDEO_QUALITY_VALUE;
+import static app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch.VIDEO_QUALITY_1080P_PREMIUM_NAME;
+import static app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch.VideoQualityMenuInterface;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.style.ForegroundColorSpan;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.animation.Animation;
+import android.view.animation.TranslateAnimation;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch;
+import app.revanced.extension.youtube.settings.Settings;
+
+@SuppressWarnings("unused")
+public class VideoQualityDialogButton {
+
+ private static final int DRAWABLE_LD = getDrawableIdentifier("revanced_video_quality_dialog_button_ld");
+ private static final int DRAWABLE_SD = getDrawableIdentifier("revanced_video_quality_dialog_button_sd");
+ private static final int DRAWABLE_HD = getDrawableIdentifier("revanced_video_quality_dialog_button_hd");
+ private static final int DRAWABLE_FHD = getDrawableIdentifier("revanced_video_quality_dialog_button_fhd");
+ private static final int DRAWABLE_FHD_PLUS = getDrawableIdentifier("revanced_video_quality_dialog_button_fhd_plus");
+ private static final int DRAWABLE_QHD = getDrawableIdentifier("revanced_video_quality_dialog_button_qhd");
+ private static final int DRAWABLE_4K = getDrawableIdentifier("revanced_video_quality_dialog_button_4k");
+ private static final int DRAWABLE_UNKNOWN = getDrawableIdentifier("revanced_video_quality_dialog_button_unknown");
+
+ @Nullable
+ private static PlayerControlButton instance;
+
+ /**
+ * The current resource name of the button icon.
+ */
+ private static int currentIconResource;
+
+ private static int getDrawableIdentifier(String resourceName) {
+ final int resourceId = Utils.getResourceIdentifier(resourceName, "drawable");
+ if (resourceId == 0) Logger.printException(() -> "Could not find resource: " + resourceName);
+ return resourceId;
+ }
+
+ /**
+ * Updates the button icon based on the current video quality.
+ */
+ public static void updateButtonIcon(@Nullable VideoQuality quality) {
+ try {
+ Utils.verifyOnMainThread();
+ if (instance == null) return;
+
+ final int resolution = quality == null
+ ? AUTOMATIC_VIDEO_QUALITY_VALUE // Video is still loading.
+ : quality.patch_getResolution();
+
+ final int iconResource = switch (resolution) {
+ case 144, 240, 360 -> DRAWABLE_LD;
+ case 480 -> DRAWABLE_SD;
+ case 720 -> DRAWABLE_HD;
+ case 1080 -> VIDEO_QUALITY_1080P_PREMIUM_NAME.equals(quality.patch_getQualityName())
+ ? DRAWABLE_FHD_PLUS
+ : DRAWABLE_FHD;
+ case 1440 -> DRAWABLE_QHD;
+ case 2160 -> DRAWABLE_4K;
+ default -> DRAWABLE_UNKNOWN;
+ };
+
+ if (iconResource != currentIconResource) {
+ currentIconResource = iconResource;
+
+ Utils.runOnMainThreadDelayed(() -> {
+ if (iconResource != currentIconResource) {
+ Logger.printDebug(() -> "Ignoring stale button update to: " + quality);
+ return;
+ }
+ instance.setIcon(iconResource);
+ }, 100);
+ }
+ } catch (Exception ex) {
+ Logger.printException(() -> "updateButtonIcon failure", ex);
+ }
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void initializeButton(View controlsView) {
+ try {
+ instance = new PlayerControlButton(
+ controlsView,
+ "revanced_video_quality_dialog_button",
+ "revanced_video_quality_dialog_button_placeholder",
+ Settings.VIDEO_QUALITY_DIALOG_BUTTON::get,
+ view -> {
+ try {
+ showVideoQualityDialog(view.getContext());
+ } catch (Exception ex) {
+ Logger.printException(() -> "Video quality button onClick failure", ex);
+ }
+ },
+ view -> {
+ try {
+ List qualities = RememberVideoQualityPatch.getCurrentQualities();
+ VideoQualityMenuInterface menu = RememberVideoQualityPatch.getCurrentMenuInterface();
+ if (qualities == null || menu == null) {
+ Logger.printDebug(() -> "Cannot reset quality, videoQualities is null");
+ return true;
+ }
+
+ // Reset to default quality.
+ final int defaultResolution = RememberVideoQualityPatch.getDefaultQualityResolution();
+ for (VideoQuality quality : qualities) {
+ final int resolution = quality.patch_getResolution();
+ if (resolution != AUTOMATIC_VIDEO_QUALITY_VALUE && resolution <= defaultResolution) {
+ Logger.printDebug(() -> "Resetting quality to: " + quality);
+ menu.patch_setQuality(quality);
+ return true;
+ }
+ }
+
+ // Existing hook cannot set default quality to auto.
+ // Instead show the quality dialog.
+ showVideoQualityDialog(view.getContext());
+ return true;
+ } catch (Exception ex) {
+ Logger.printException(() -> "Video quality button reset failure", ex);
+ }
+ return false;
+ }
+ );
+
+ // Set initial icon.
+ updateButtonIcon(RememberVideoQualityPatch.getCurrentQuality());
+ } catch (Exception ex) {
+ Logger.printException(() -> "initializeButton failure", ex);
+ }
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void setVisibilityImmediate(boolean visible) {
+ if (instance != null) {
+ instance.setVisibilityImmediate(visible);
+ }
+ }
+
+ /**
+ * Injection point.
+ */
+ public static void setVisibility(boolean visible, boolean animated) {
+ if (instance != null) {
+ instance.setVisibility(visible, animated);
+ }
+ }
+
+ /**
+ * Shows a dialog with available video qualities, excluding Auto, with a title showing the current quality.
+ */
+ private static void showVideoQualityDialog(Context context) {
+ try {
+ List currentQualities = RememberVideoQualityPatch.getCurrentQualities();
+ VideoQuality currentQuality = RememberVideoQualityPatch.getCurrentQuality();
+ if (currentQualities == null || currentQuality == null) {
+ Logger.printDebug(() -> "Cannot show qualities dialog, videoQualities is null");
+ return;
+ }
+ if (currentQualities.size() < 2) {
+ // Should never happen.
+ Logger.printException(() -> "Cannot show qualities dialog, no qualities available");
+ return;
+ }
+
+ VideoQualityMenuInterface menu = RememberVideoQualityPatch.getCurrentMenuInterface();
+ if (menu == null) {
+ Logger.printDebug(() -> "Cannot show qualities dialog, menu is null");
+ return;
+ }
+
+ // -1 adjustment for automatic quality at first index.
+ final int listViewSelectedIndex = currentQualities.indexOf(currentQuality) - 1;
+
+ List qualityLabels = new ArrayList<>(currentQualities.size() - 1);
+ for (VideoQuality availableQuality : currentQualities) {
+ if (availableQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE) {
+ qualityLabels.add(availableQuality.patch_getQualityName());
+ }
+ }
+
+ Dialog dialog = new Dialog(context);
+ dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
+ dialog.setCanceledOnTouchOutside(true);
+ dialog.setCancelable(true);
+
+ final int dip4 = dipToPixels(4); // Height for handle bar.
+ final int dip5 = dipToPixels(5); // Padding for mainLayout.
+ final int dip6 = dipToPixels(6); // Bottom margin.
+ final int dip8 = dipToPixels(8); // Side padding.
+ final int dip16 = dipToPixels(16); // Left padding for ListView.
+ final int dip20 = dipToPixels(20); // Margin below handle.
+ final int dip40 = dipToPixels(40); // Width for handle bar.
+
+ LinearLayout mainLayout = new LinearLayout(context);
+ mainLayout.setOrientation(LinearLayout.VERTICAL);
+ mainLayout.setPadding(dip5, dip8, dip5, dip8);
+
+ ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
+ Utils.createCornerRadii(12), null, null));
+ background.getPaint().setColor(Utils.getDialogBackgroundColor());
+ mainLayout.setBackground(background);
+
+ View handleBar = new View(context);
+ ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
+ Utils.createCornerRadii(4), null, null));
+ final int baseColor = Utils.getDialogBackgroundColor();
+ final int adjustedHandleBarBackgroundColor = Utils.adjustColorBrightness(
+ baseColor, 0.9f, 1.25f);
+ handleBackground.getPaint().setColor(adjustedHandleBarBackgroundColor);
+ handleBar.setBackground(handleBackground);
+ LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(dip40, dip4);
+ handleParams.gravity = Gravity.CENTER_HORIZONTAL;
+ handleParams.setMargins(0, 0, 0, dip20);
+ handleBar.setLayoutParams(handleParams);
+ mainLayout.addView(handleBar);
+
+ // Create SpannableStringBuilder for formatted text.
+ SpannableStringBuilder spannableTitle = new SpannableStringBuilder();
+ String titlePart = str("video_quality_quick_menu_title");
+ String separatorPart = str("video_quality_title_seperator");
+
+ // Append title part with default foreground color.
+ spannableTitle.append(titlePart);
+ spannableTitle.setSpan(
+ new ForegroundColorSpan(Utils.getAppForegroundColor()),
+ 0,
+ titlePart.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+ spannableTitle.append(" "); // Space after title.
+
+ // Append separator part with adjusted title color.
+ int separatorStart = spannableTitle.length();
+ spannableTitle.append(separatorPart);
+ final int adjustedTitleForegroundColor = Utils.adjustColorBrightness(
+ Utils.getAppForegroundColor(), 1.6f, 0.6f);
+ spannableTitle.setSpan(
+ new ForegroundColorSpan(adjustedTitleForegroundColor),
+ separatorStart,
+ separatorStart + separatorPart.length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+ spannableTitle.append(" "); // Space after separator.
+
+ // Append quality label with adjusted title color.
+ final int qualityStart = spannableTitle.length();
+ spannableTitle.append(currentQuality.patch_getQualityName());
+ spannableTitle.setSpan(
+ new ForegroundColorSpan(adjustedTitleForegroundColor),
+ qualityStart,
+ qualityStart + currentQuality.patch_getQualityName().length(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
+ );
+
+ // Add title with current quality.
+ TextView titleView = new TextView(context);
+ titleView.setText(spannableTitle);
+ titleView.setTextSize(16);
+ // Remove setTextColor since color is handled by SpannableStringBuilder.
+ LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.WRAP_CONTENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ titleParams.setMargins(dip8, 0, 0, dip20);
+ titleView.setLayoutParams(titleParams);
+ mainLayout.addView(titleView);
+
+ ListView listView = new ListView(context);
+ CustomQualityAdapter adapter = new CustomQualityAdapter(context, qualityLabels);
+ adapter.setSelectedPosition(listViewSelectedIndex);
+ listView.setAdapter(adapter);
+ listView.setDivider(null);
+ listView.setPadding(dip16, 0, 0, 0);
+
+ listView.setOnItemClickListener((parent, view, which, id) -> {
+ try {
+ final int originalIndex = which + 1; // Adjust for automatic.
+ VideoQuality selectedQuality = currentQualities.get(originalIndex);
+ Logger.printDebug(() -> "User clicked on quality: " + selectedQuality);
+
+ if (RememberVideoQualityPatch.shouldRememberVideoQuality()) {
+ RememberVideoQualityPatch.saveDefaultQuality(selectedQuality.patch_getResolution());
+ }
+ // Don't update button icon now. Icon will update when the actual
+ // quality is changed by YT. This is needed to ensure the icon is correct
+ // if YT ignores changing from 1080p Premium to regular 1080p.
+ menu.patch_setQuality(selectedQuality);
+
+ dialog.dismiss();
+ } catch (Exception ex) {
+ Logger.printException(() -> "Video quality selection failure", ex);
+ }
+ });
+
+ LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.WRAP_CONTENT);
+ listViewParams.setMargins(0, 0, 0, dip5);
+ listView.setLayoutParams(listViewParams);
+ mainLayout.addView(listView);
+
+ LinearLayout wrapperLayout = new LinearLayout(context);
+ wrapperLayout.setOrientation(LinearLayout.VERTICAL);
+ wrapperLayout.setPadding(dip8, 0, dip8, 0);
+ wrapperLayout.addView(mainLayout);
+ dialog.setContentView(wrapperLayout);
+
+ Window window = dialog.getWindow();
+ if (window != null) {
+ WindowManager.LayoutParams params = window.getAttributes();
+ params.gravity = Gravity.BOTTOM;
+ params.y = dip6;
+ int portraitWidth = context.getResources().getDisplayMetrics().widthPixels;
+ if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
+ portraitWidth = Math.min(
+ portraitWidth,
+ context.getResources().getDisplayMetrics().heightPixels);
+ }
+ params.width = portraitWidth;
+ params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ window.setAttributes(params);
+ window.setBackgroundDrawable(null);
+ }
+
+ final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
+ Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
+ slideInABottomAnimation.setDuration(fadeDurationFast);
+ mainLayout.startAnimation(slideInABottomAnimation);
+
+ // noinspection ClickableViewAccessibility
+ mainLayout.setOnTouchListener(new View.OnTouchListener() {
+ final float dismissThreshold = dipToPixels(100);
+ float touchY;
+ float translationY;
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ touchY = event.getRawY();
+ translationY = mainLayout.getTranslationY();
+ return true;
+ case MotionEvent.ACTION_MOVE:
+ final float deltaY = event.getRawY() - touchY;
+ if (deltaY >= 0) {
+ mainLayout.setTranslationY(translationY + deltaY);
+ }
+ return true;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ if (mainLayout.getTranslationY() > dismissThreshold) {
+ //noinspection ExtractMethodRecommender
+ final float remainingDistance = context.getResources().getDisplayMetrics().heightPixels
+ - mainLayout.getTop();
+ TranslateAnimation slideOut = new TranslateAnimation(
+ 0, 0, mainLayout.getTranslationY(), remainingDistance);
+ slideOut.setDuration(fadeDurationFast);
+ slideOut.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ dialog.dismiss();
+ }
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ mainLayout.startAnimation(slideOut);
+ } else {
+ TranslateAnimation slideBack = new TranslateAnimation(
+ 0, 0, mainLayout.getTranslationY(), 0);
+ slideBack.setDuration(fadeDurationFast);
+ mainLayout.startAnimation(slideBack);
+ mainLayout.setTranslationY(0);
+ }
+ return true;
+ default:
+ return false;
+ }
+ }
+ });
+
+ dialog.show();
+ } catch (Exception ex) {
+ Logger.printException(() -> "showVideoQualityDialog failure", ex);
+ }
+ }
+
+ private static class CustomQualityAdapter extends ArrayAdapter {
+ private int selectedPosition = -1;
+
+ public CustomQualityAdapter(@NonNull Context context, @NonNull List objects) {
+ super(context, 0, objects);
+ }
+
+ private void setSelectedPosition(int position) {
+ this.selectedPosition = position;
+ notifyDataSetChanged();
+ }
+
+ @NonNull
+ @Override
+ public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
+ ViewHolder viewHolder;
+
+ if (convertView == null) {
+ convertView = LayoutInflater.from(getContext()).inflate(
+ Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"),
+ parent,
+ false
+ );
+ viewHolder = new ViewHolder();
+ viewHolder.checkIcon = convertView.findViewById(
+ Utils.getResourceIdentifier("revanced_check_icon", "id")
+ );
+ viewHolder.placeholder = convertView.findViewById(
+ Utils.getResourceIdentifier("revanced_check_icon_placeholder", "id")
+ );
+ viewHolder.textView = convertView.findViewById(
+ Utils.getResourceIdentifier("revanced_item_text", "id")
+ );
+ convertView.setTag(viewHolder);
+ } else {
+ viewHolder = (ViewHolder) convertView.getTag();
+ }
+
+ viewHolder.textView.setText(getItem(position));
+ final boolean isSelected = position == selectedPosition;
+ viewHolder.checkIcon.setVisibility(isSelected ? View.VISIBLE : View.GONE);
+ viewHolder.placeholder.setVisibility(isSelected ? View.GONE : View.INVISIBLE);
+
+ return convertView;
+ }
+
+ private static class ViewHolder {
+ ImageView checkIcon;
+ View placeholder;
+ TextView textView;
+ }
+ }
+}
diff --git a/extensions/youtube/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/VideoQuality.java b/extensions/youtube/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/VideoQuality.java
index 67dbc6873..628c423ab 100644
--- a/extensions/youtube/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/VideoQuality.java
+++ b/extensions/youtube/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/VideoQuality.java
@@ -1,12 +1,8 @@
package com.google.android.libraries.youtube.innertube.model.media;
-public class VideoQuality {
- public final String patch_getQualityName() {
- throw new UnsupportedOperationException("Stub");
- }
+public abstract class VideoQuality implements Comparable {
+ public abstract String patch_getQualityName();
- public final int patch_getResolution() {
- throw new UnsupportedOperationException("Stub");
- }
+ public abstract int patch_getResolution();
}
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 559de899f..91f78e8db 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -1664,6 +1664,10 @@ public final class app/revanced/patches/youtube/video/quality/VideoQualityPatchK
public static final fun getVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
+public final class app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatchKt {
+ public static final fun getVideoQualityButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
public final class app/revanced/patches/youtube/video/speed/PlaybackSpeedPatchKt {
public static final fun getPlaybackSpeedPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/recyclerviewtree/hook/RecyclerViewTreeHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/recyclerviewtree/hook/RecyclerViewTreeHookPatch.kt
index 86588df09..58033a223 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/recyclerviewtree/hook/RecyclerViewTreeHookPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/recyclerviewtree/hook/RecyclerViewTreeHookPatch.kt
@@ -11,7 +11,6 @@ val recyclerViewTreeHookPatch = bytecodePatch {
dependsOn(sharedExtensionPatch)
execute {
-
recyclerViewTreeObserverFingerprint.method.apply {
val insertIndex = recyclerViewTreeObserverFingerprint.patternMatch!!.startIndex + 1
val recyclerViewParameter = 2
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/AdvancedVideoQualityMenuPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/AdvancedVideoQualityMenuPatch.kt
index 55285a56e..3e8aa23a1 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/AdvancedVideoQualityMenuPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/AdvancedVideoQualityMenuPatch.kt
@@ -68,7 +68,6 @@ internal val advancedVideoQualityMenuPatch = bytecodePatch {
// region Patch for the old type of the video quality menu.
// Used for regular videos when spoofing to old app version,
// and for the Shorts quality flyout on newer app versions.
-
videoQualityMenuViewInflateFingerprint.let {
it.method.apply {
val checkCastIndex = it.patternMatch!!.endIndex
@@ -77,7 +76,7 @@ internal val advancedVideoQualityMenuPatch = bytecodePatch {
addInstruction(
checkCastIndex + 1,
"invoke-static { v$listViewRegister }, $EXTENSION_CLASS_DESCRIPTOR->" +
- "showAdvancedVideoQualityMenu(Landroid/widget/ListView;)V",
+ "addVideoQualityListMenuListener(Landroid/widget/ListView;)V",
)
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt
index c98bdac63..f04079632 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt
@@ -23,7 +23,7 @@ internal val videoQualityFingerprint = fingerprint {
/**
* Matches with the class found in [videoQualitySetterFingerprint].
*/
-internal val setQualityByIndexMethodClassFieldReferenceFingerprint = fingerprint {
+internal val setVideoQualityFingerprint = fingerprint {
returns("V")
parameters("L")
opcodes(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt
index 1e42048ba..7a31a6b74 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt
@@ -67,13 +67,6 @@ val rememberVideoQualityPatch = bytecodePatch {
SwitchPreference("revanced_remember_video_quality_last_selected_toast")
))
- /*
- * The following code works by hooking the method which is called when the user selects a video quality
- * to remember the last selected video quality.
- *
- * It also hooks the method which is called when the video quality to set is determined.
- * Conveniently, at this point the video quality is overridden to the remembered playback speed.
- */
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "newVideoStarted")
videoQualityFingerprint.let {
@@ -82,7 +75,7 @@ val rememberVideoQualityPatch = bytecodePatch {
0,
"""
invoke-static { p2, p1 }, $EXTENSION_CLASS_DESCRIPTOR->fixVideoQualityResolution(Ljava/lang/String;I)I
- move-result p1
+ move-result p1
"""
)
@@ -142,10 +135,10 @@ val rememberVideoQualityPatch = bytecodePatch {
}
// Inject a call to set the remembered quality once a video loads.
- setQualityByIndexMethodClassFieldReferenceFingerprint.match(
+ setVideoQualityFingerprint.match(
videoQualitySetterFingerprint.originalClassDef
).let { match ->
- // This instruction refers to the field with the type that contains the setQualityByIndex method.
+ // This instruction refers to the field with the type that contains the setQuality method.
val instructions = match.method.implementation!!.instructions
val onItemClickListenerClassReference =
(instructions.elementAt(0) as ReferenceInstruction).reference
@@ -160,15 +153,10 @@ val rememberVideoQualityPatch = bytecodePatch {
// Add interface and helper methods to allow extension code to call obfuscated methods.
interfaces.add(EXTENSION_VIDEO_QUALITY_MENU_INTERFACE)
- // Get the name of the setQualityByIndex method.
- val setQualityMenuIndexMethod = methods.single {
- method -> method.parameterTypes.firstOrNull() == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
- }
-
methods.add(
ImmutableMethod(
type,
- "patch_setMenuIndexFromQuality",
+ "patch_setQuality",
listOf(
ImmutableMethodParameter(YOUTUBE_VIDEO_QUALITY_CLASS_TYPE, null, null)
),
@@ -178,6 +166,10 @@ val rememberVideoQualityPatch = bytecodePatch {
null,
MutableMethodImplementation(2),
).toMutable().apply {
+ val setQualityMenuIndexMethod = methods.single { method ->
+ method.parameterTypes.firstOrNull() == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
+ }
+
addInstructions(
0,
"""
@@ -192,7 +184,7 @@ val rememberVideoQualityPatch = bytecodePatch {
videoQualitySetterFingerprint.method.addInstructions(
0,
"""
- # Get the object instance to invoke the setQualityByIndex method on.
+ # Get object instance to invoke setQuality method.
iget-object v0, p0, $onItemClickListenerClassReference
iget-object v0, v0, $setQualityFieldReference
@@ -202,15 +194,15 @@ val rememberVideoQualityPatch = bytecodePatch {
)
}
- // Inject a call to remember the selected quality.
+ // Inject a call to remember the selected quality for Shorts.
videoQualityItemOnClickFingerprint.match(
videoQualityItemOnClickParentFingerprint.classDef
).method.addInstruction(
0,
- "invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->userChangedQuality(I)V"
+ "invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->userChangedShortsQuality(I)V"
)
- // Inject a call to remember the user selected quality.
+ // Inject a call to remember the user selected quality for regular videos.
videoQualityChangedFingerprint.let {
it.method.apply {
val index = it.patternMatch!!.startIndex
@@ -218,7 +210,7 @@ val rememberVideoQualityPatch = bytecodePatch {
addInstruction(
index + 1,
- "invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->userChangedQualityInFlyout(I)V",
+ "invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->userChangedQuality(I)V",
)
}
}
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt
index 2fb92c848..432b4f8f3 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt
@@ -5,6 +5,7 @@ import app.revanced.patches.shared.misc.settings.preference.BasePreference
import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
+import app.revanced.patches.youtube.video.quality.button.videoQualityButtonPatch
/**
* Video quality settings. Used to organize all speed related settings together.
@@ -19,6 +20,7 @@ val videoQualityPatch = bytecodePatch(
dependsOn(
rememberVideoQualityPatch,
advancedVideoQualityMenuPatch,
+ videoQualityButtonPatch,
)
compatibleWith(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatch.kt
new file mode 100644
index 000000000..aaa42c827
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatch.kt
@@ -0,0 +1,64 @@
+package app.revanced.patches.youtube.video.quality.button
+
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.patch.resourcePatch
+import app.revanced.patches.all.misc.resources.addResources
+import app.revanced.patches.all.misc.resources.addResourcesPatch
+import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
+import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
+import app.revanced.patches.youtube.misc.playercontrols.*
+import app.revanced.patches.youtube.misc.settings.PreferenceScreen
+import app.revanced.patches.youtube.misc.settings.settingsPatch
+import app.revanced.patches.youtube.video.quality.rememberVideoQualityPatch
+import app.revanced.util.ResourceGroup
+import app.revanced.util.copyResources
+
+private val videoQualityButtonResourcePatch = resourcePatch {
+ dependsOn(playerControlsResourcePatch)
+
+ execute {
+ copyResources(
+ "qualitybutton",
+ ResourceGroup(
+ "drawable",
+ "revanced_video_quality_dialog_button_ld.xml",
+ "revanced_video_quality_dialog_button_sd.xml",
+ "revanced_video_quality_dialog_button_hd.xml",
+ "revanced_video_quality_dialog_button_fhd.xml",
+ "revanced_video_quality_dialog_button_fhd_plus.xml",
+ "revanced_video_quality_dialog_button_qhd.xml",
+ "revanced_video_quality_dialog_button_4k.xml",
+ "revanced_video_quality_dialog_button_unknown.xml",
+ ),
+ )
+
+ addBottomControl("qualitybutton")
+ }
+}
+
+private const val QUALITY_BUTTON_CLASS_DESCRIPTOR =
+ "Lapp/revanced/extension/youtube/videoplayer/VideoQualityDialogButton;"
+
+val videoQualityButtonPatch = bytecodePatch(
+ description = "Adds the option to display video quality dialog button in the video player.",
+) {
+ dependsOn(
+ sharedExtensionPatch,
+ settingsPatch,
+ addResourcesPatch,
+ rememberVideoQualityPatch,
+ videoQualityButtonResourcePatch,
+ playerControlsPatch,
+ )
+
+ execute {
+ addResources("youtube", "video.quality.button.videoQualityButtonPatch")
+
+ PreferenceScreen.PLAYER.addPreferences(
+ SwitchPreference("revanced_video_quality_dialog_button"),
+ )
+
+ initializeBottomControl(QUALITY_BUTTON_CLASS_DESCRIPTOR)
+ injectVisibilityCheckCall(QUALITY_BUTTON_CLASS_DESCRIPTOR)
+ }
+}
diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml
index 8a356d06d..3624d7f22 100644
--- a/patches/src/main/resources/addresources/values/strings.xml
+++ b/patches/src/main/resources/addresources/values/strings.xml
@@ -1543,6 +1543,11 @@ Enabling this can unlock higher video qualities"
Button is shown. Tap and hold to reset playback speed to default
Button is not shown
+
+ Show video quality button
+ Button is shown. Tap and hold to reset quality to default
+ Button is not shown
+
Custom playback speed menu
Custom speed menu is shown
diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_4k.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_4k.xml
new file mode 100644
index 000000000..22051e3fb
--- /dev/null
+++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_4k.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd.xml
new file mode 100644
index 000000000..b7a8f5c73
--- /dev/null
+++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd_plus.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd_plus.xml
new file mode 100644
index 000000000..18e163ca2
--- /dev/null
+++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd_plus.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_hd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_hd.xml
new file mode 100644
index 000000000..db2bad1b1
--- /dev/null
+++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_hd.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_ld.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_ld.xml
new file mode 100644
index 000000000..a49f5a200
--- /dev/null
+++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_ld.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_qhd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_qhd.xml
new file mode 100644
index 000000000..7609d368e
--- /dev/null
+++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_qhd.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_sd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_sd.xml
new file mode 100644
index 000000000..150ede850
--- /dev/null
+++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_sd.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_unknown.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_unknown.xml
new file mode 100644
index 000000000..4e5750cfe
--- /dev/null
+++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_unknown.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/patches/src/main/resources/qualitybutton/host/layout/youtube_controls_bottom_ui_container.xml b/patches/src/main/resources/qualitybutton/host/layout/youtube_controls_bottom_ui_container.xml
new file mode 100644
index 000000000..deb534fba
--- /dev/null
+++ b/patches/src/main/resources/qualitybutton/host/layout/youtube_controls_bottom_ui_container.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/patches/src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml b/patches/src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml
index 315251b0c..c0109ebc5 100644
--- a/patches/src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml
+++ b/patches/src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml
@@ -13,7 +13,6 @@
android:layout_height="60.0dip"
android:paddingTop="6.0dp"
android:paddingBottom="0dp"
- android:longClickable="false"
android:scaleType="center"
android:src="@drawable/revanced_playback_speed_dialog_button"
yt:layout_constraintBottom_toTopOf="@+id/quick_actions_container"