feat(YouTube): Add player button to change video quality (#5435)

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
MarcaD
2025-08-03 18:23:46 +03:00
committed by GitHub
parent 6e60ac6963
commit 7bdc32867a
27 changed files with 896 additions and 152 deletions

View File

@@ -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.
* <p>

View File

@@ -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;
}
}

View File

@@ -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<VideoQuality> videoQualities;
private static List<VideoQuality> 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<VideoQuality> 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -333,10 +333,8 @@ class AbstractPreferenceSearchData<T extends Preference> {
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);

View File

@@ -187,4 +187,56 @@ public class PlayerControlButton {
if (view != null) view.setVisibility(View.GONE);
isVisible = false;
}
}
/**
* 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();
}
}

View File

@@ -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<VideoQuality> 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<VideoQuality> 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<String> 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<String> {
private int selectedPosition = -1;
public CustomQualityAdapter(@NonNull Context context, @NonNull List<String> 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;
}
}
}

View File

@@ -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<VideoQuality> {
public abstract String patch_getQualityName();
public final int patch_getResolution() {
throw new UnsupportedOperationException("Stub");
}
public abstract int patch_getResolution();
}

View File

@@ -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;
}

View File

@@ -11,7 +11,6 @@ val recyclerViewTreeHookPatch = bytecodePatch {
dependsOn(sharedExtensionPatch)
execute {
recyclerViewTreeObserverFingerprint.method.apply {
val insertIndex = recyclerViewTreeObserverFingerprint.patternMatch!!.startIndex + 1
val recyclerViewParameter = 2

View File

@@ -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",
)
}
}

View File

@@ -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(

View File

@@ -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",
)
}
}

View File

@@ -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(

View File

@@ -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)
}
}

View File

@@ -1543,6 +1543,11 @@ Enabling this can unlock higher video qualities"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Button is shown. Tap and hold to reset playback speed to default</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Button is not shown</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Show video quality button</string>
<string name="revanced_video_quality_dialog_button_summary_on">Button is shown. Tap and hold to reset quality to default</string>
<string name="revanced_video_quality_dialog_button_summary_off">Button is not shown</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Custom playback speed menu</string>
<string name="revanced_custom_speed_menu_summary_on">Custom speed menu is shown</string>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.61719,5 C3.15625,5,2.76953,5.15234,2.46094,5.46094 C2.15234,5.76953,2,6.15625,2,6.61719 L2,17.3828 C2,17.8438,2.15234,18.2305,2.46094,18.5391 C2.76953,18.8477,3.15625,19,3.61719,19 L20.3828,19 C20.8438,19,21.2305,18.8477,21.5391,18.5391 C21.8477,18.2305,22,17.8438,22,17.3828 L22,6.61719 C22,6.15625,21.8477,5.76953,21.5391,5.46094 C21.2305,5.15234,20.8438,5,20.3828,5 L3.61719,5 Z M3.61719,6 L20.3828,6 C20.5625,6,20.7109,6.05859,20.8281,6.17188 C20.9414,6.28906,21,6.4375,21,6.61719 L21,17.3828 C21,17.5625,20.9414,17.7109,20.8281,17.8281 C20.7109,17.9414,20.5625,18,20.3828,18 L3.61719,18 C3.4375,18,3.28906,17.9414,3.17188,17.8281 C3.05859,17.7109,3,17.5625,3,17.3828 L3,6.61719 C3,6.4375,3.05859,6.28906,3.17188,6.17188 C3.28906,6.05859,3.4375,6,3.61719,6 Z M6.61719,9.30859 L6.61719,13.1914 L9.38672,13.1914 L9.38672,14.6914 L10.2715,14.6914 L10.2715,13.1914 L11.502,13.1914 L11.502,12.3086 L10.2715,12.3086 L10.2715,9.30859 L9.38672,9.30859 L9.38672,12.3086 L7.50195,12.3086 L7.50195,9.30859 L6.61719,9.30859 Z M13.0176,9.30859 L13.0176,14.6914 L13.9023,14.6914 L13.9023,12.3262 L16.1914,14.6914 L17.4199,14.6914 L14.7676,11.9609 L17.4199,9.30859 L16.2285,9.30859 L13.9023,11.6348 L13.9023,9.30859 L13.0176,9.30859 Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M9.80859,14.6914 L10.6914,14.6914 L10.6914,12.6914 L12.8086,12.6914 L12.8086,14.6914 L13.6914,14.6914 L13.6914,9.30859 L12.8086,9.30859 L12.8086,11.8086 L10.6914,11.8086 L10.6914,9.30859 L9.80859,9.30859 Z M15.0781,14.6914 L17.8828,14.6914 C18.1797,14.6914,18.4336,14.5859,18.6445,14.375 C18.8555,14.1641,18.9609,13.9102,18.9609,13.6172 L18.9609,10.3828 C18.9609,10.0898,18.8555,9.83594,18.6445,9.625 C18.4336,9.41406,18.1797,9.30859,17.8828,9.30859 L15.0781,9.30859 Z M15.9609,13.8086 L15.9609,10.1914 L17.7695,10.1914 C17.8477,10.1914,17.918,10.2227,17.9805,10.2891 C18.043,10.3516,18.0781,10.4219,18.0781,10.5 L18.0781,13.5 C18.0781,13.5781,18.043,13.6484,17.9805,13.7109 C17.918,13.7773,17.8477,13.8086,17.7695,13.8086 Z M5.03906,14.6914 L5.92188,14.6914 L5.92188,12.6172 L7.92188,12.6172 L7.92188,11.7305 L5.92188,11.7305 L5.92188,10.1914 L8.42188,10.1914 L8.42188,9.30859 L5.03906,9.30859 Z M3.61719,19 C3.15625,19,2.76953,18.8477,2.46094,18.5391 C2.15234,18.2305,2,17.8438,2,17.3828 L2,6.61719 C2,6.15625,2.15234,5.76953,2.46094,5.46094 C2.76953,5.15234,3.15625,5,3.61719,5 L20.3828,5 C20.8438,5,21.2305,5.15234,21.5391,5.46094 C21.8477,5.76953,22,6.15625,22,6.61719 L22,17.3828 C22,17.8438,21.8477,18.2305,21.5391,18.5391 C21.2305,18.8477,20.8438,19,20.3828,19 Z M3.61719,18 L20.3828,18 C20.5625,18,20.7109,17.9414,20.8281,17.8281 C20.9414,17.7109,21,17.5625,21,17.3828 L21,6.61719 C21,6.4375,20.9414,6.28906,20.8281,6.17188 C20.7109,6.05859,20.5625,6,20.3828,6 L3.61719,6 C3.4375,6,3.28906,6.05859,3.17188,6.17188 C3.05859,6.28906,3,6.4375,3,6.61719 L3,17.3828 C3,17.5625,3.05859,17.7109,3.17188,17.8281 C3.28906,17.9414,3.4375,18,3.61719,18 Z M3,18 L3,6 Z M3,18" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.61719,5 C3.15625,5,2.76953,5.15234,2.46094,5.46094 C2.15234,5.76953,2,6.15625,2,6.61719 L2,17.3828 C2,17.8438,2.15234,18.2305,2.46094,18.5391 C2.76953,18.8477,3.15625,19,3.61719,19 L20.3828,19 C20.8438,19,21.2305,18.8477,21.5391,18.5391 C21.8477,18.2305,22,17.8438,22,17.3828 L22,6.61719 C22,6.15625,21.8477,5.76953,21.5391,5.46094 C21.2305,5.15234,20.8438,5,20.3828,5 L3.61719,5 Z M3.61719,6 L20.3828,6 C20.5625,6,20.7109,6.05859,20.8281,6.17188 C20.9414,6.28906,21,6.4375,21,6.61719 L21,17.3828 C21,17.5625,20.9414,17.7109,20.8281,17.8281 C20.7109,17.9414,20.5625,18,20.3828,18 L3.61719,18 C3.4375,18,3.28906,17.9414,3.17188,17.8281 C3.05859,17.7109,3,17.5625,3,17.3828 L3,6.61719 C3,6.4375,3.05859,6.28906,3.17188,6.17188 C3.28906,6.05859,3.4375,6,3.61719,6 Z M5.03906,9.30859 L5.03906,14.6914 L5.92188,14.6914 L5.92188,12.6172 L7.92188,12.6172 L7.92188,11.7305 L5.92188,11.7305 L5.92188,10.1914 L8.42188,10.1914 L8.42188,9.30859 L5.03906,9.30859 Z M9.80859,9.30859 L9.80859,14.6914 L10.6914,14.6914 L10.6914,12.6914 L12.8086,12.6914 L12.8086,14.6914 L13.6914,14.6914 L13.6914,9.30859 L12.8086,9.30859 L12.8086,11.8086 L10.6914,11.8086 L10.6914,9.30859 L9.80859,9.30859 Z M15.0781,9.30859 L15.0781,14.6914 L17.8828,14.6914 C18.1797,14.6914,18.4336,14.5859,18.6445,14.375 C18.8555,14.1641,18.9609,13.9102,18.9609,13.6172 L18.9609,10.3828 C18.9609,10.0898,18.8555,9.83594,18.6445,9.625 C18.4336,9.41406,18.1797,9.30859,17.8828,9.30859 L15.0781,9.30859 Z M15.9609,10.1914 L17.7695,10.1914 C17.8477,10.1914,17.918,10.2227,17.9805,10.2891 C18.043,10.3516,18.0781,10.4219,18.0781,10.5 L18.0781,13.5 C18.0781,13.5781,18.043,13.6484,17.9805,13.7109 C17.918,13.7773,17.8477,13.8086,17.7695,13.8086 L15.9609,13.8086 L15.9609,10.1914 Z M5.03906,15.5098 L5.03906,16.3926 L18.9609,16.3926 L18.9609,15.5098 L5.03906,15.5098 Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.61719,5 C3.15625,5,2.76953,5.15234,2.46094,5.46094 C2.15234,5.76953,2,6.15625,2,6.61719 L2,17.3828 C2,17.8438,2.15234,18.2305,2.46094,18.5391 C2.76953,18.8477,3.15625,19,3.61719,19 L20.3828,19 C20.8438,19,21.2305,18.8477,21.5391,18.5391 C21.8477,18.2305,22,17.8438,22,17.3828 L22,6.61719 C22,6.15625,21.8477,5.76953,21.5391,5.46094 C21.2305,5.15234,20.8438,5,20.3828,5 L3.61719,5 Z M3.61719,6 L20.3828,6 C20.5625,6,20.7109,6.05859,20.8281,6.17188 C20.9414,6.28906,21,6.4375,21,6.61719 L21,17.3828 C21,17.5625,20.9414,17.7109,20.8281,17.8281 C20.7109,17.9414,20.5625,18,20.3828,18 L3.61719,18 C3.4375,18,3.28906,17.9414,3.17188,17.8281 C3.05859,17.7109,3,17.5625,3,17.3828 L3,6.61719 C3,6.4375,3.05859,6.28906,3.17188,6.17188 C3.28906,6.05859,3.4375,6,3.61719,6 Z M6.63867,9.30859 L6.63867,14.6914 L7.52148,14.6914 L7.52148,12.6914 L10.1387,12.6914 L10.1387,14.6914 L11.0215,14.6914 L11.0215,9.30859 L10.1387,9.30859 L10.1387,11.8086 L7.52148,11.8086 L7.52148,9.30859 L6.63867,9.30859 Z M13.0215,9.30859 L13.0215,14.6914 L16.3301,14.6914 C16.627,14.6914,16.877,14.5859,17.0879,14.375 C17.2988,14.1641,17.4043,13.9102,17.4043,13.6172 L17.4043,10.3828 C17.4043,10.0898,17.2988,9.83594,17.0879,9.625 C16.877,9.41406,16.627,9.30859,16.3301,9.30859 L13.0215,9.30859 Z M13.9043,10.1914 L16.2129,10.1914 C16.291,10.1914,16.3613,10.2227,16.4238,10.2891 C16.4902,10.3516,16.5215,10.4219,16.5215,10.5 L16.5215,13.5 C16.5215,13.5781,16.4902,13.6484,16.4238,13.7109 C16.3613,13.7773,16.291,13.8086,16.2129,13.8086 L13.9043,13.8086 L13.9043,10.1914 Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.61719,5 C3.15625,5,2.76953,5.15234,2.46094,5.46094 C2.15234,5.76953,2,6.15625,2,6.61719 L2,17.3828 C2,17.8438,2.15234,18.2305,2.46094,18.5391 C2.76953,18.8477,3.15625,19,3.61719,19 L20.3828,19 C20.8438,19,21.2305,18.8477,21.5391,18.5391 C21.8477,18.2305,22,17.8438,22,17.3828 L22,6.61719 C22,6.15625,21.8477,5.76953,21.5391,5.46094 C21.2305,5.15234,20.8438,5,20.3828,5 L3.61719,5 Z M3.61719,6 L20.3828,6 C20.5625,6,20.7109,6.05859,20.8281,6.17188 C20.9414,6.28906,21,6.4375,21,6.61719 L21,17.3828 C21,17.5625,20.9414,17.7109,20.8281,17.8281 C20.7109,17.9414,20.5625,18,20.3828,18 L3.61719,18 C3.4375,18,3.28906,17.9414,3.17188,17.8281 C3.05859,17.7109,3,17.5625,3,17.3828 L3,6.61719 C3,6.4375,3.05859,6.28906,3.17188,6.17188 C3.28906,6.05859,3.4375,6,3.61719,6 Z M6.61523,9.30859 L6.61523,14.6914 L10.998,14.6914 L10.998,13.8086 L7.49805,13.8086 L7.49805,9.30859 L6.61523,9.30859 Z M13,9.30859 L13,14.6914 L16.3086,14.6914 C16.6048,14.6914,16.8575,14.5878,17.0684,14.377 C17.2792,14.166,17.3848,13.9112,17.3848,13.6152 L17.3848,10.3848 C17.3848,10.0888,17.2792,9.83405,17.0684,9.62305 C16.8575,9.41221,16.6048,9.30859,16.3086,9.30859 L13,9.30859 Z M13.8848,10.1914 L16.1914,10.1914 C16.2684,10.1914,16.3403,10.2249,16.4043,10.2891 C16.4685,10.3531,16.5,10.423,16.5,10.5 L16.5,13.5 C16.5,13.577,16.4685,13.6469,16.4043,13.7109 C16.3403,13.7751,16.2684,13.8086,16.1914,13.8086 L13.8848,13.8086 L13.8848,10.1914 Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.61719,5 C3.15625,5,2.76953,5.15234,2.46094,5.46094 C2.15234,5.76953,2,6.15625,2,6.61719 L2,17.3828 C2,17.8438,2.15234,18.2305,2.46094,18.5391 C2.76953,18.8477,3.15625,19,3.61719,19 L20.3828,19 C20.8438,19,21.2305,18.8477,21.5391,18.5391 C21.8477,18.2305,22,17.8438,22,17.3828 L22,6.61719 C22,6.15625,21.8477,5.76953,21.5391,5.46094 C21.2305,5.15234,20.8438,5,20.3828,5 L3.61719,5 Z M3.61719,6 L20.3828,6 C20.5625,6,20.7109,6.05859,20.8281,6.17188 C20.9414,6.28906,21,6.4375,21,6.61719 L21,17.3828 C21,17.5625,20.9414,17.7109,20.8281,17.8281 C20.7109,17.9414,20.5625,18,20.3828,18 L3.61719,18 C3.4375,18,3.28906,17.9414,3.17188,17.8281 C3.05859,17.7109,3,17.5625,3,17.3828 L3,6.61719 C3,6.4375,3.05859,6.28906,3.17188,6.17188 C3.28906,6.05859,3.4375,6,3.61719,6 Z M5.7168,9.30859 C5.41992,9.30859,5.16602,9.41406,4.95508,9.625 C4.74414,9.83594,4.63867,10.0898,4.63867,10.3828 L4.63867,13.6172 C4.63867,13.9102,4.74414,14.1641,4.95508,14.375 C5.16602,14.5859,5.41992,14.6914,5.7168,14.6914 L6.38867,14.6914 L6.38867,16.1914 L7.27148,16.1914 L7.27148,14.6914 L7.94727,14.6914 C8.24414,14.6914,8.49414,14.5859,8.70508,14.375 C8.91602,14.1641,9.02148,13.9102,9.02148,13.6172 L9.02148,10.3828 C9.02148,10.0898,8.91602,9.83594,8.70508,9.625 C8.49414,9.41406,8.24414,9.30859,7.94727,9.30859 L5.7168,9.30859 Z M10.3086,9.30859 L10.3086,14.6914 L11.1914,14.6914 L11.1914,12.6914 L13.3086,12.6914 L13.3086,14.6914 L14.1914,14.6914 L14.1914,9.30859 L13.3086,9.30859 L13.3086,11.8086 L11.1914,11.8086 L11.1914,9.30859 L10.3086,9.30859 Z M15.4785,9.30859 L15.4785,14.6914 L18.2832,14.6914 C18.5801,14.6914,18.834,14.5859,19.0449,14.375 C19.2559,14.1641,19.3613,13.9102,19.3613,13.6172 L19.3613,10.3828 C19.3613,10.0898,19.2559,9.83594,19.0449,9.625 C18.834,9.41406,18.5801,9.30859,18.2832,9.30859 L15.4785,9.30859 Z M5.83008,10.1914 L7.83008,10.1914 C7.90821,10.1914,7.97852,10.2227,8.04102,10.2891 C8.10742,10.3516,8.13867,10.4219,8.13867,10.5 L8.13867,13.5 C8.13867,13.5781,8.10743,13.6484,8.04102,13.7109 C7.97852,13.7773,7.9082,13.8086,7.83008,13.8086 L5.83008,13.8086 C5.75586,13.8086,5.68164,13.7773,5.61914,13.7109 C5.55664,13.6484,5.52148,13.5781,5.52148,13.5 L5.52148,10.5 C5.52148,10.4219,5.55664,10.3516,5.61914,10.2891 C5.68164,10.2227,5.75586,10.1914,5.83008,10.1914 Z M16.3613,10.1914 L18.1699,10.1914 C18.2481,10.1914,18.3184,10.2227,18.3809,10.2891 C18.4434,10.3516,18.4785,10.4219,18.4785,10.5 L18.4785,13.5 C18.4785,13.5781,18.4434,13.6484,18.3809,13.7109 C18.3184,13.7773,18.248,13.8086,18.1699,13.8086 L16.3613,13.8086 L16.3613,10.1914 Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M3.61719,5 C3.15625,5,2.76953,5.15234,2.46094,5.46094 C2.15234,5.76953,2,6.15625,2,6.61719 L2,17.3828 C2,17.8438,2.15234,18.2305,2.46094,18.5391 C2.76953,18.8477,3.15625,19,3.61719,19 L20.3828,19 C20.8438,19,21.2305,18.8477,21.5391,18.5391 C21.8477,18.2305,22,17.8438,22,17.3828 L22,6.61719 C22,6.15625,21.8477,5.76953,21.5391,5.46094 C21.2305,5.15234,20.8438,5,20.3828,5 L3.61719,5 Z M3.61719,6 L20.3828,6 C20.5625,6,20.7109,6.05859,20.8281,6.17188 C20.9414,6.28906,21,6.4375,21,6.61719 L21,17.3828 C21,17.5625,20.9414,17.7109,20.8281,17.8281 C20.7109,17.9414,20.5625,18,20.3828,18 L3.61719,18 C3.4375,18,3.28906,17.9414,3.17188,17.8281 C3.05859,17.7109,3,17.5625,3,17.3828 L3,6.61719 C3,6.4375,3.05859,6.28906,3.17188,6.17188 C3.28906,6.05859,3.4375,6,3.61719,6 Z M7.30859,9.30859 C7.10209,9.30859,6.93414,9.37055,6.80664,9.49805 C6.67914,9.62571,6.61523,9.79367,6.61523,10 L6.61523,11.6914 C6.61523,11.8979,6.67914,12.0659,6.80664,12.1934 C6.93414,12.3209,7.10209,12.3848,7.30859,12.3848 L10.1152,12.3848 L10.1152,13.8086 L7.5,13.8086 L7.5,13.3086 L6.61523,13.3086 L6.61523,14 C6.61523,14.2063,6.67914,14.3743,6.80664,14.502 C6.93414,14.6295,7.10209,14.6914,7.30859,14.6914 L10.3086,14.6914 C10.5149,14.6914,10.6809,14.6295,10.8086,14.502 C10.9361,14.3743,11,14.2063,11,14 L11,12.3086 C11,12.1021,10.9361,11.9341,10.8086,11.8066 C10.6809,11.6791,10.5149,11.6152,10.3086,11.6152 L7.5,11.6152 L7.5,10.1914 L10.1152,10.1914 L10.1152,10.6914 L11,10.6914 L11,10 C11,9.79367,10.9361,9.62571,10.8086,9.49805 C10.6809,9.37055,10.5149,9.30859,10.3086,9.30859 L7.30859,9.30859 Z M13,9.30859 L13,14.6914 L16.3086,14.6914 C16.6048,14.6914,16.8575,14.5878,17.0684,14.377 C17.2792,14.166,17.3848,13.9112,17.3848,13.6152 L17.3848,10.3848 C17.3848,10.0888,17.2792,9.83405,17.0684,9.62305 C16.8575,9.41221,16.6048,9.30859,16.3086,9.30859 L13,9.30859 Z M13.8848,10.1914 L16.1914,10.1914 C16.2684,10.1914,16.3403,10.2249,16.4043,10.2891 C16.4685,10.3531,16.5,10.423,16.5,10.5 L16.5,13.5 C16.5,13.577,16.4685,13.6469,16.4043,13.7109 C16.3403,13.7751,16.2684,13.8086,16.1914,13.8086 L13.8848,13.8086 L13.8848,10.1914 Z" />
</vector>

View File

@@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:fillAlpha="0.8"
android:strokeAlpha="0.8"
android:pathData="M3.61719,5 C3.15625,5,2.76953,5.15234,2.46094,5.46094 C2.15234,5.76953,2,6.15625,2,6.61719 L2,17.3828 C2,17.8438,2.15234,18.2305,2.46094,18.5391 C2.76953,18.8477,3.15625,19,3.61719,19 L20.3828,19 C20.8438,19,21.2305,18.8477,21.5391,18.5391 C21.8477,18.2305,22,17.8438,22,17.3828 L22,6.61719 C22,6.15625,21.8477,5.76953,21.5391,5.46094 C21.2305,5.15234,20.8438,5,20.3828,5 Z M3.61719,6 L20.3828,6 C20.5625,6,20.7109,6.05859,20.8281,6.17188 C20.9414,6.28906,21,6.4375,21,6.61719 L21,17.3828 C21,17.5625,20.9414,17.7109,20.8281,17.8281 C20.7109,17.9414,20.5625,18,20.3828,18 L3.61719,18 C3.4375,18,3.28906,17.9414,3.17188,17.8281 C3.05859,17.7109,3,17.5625,3,17.3828 L3,6.61719 C3,6.4375,3.05859,6.28906,3.17188,6.17188 C3.28906,6.05859,3.4375,6,3.61719,6 Z" />
</vector>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:yt="http://schemas.android.com/apk/res-auto"
android:id="@+id/youtube_controls_bottom_ui_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layoutDirection="ltr">
<com.google.android.libraries.youtube.common.ui.TouchImageView
android:id="@+id/revanced_video_quality_dialog_button"
style="@style/YouTubePlayerButton"
android:layout_width="48.0dip"
android:layout_height="60.0dip"
android:paddingTop="6.0dp"
android:paddingBottom="0dp"
android:scaleType="center"
yt:layout_constraintBottom_toTopOf="@+id/quick_actions_container"
yt:layout_constraintRight_toLeftOf="@+id/fullscreen_button" />
<View
android:id="@+id/revanced_video_quality_dialog_button_placeholder"
android:layout_width="48.0dip"
android:layout_height="60.0dip"
android:visibility="gone"
yt:layout_constraintBottom_toTopOf="@+id/quick_actions_container"
yt:layout_constraintRight_toLeftOf="@+id/fullscreen_button" />
</android.support.constraint.ConstraintLayout>

View File

@@ -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"