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"