feat(YouTube - Playback speed): Show current playback speed on player speed dialog button (#5607)

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
This commit is contained in:
MarcaD
2025-08-09 04:27:52 +03:00
committed by GitHub
parent 9c0638d128
commit 30176a3318
38 changed files with 780 additions and 774 deletions

View File

@@ -329,7 +329,7 @@ public class Utils {
return (R) child;
}
throw new IllegalArgumentException("View with resource name '" + str + "' not found");
throw new IllegalArgumentException("View with resource name not found: " + str);
}
/**

View File

@@ -1,16 +1,20 @@
package app.revanced.extension.youtube
import app.revanced.extension.shared.Logger
import java.util.Collections
/**
* generic event provider class
*/
class Event<T> {
private val eventListeners = mutableSetOf<(T) -> Unit>()
private val eventListeners = Collections.synchronizedSet(mutableSetOf<(T) -> Unit>())
operator fun plusAssign(observer: (T) -> Unit) {
addObserver(observer)
}
fun addObserver(observer: (T) -> Unit) {
Logger.printDebug { "Adding observer: $observer" }
eventListeners.add(observer)
}
@@ -23,7 +27,8 @@ class Event<T> {
}
operator fun invoke(value: T) {
for (observer in eventListeners)
for (observer in eventListeners) {
observer.invoke(value)
}
}
}

View File

@@ -1,12 +1,18 @@
package app.revanced.extension.youtube.patches;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.Event;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.shared.VideoState;
/**
@@ -16,11 +22,31 @@ import app.revanced.extension.youtube.shared.VideoState;
public final class VideoInformation {
public interface PlaybackController {
// Methods are added to YT classes during patching.
boolean seekTo(long videoTime);
void seekToRelative(long videoTimeOffset);
// Methods are added during patching.
boolean patch_seekTo(long videoTime);
void patch_seekToRelative(long videoTimeOffset);
}
/**
* Interface to use obfuscated methods.
*/
public interface VideoQualityMenuInterface {
// Method is added during patching.
void patch_setQuality(VideoQuality quality);
}
/**
* 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 float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
/**
* Prefix present in all Short player parameters signature.
@@ -30,12 +56,10 @@ public final class VideoInformation {
private static WeakReference<PlaybackController> playerControllerRef = new WeakReference<>(null);
private static WeakReference<PlaybackController> mdxPlayerDirectorRef = new WeakReference<>(null);
@NonNull
private static String videoId = "";
private static long videoLength = 0;
private static long videoTime = -1;
@NonNull
private static volatile String playerResponseVideoId = "";
private static volatile boolean playerResponseVideoIdIsShort;
private static volatile boolean videoIdIsShort;
@@ -45,6 +69,44 @@ public final class VideoInformation {
*/
private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
private static int desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE;
private static boolean qualityNeedsUpdating;
/**
* The available qualities of the current video.
*/
@Nullable
private static VideoQuality[] currentQualities;
/**
* 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;
/**
* Callback for when the current quality changes.
*/
public static final Event<VideoQuality> onQualityChange = new Event<>();
@Nullable
public static VideoQuality[] getCurrentQualities() {
return currentQualities;
}
@Nullable
public static VideoQuality getCurrentQuality() {
return currentQuality;
}
/**
* Injection point.
*
@@ -52,12 +114,18 @@ public final class VideoInformation {
*/
public static void initialize(@NonNull PlaybackController playerController) {
try {
Logger.printDebug(() -> "newVideoStarted");
playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController));
videoTime = -1;
videoLength = 0;
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE;
currentQualities = null;
currentMenuInterface = null;
setCurrentQuality(null);
} catch (Exception ex) {
Logger.printException(() -> "Failed to initialize", ex);
Logger.printException(() -> "initialize failure", ex);
}
}
@@ -197,14 +265,14 @@ public final class VideoInformation {
if (controller == null) {
Logger.printDebug(() -> "Cannot seekTo because player controller is null");
} else {
if (controller.seekTo(adjustedSeekTime)) return true;
if (controller.patch_seekTo(adjustedSeekTime)) return true;
Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD.");
// Else the video is loading or changing videos, or video is casting to a different device.
}
// Try calling the seekTo method of the MDX player director (called when casting).
// The difference has to be a different second mark in order to avoid infinite skip loops
// as the Lounge API only supports seconds.
// as the Lounge API only supports whole seconds.
if (adjustedSeekTime / 1000 == videoTime / 1000) {
Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small "
+ "(" + (adjustedSeekTime - videoTime) + "ms)");
@@ -217,9 +285,9 @@ public final class VideoInformation {
return false;
}
return controller.seekTo(adjustedSeekTime);
return controller.patch_seekTo(adjustedSeekTime);
} catch (Exception ex) {
Logger.printException(() -> "Failed to seek", ex);
Logger.printException(() -> "seekTo failure", ex);
return false;
}
}
@@ -239,7 +307,7 @@ public final class VideoInformation {
if (controller == null) {
Logger.printDebug(() -> "Cannot seek relative as player controller is null");
} else {
controller.seekToRelative(seekTime);
controller.patch_seekToRelative(seekTime);
}
// Adjust the fine adjustment function so it's at least 1 second before/after.
@@ -255,10 +323,10 @@ public final class VideoInformation {
if (controller == null) {
Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null");
} else {
controller.seekToRelative(adjustedSeekTime);
controller.patch_seekToRelative(adjustedSeekTime);
}
} catch (Exception ex) {
Logger.printException(() -> "Failed to seek relative", ex);
Logger.printException(() -> "seekToRelative failure", ex);
}
}
@@ -373,4 +441,123 @@ public final class VideoInformation {
playbackSpeed = newlyLoadedPlaybackSpeed;
}
}
/**
* @param resolution The desired video quality resolution to use.
*/
public static void setDesiredVideoResolution(int resolution) {
Utils.verifyOnMainThread();
Logger.printDebug(() -> "Setting desired video resolution: " + resolution);
desiredVideoResolution = resolution;
qualityNeedsUpdating = true;
}
private static void setCurrentQuality(@Nullable VideoQuality quality) {
Utils.verifyOnMainThread();
if (currentQuality != quality) {
Logger.printDebug(() -> "Current quality changed to: " + quality);
currentQuality = quality;
onQualityChange.invoke(quality);
}
}
/**
* Forcefully changes the video quality of the currently playing video.
*/
public static void changeQuality(VideoQuality quality) {
Utils.verifyOnMainThread();
if (currentMenuInterface == null) {
Logger.printException(() -> "Cannot change quality, menu interface is null");
return;
}
currentMenuInterface.patch_setQuality(quality);
}
/**
* 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;
}
/**
* Injection point.
*
* @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
* @param originalQualityIndex quality index to use, as chosen by YouTube
*/
public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) {
try {
Utils.verifyOnMainThread();
currentMenuInterface = menu;
final boolean availableQualitiesChanged = (currentQualities == null)
|| !Arrays.equals(currentQualities, qualities);
if (availableQualitiesChanged) {
currentQualities = qualities;
Logger.printDebug(() -> "VideoQualities: " + Arrays.toString(currentQualities));
}
VideoQuality updatedCurrentQuality = qualities[originalQualityIndex];
if (updatedCurrentQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE
&& (currentQuality == null || currentQuality != updatedCurrentQuality)) {
setCurrentQuality(updatedCurrentQuality);
}
final int preferredQuality = desiredVideoResolution;
if (preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
return originalQualityIndex; // Nothing to do.
}
// 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) {
qualityNeedsUpdating = false;
} else if (!availableQualitiesChanged) {
return originalQualityIndex;
}
// Find the highest quality that is equal to or less than the preferred.
int i = 0;
final int lastQualityIndex = qualities.length - 1;
for (VideoQuality quality : qualities) {
final int qualityResolution = quality.patch_getResolution();
if ((qualityResolution != AUTOMATIC_VIDEO_QUALITY_VALUE && qualityResolution <= preferredQuality)
// Use the lowest video quality if the default is lower than all available.
|| i == lastQualityIndex) {
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()) {
changeQuality(quality);
return i;
}
return originalQualityIndex;
}
i++;
}
} catch (Exception ex) {
Logger.printException(() -> "setVideoQuality failure", ex);
}
return originalQualityIndex;
}
}

View File

@@ -3,12 +3,8 @@ package app.revanced.extension.youtube.patches.playback.quality;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.NetworkType;
import androidx.annotation.Nullable;
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
import java.util.Arrays;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BooleanSetting;
@@ -16,69 +12,15 @@ 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 {
/**
* Interface to use obfuscated methods.
*/
public interface VideoQualityMenuInterface {
void patch_setQuality(VideoQuality quality);
}
/**
* 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;
private static final IntegerSetting shortsQualityMobile = Settings.SHORTS_QUALITY_DEFAULT_MOBILE;
private static boolean qualityNeedsUpdating;
/**
* The available qualities of the current video.
*/
@Nullable
private static VideoQuality[] currentQualities;
/**
* 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 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()
@@ -128,87 +70,12 @@ public class RememberVideoQualityPatch {
/**
* Injection point.
*
* @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
* @param originalQualityIndex quality index to use, as chosen by YouTube
*/
public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) {
try {
Utils.verifyOnMainThread();
currentMenuInterface = menu;
final boolean availableQualitiesChanged = (currentQualities == null)
|| !Arrays.equals(currentQualities, qualities);
if (availableQualitiesChanged) {
currentQualities = qualities;
Logger.printDebug(() -> "VideoQualities: " + Arrays.toString(currentQualities));
}
VideoQuality updatedCurrentQuality = qualities[originalQualityIndex];
if (updatedCurrentQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE
&& (currentQuality == null || currentQuality != updatedCurrentQuality)) {
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.
}
// 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.
int i = 0;
for (VideoQuality quality : qualities) {
final int qualityResolution = quality.patch_getResolution();
if ((qualityResolution != AUTOMATIC_VIDEO_QUALITY_VALUE && qualityResolution <= preferredQuality)
// Use the lowest video quality if the default is lower than all available.
|| i == qualities.length - 1) {
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++;
}
} catch (Exception ex) {
Logger.printException(() -> "setVideoQuality failure", ex);
}
return originalQualityIndex;
}
/**
* Injection point.
* @param userSelectedQualityIndex Element index of {@link #currentQualities}.
* @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}.
*/
public static void userChangedShortsQuality(int userSelectedQualityIndex) {
try {
if (shouldRememberVideoQuality()) {
VideoQuality[] currentQualities = VideoInformation.getCurrentQualities();
if (currentQualities == null) {
Logger.printDebug(() -> "Cannot save default quality, qualities is null");
return;
@@ -227,6 +94,7 @@ public class RememberVideoQualityPatch {
*/
public static void userChangedQuality(int videoResolution) {
Utils.verifyOnMainThread();
Logger.printDebug(() -> "User changed quality to: " + videoResolution);
if (shouldRememberVideoQuality()) {
saveDefaultQuality(videoResolution);
@@ -237,27 +105,6 @@ public class RememberVideoQualityPatch {
* Injection point.
*/
public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) {
Utils.verifyOnMainThread();
Logger.printDebug(() -> "newVideoStarted");
currentQualities = null;
currentQuality = null;
currentMenuInterface = null;
qualityNeedsUpdating = true;
// 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;
VideoInformation.setDesiredVideoResolution(getDefaultQualityResolution());
}
}

View File

@@ -100,7 +100,8 @@ public class CustomPlaybackSpeedPatch {
private static WeakReference<Dialog> currentDialog = new WeakReference<>(null);
static {
// Cap at 2 decimals (rounds automatically).
// Use same 2 digit format as built in speed picker,
speedFormatter.setMinimumFractionDigits(2);
speedFormatter.setMaximumFractionDigits(2);
final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get();
@@ -321,7 +322,7 @@ public class CustomPlaybackSpeedPatch {
TextView currentSpeedText = new TextView(context);
float currentSpeed = VideoInformation.getPlaybackSpeed();
// Initially show with only 0 minimum digits, so 1.0 shows as 1x
currentSpeedText.setText(formatSpeedStringX(currentSpeed, 0));
currentSpeedText.setText(formatSpeedStringX(currentSpeed));
currentSpeedText.setTextColor(Utils.getAppForegroundColor());
currentSpeedText.setTextSize(16);
currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD);
@@ -398,10 +399,11 @@ public class CustomPlaybackSpeedPatch {
return null;
}
VideoInformation.overridePlaybackSpeed(roundedSpeed);
RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
currentSpeedText.setText(formatSpeedStringX(roundedSpeed, 2)); // Update display.
currentSpeedText.setText(formatSpeedStringX(roundedSpeed)); // Update display.
speedSlider.setProgress(speedToProgressValue(roundedSpeed)); // Update slider.
RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
VideoInformation.overridePlaybackSpeed(roundedSpeed);
return null;
};
@@ -437,7 +439,7 @@ public class CustomPlaybackSpeedPatch {
gridParams.setMargins(0, 0, 0, 0); // No margins around GridLayout.
gridLayout.setLayoutParams(gridParams);
// For all buttons show at least 1 zero in decimal (2 -> "2.0").
// For button use 1 digit minimum.
speedFormatter.setMinimumFractionDigits(1);
// Add buttons for each preset playback speed.
@@ -455,7 +457,7 @@ public class CustomPlaybackSpeedPatch {
// Create speed button.
Button speedButton = new Button(context, null, 0);
speedButton.setText(speedFormatter.format(speed)); // Do not use 'x' speed format.
speedButton.setText(speedFormatter.format(speed));
speedButton.setTextColor(Utils.getAppForegroundColor());
speedButton.setTextSize(12);
speedButton.setAllCaps(false);
@@ -498,6 +500,9 @@ public class CustomPlaybackSpeedPatch {
gridLayout.addView(buttonContainer);
}
// Restore 2 digit minimum.
speedFormatter.setMinimumFractionDigits(2);
// Add in-rows speed buttons layout to main layout.
mainLayout.addView(gridLayout);
@@ -631,8 +636,7 @@ public class CustomPlaybackSpeedPatch {
* @param speed The playback speed value to format.
* @return A string representation of the speed with 'x' (e.g. "1.25x" or "1.00x").
*/
private static String formatSpeedStringX(float speed, int minimumFractionDigits) {
speedFormatter.setMinimumFractionDigits(minimumFractionDigits);
private static String formatSpeedStringX(float speed) {
return speedFormatter.format(speed) + 'x';
}

View File

@@ -26,6 +26,7 @@ public class CreateSegmentButton {
controlsView,
"revanced_sb_create_segment_button",
null,
null,
CreateSegmentButton::shouldBeShown,
v -> SponsorBlockViewController.toggleNewSegmentLayoutVisibility(),
null

View File

@@ -28,6 +28,7 @@ public class VotingButton {
controlsView,
"revanced_sb_voting_button",
null,
null,
VotingButton::shouldBeShown,
v -> SponsorBlockUtils.onVotingClicked(v.getContext()),
null

View File

@@ -7,7 +7,6 @@ import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.youtube.patches.CopyVideoUrlPatch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@SuppressWarnings("unused")
public class CopyVideoUrlButton {
@@ -23,6 +22,7 @@ public class CopyVideoUrlButton {
controlsView,
"revanced_copy_video_url_button",
"revanced_copy_video_url_button_placeholder",
null,
Settings.COPY_VIDEO_URL::get,
view -> CopyVideoUrlPatch.copyUrl(false),
view -> {

View File

@@ -7,7 +7,6 @@ import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.youtube.patches.CopyVideoUrlPatch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@SuppressWarnings("unused")
public class CopyVideoUrlTimestampButton {
@@ -23,6 +22,7 @@ public class CopyVideoUrlTimestampButton {
controlsView,
"revanced_copy_video_url_timestamp_button",
"revanced_copy_video_url_timestamp_button_placeholder",
null,
Settings.COPY_VIDEO_URL_TIMESTAMP::get,
view -> CopyVideoUrlPatch.copyUrl(true),
view -> {

View File

@@ -23,6 +23,7 @@ public class ExternalDownloadButton {
controlsView,
"revanced_external_download_button",
"revanced_external_download_button_placeholder",
null,
Settings.EXTERNAL_DOWNLOADER::get,
ExternalDownloadButton::onDownloadClick,
null

View File

@@ -4,16 +4,26 @@ import android.view.View;
import androidx.annotation.Nullable;
import java.text.DecimalFormat;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class PlaybackSpeedDialogButton {
@Nullable
private static PlayerControlButton instance;
private static final DecimalFormat speedDecimalFormatter = new DecimalFormat();
static {
speedDecimalFormatter.setMinimumFractionDigits(1);
speedDecimalFormatter.setMaximumFractionDigits(2);
}
/**
* Injection point.
*/
@@ -21,8 +31,10 @@ public class PlaybackSpeedDialogButton {
try {
instance = new PlayerControlButton(
controlsView,
"revanced_playback_speed_dialog_button_container",
"revanced_playback_speed_dialog_button",
"revanced_playback_speed_dialog_button_placeholder",
"revanced_playback_speed_dialog_button_text",
Settings.PLAYBACK_SPEED_DIALOG_BUTTON::get,
view -> {
try {
@@ -37,11 +49,11 @@ public class PlaybackSpeedDialogButton {
},
view -> {
try {
final float defaultSpeed = Settings.PLAYBACK_SPEED_DEFAULT.get();
final float speed = (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get() ||
VideoInformation.getPlaybackSpeed() == Settings.PLAYBACK_SPEED_DEFAULT.get())
VideoInformation.getPlaybackSpeed() == defaultSpeed)
? 1.0f
: Settings.PLAYBACK_SPEED_DEFAULT.get();
: defaultSpeed;
VideoInformation.overridePlaybackSpeed(speed);
} catch (Exception ex) {
Logger.printException(() -> "speed button reset failure", ex);
@@ -49,22 +61,53 @@ public class PlaybackSpeedDialogButton {
return true;
}
);
// Set the appropriate icon.
updateButtonAppearance();
} catch (Exception ex) {
Logger.printException(() -> "initializeButton failure", ex);
}
}
/**
* injection point
* Injection point.
*/
public static void setVisibilityImmediate(boolean visible) {
if (instance != null) instance.setVisibilityImmediate(visible);
if (instance != null) {
instance.setVisibilityImmediate(visible);
}
}
/**
* injection point
* Injection point.
*/
public static void setVisibility(boolean visible, boolean animated) {
if (instance != null) instance.setVisibility(visible, animated);
if (instance != null) {
instance.setVisibility(visible, animated);
}
}
/**
* Injection point.
*/
public static void videoSpeedChanged(float currentVideoSpeed) {
updateButtonAppearance();
}
/**
* Updates the button's appearance, including icon and text overlay.
*/
private static void updateButtonAppearance() {
if (instance == null) return;
try {
Utils.verifyOnMainThread();
String speedText = speedDecimalFormatter.format(VideoInformation.getPlaybackSpeed());
instance.setTextOverlay(speedText);
Logger.printDebug(() -> "Updated playback speed button text to: " + speedText);
} catch (Exception ex) {
Logger.printException(() -> "updateButtonAppearance failure", ex);
}
}
}

View File

@@ -3,6 +3,7 @@ package app.revanced.extension.youtube.videoplayer;
import android.view.View;
import android.view.animation.Animation;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
@@ -14,11 +15,12 @@ import app.revanced.extension.youtube.shared.PlayerType;
import kotlin.Unit;
public class PlayerControlButton {
public interface PlayerControlButtonVisibility {
public interface PlayerControlButtonStatus {
/**
* @return If the button should be shown when the player overlay is visible.
*/
boolean shouldBeShown();
boolean buttonEnabled();
}
private static final int fadeInDuration;
@@ -44,23 +46,46 @@ public class PlayerControlButton {
fadeOutImmediate.setDuration(Utils.getResourceInteger("fade_duration_fast"));
}
private final WeakReference<View> containerRef;
private final WeakReference<View> buttonRef;
/**
* Empty view with the same layout size as the button. Used to fill empty space while the
* fade out animation runs. Without this the chapter titles overlapping the button when fading out.
*/
private final WeakReference<View> placeHolderRef;
private final PlayerControlButtonVisibility visibilityCheck;
private final WeakReference<TextView> textOverlayRef;
private final PlayerControlButtonStatus enabledStatus;
private boolean isVisible;
public PlayerControlButton(View controlsViewGroup,
String imageViewButtonId,
String buttonId,
@Nullable String placeholderId,
PlayerControlButtonVisibility buttonVisibility,
@Nullable String textOverlayId,
PlayerControlButtonStatus enabledStatus,
View.OnClickListener onClickListener,
@Nullable View.OnLongClickListener longClickListener) {
ImageView imageView = Utils.getChildViewByResourceName(controlsViewGroup, imageViewButtonId);
imageView.setVisibility(View.GONE);
this(controlsViewGroup, buttonId, buttonId, placeholderId, textOverlayId,
enabledStatus, onClickListener, longClickListener);
}
public PlayerControlButton(View controlsViewGroup,
String viewToHide,
String buttonId,
@Nullable String placeholderId,
@Nullable String textOverlayId,
PlayerControlButtonStatus enabledStatus,
View.OnClickListener onClickListener,
@Nullable View.OnLongClickListener longClickListener) {
View containerView = Utils.getChildViewByResourceName(controlsViewGroup, viewToHide);
containerView.setVisibility(View.GONE);
containerRef = new WeakReference<>(containerView);
View button = Utils.getChildViewByResourceName(controlsViewGroup, buttonId);
button.setOnClickListener(onClickListener);
if (longClickListener != null) {
button.setOnLongClickListener(longClickListener);
}
buttonRef = new WeakReference<>(button);
View tempPlaceholder = null;
if (placeholderId != null) {
@@ -69,19 +94,19 @@ public class PlayerControlButton {
}
placeHolderRef = new WeakReference<>(tempPlaceholder);
imageView.setOnClickListener(onClickListener);
if (longClickListener != null) {
imageView.setOnLongClickListener(longClickListener);
TextView tempTextOverlay = null;
if (textOverlayId != null) {
tempTextOverlay = Utils.getChildViewByResourceName(controlsViewGroup, textOverlayId);
}
textOverlayRef = new WeakReference<>(tempTextOverlay);
visibilityCheck = buttonVisibility;
buttonRef = new WeakReference<>(imageView);
this.enabledStatus = enabledStatus;
isVisible = false;
// Update the visibility after the player type changes.
// This ensures that button animations are cleared and their states are updated correctly
// when switching between states like minimized, maximized, or fullscreen, preventing
// "stuck" animations or incorrect visibility. Without this fix the issue is most noticable
// "stuck" animations or incorrect visibility. Without this fix the issue is most noticeable
// when maximizing type 3 miniplayer.
PlayerType.getOnChange().addObserver((PlayerType type) -> {
playerTypeChanged(type);
@@ -111,33 +136,33 @@ public class PlayerControlButton {
if (isVisible == visible) return;
isVisible = visible;
View button = buttonRef.get();
if (button == null) return;
View container = containerRef.get();
if (container == null) return;
View placeholder = placeHolderRef.get();
final boolean shouldBeShown = visibilityCheck.shouldBeShown();
final boolean buttonEnabled = enabledStatus.buttonEnabled();
if (visible && shouldBeShown) {
button.clearAnimation();
if (visible && buttonEnabled) {
container.clearAnimation();
if (animated) {
button.startAnimation(PlayerControlButton.fadeInAnimation);
container.startAnimation(fadeInAnimation);
}
button.setVisibility(View.VISIBLE);
container.setVisibility(View.VISIBLE);
if (placeholder != null) {
placeholder.setVisibility(View.GONE);
}
} else {
if (button.getVisibility() == View.VISIBLE) {
button.clearAnimation();
if (container.getVisibility() == View.VISIBLE) {
container.clearAnimation();
if (animated) {
button.startAnimation(PlayerControlButton.fadeOutAnimation);
container.startAnimation(fadeOutAnimation);
}
button.setVisibility(View.GONE);
container.setVisibility(View.GONE);
}
if (placeholder != null) {
placeholder.setVisibility(shouldBeShown
placeholder.setVisibility(buttonEnabled
? View.VISIBLE
: View.GONE);
}
@@ -155,36 +180,37 @@ public class PlayerControlButton {
return;
}
View button = buttonRef.get();
if (button == null) return;
View container = containerRef.get();
if (container == null) return;
button.clearAnimation();
container.clearAnimation();
View placeholder = placeHolderRef.get();
if (visibilityCheck.shouldBeShown()) {
if (enabledStatus.buttonEnabled()) {
if (isVisible) {
button.setVisibility(View.VISIBLE);
container.setVisibility(View.VISIBLE);
if (placeholder != null) placeholder.setVisibility(View.GONE);
} else {
button.setVisibility(View.GONE);
container.setVisibility(View.GONE);
if (placeholder != null) placeholder.setVisibility(View.VISIBLE);
}
} else {
button.setVisibility(View.GONE);
container.setVisibility(View.GONE);
if (placeholder != null) placeholder.setVisibility(View.GONE);
}
}
public void hide() {
Utils.verifyOnMainThread();
if (!isVisible) return;
Utils.verifyOnMainThread();
View view = buttonRef.get();
View view = containerRef.get();
if (view == null) return;
view.setVisibility(View.GONE);
view = placeHolderRef.get();
if (view != null) view.setVisibility(View.GONE);
View placeHolder = placeHolderRef.get();
if (placeHolder != null) view.setVisibility(View.GONE);
isVisible = false;
}
@@ -193,50 +219,20 @@ public class PlayerControlButton {
* @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.
* Sets the text to be displayed on the text overlay.
* @param text The text to set on the overlay, or null to clear the text.
*/
public void startAnimation(Animation animation) {
try {
View button = buttonRef.get();
if (button != null) {
button.startAnimation(animation);
}
} catch (Exception ex) {
Logger.printException(() -> "startAnimation failure", ex);
public void setTextOverlay(CharSequence text) {
TextView textOverlay = textOverlayRef.get();
if (textOverlay != null) {
textOverlay.setText(text);
}
}
/**
* 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

@@ -2,9 +2,8 @@ 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 static app.revanced.extension.youtube.patches.VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE;
import static app.revanced.extension.youtube.patches.VideoInformation.VIDEO_QUALITY_1080P_PREMIUM_NAME;
import android.app.Dialog;
import android.content.Context;
@@ -14,6 +13,7 @@ import android.graphics.drawable.shapes.RoundRectShape;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.text.style.UnderlineSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
@@ -39,73 +39,25 @@ import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch;
import app.revanced.extension.youtube.settings.Settings;
import kotlin.Unit;
@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;
@Nullable
private static CharSequence currentOverlayText;
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);
}
static {
VideoInformation.onQualityChange.addObserver((@Nullable VideoQuality quality) -> {
updateButtonText(quality);
return Unit.INSTANCE;
});
}
/**
@@ -115,8 +67,10 @@ public class VideoQualityDialogButton {
try {
instance = new PlayerControlButton(
controlsView,
"revanced_video_quality_dialog_button_container",
"revanced_video_quality_dialog_button",
"revanced_video_quality_dialog_button_placeholder",
"revanced_video_quality_dialog_button_text",
Settings.VIDEO_QUALITY_DIALOG_BUTTON::get,
view -> {
try {
@@ -127,9 +81,8 @@ public class VideoQualityDialogButton {
},
view -> {
try {
VideoQuality[] qualities = RememberVideoQualityPatch.getCurrentQualities();
VideoQualityMenuInterface menu = RememberVideoQualityPatch.getCurrentMenuInterface();
if (qualities == null || menu == null) {
VideoQuality[] qualities = VideoInformation.getCurrentQualities();
if (qualities == null) {
Logger.printDebug(() -> "Cannot reset quality, videoQualities is null");
return true;
}
@@ -140,7 +93,7 @@ public class VideoQualityDialogButton {
final int resolution = quality.patch_getResolution();
if (resolution != AUTOMATIC_VIDEO_QUALITY_VALUE && resolution <= defaultResolution) {
Logger.printDebug(() -> "Resetting quality to: " + quality);
menu.patch_setQuality(quality);
VideoInformation.changeQuality(quality);
return true;
}
}
@@ -156,8 +109,8 @@ public class VideoQualityDialogButton {
}
);
// Set initial icon.
updateButtonIcon(RememberVideoQualityPatch.getCurrentQuality());
// Set initial text.
updateButtonText(VideoInformation.getCurrentQuality());
} catch (Exception ex) {
Logger.printException(() -> "initializeButton failure", ex);
}
@@ -181,13 +134,56 @@ public class VideoQualityDialogButton {
}
}
/**
* Updates the button text based on the current video quality.
*/
public static void updateButtonText(@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();
SpannableStringBuilder text = new SpannableStringBuilder();
String qualityText = switch (resolution) {
case AUTOMATIC_VIDEO_QUALITY_VALUE -> "";
case 144, 240, 360 -> "LD";
case 480 -> "SD";
case 720 -> "HD";
case 1080 -> "FHD";
case 1440 -> "QHD";
case 2160 -> "4K";
default -> "?"; // Should never happen.
};
text.append(qualityText);
if (resolution == 1080 && VIDEO_QUALITY_1080P_PREMIUM_NAME.equals(quality.patch_getQualityName())) {
// Underline the entire "FHD" text for 1080p Premium.
text.setSpan(new UnderlineSpan(), 0, qualityText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
currentOverlayText = text;
Utils.runOnMainThreadDelayed(() -> {
if (currentOverlayText != text) {
Logger.printDebug(() -> "Ignoring stale button text update of: " + text);
return;
}
instance.setTextOverlay(text);
}, 100);
} catch (Exception ex) {
Logger.printException(() -> "updateButtonText failure", ex);
}
}
/**
* Shows a dialog with available video qualities, excluding Auto, with a title showing the current quality.
*/
private static void showVideoQualityDialog(Context context) {
try {
VideoQuality[] currentQualities = RememberVideoQualityPatch.getCurrentQualities();
VideoQuality currentQuality = RememberVideoQualityPatch.getCurrentQuality();
VideoQuality[] currentQualities = VideoInformation.getCurrentQualities();
VideoQuality currentQuality = VideoInformation.getCurrentQuality();
if (currentQualities == null || currentQuality == null) {
Logger.printDebug(() -> "Cannot show qualities dialog, videoQualities is null");
return;
@@ -198,12 +194,6 @@ public class VideoQualityDialogButton {
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.
int listViewSelectedIndex = -1;
for (VideoQuality quality : currentQualities) {
@@ -317,15 +307,8 @@ public class VideoQualityDialogButton {
try {
final int originalIndex = which + 1; // Adjust for automatic.
VideoQuality selectedQuality = currentQualities[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);
RememberVideoQualityPatch.userChangedQuality(selectedQuality.patch_getResolution());
VideoInformation.changeQuality(selectedQuality);
dialog.dismiss();
} catch (Exception ex) {
@@ -356,9 +339,12 @@ public class VideoQualityDialogButton {
portraitWidth = Math.min(
portraitWidth,
context.getResources().getDisplayMetrics().heightPixels);
// Limit height in landscape mode.
params.height = Utils.percentageHeightToPixels(80);
} else {
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
}
params.width = portraitWidth;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
window.setAttributes(params);
window.setBackgroundDrawable(null);
}
@@ -428,6 +414,15 @@ public class VideoQualityDialogButton {
}
private static class CustomQualityAdapter extends ArrayAdapter<String> {
private static final int CUSTOM_LIST_ITEM_CHECKED_ID = Utils.getResourceIdentifier(
"revanced_custom_list_item_checked", "layout");
private static final int CHECK_ICON_ID = Utils.getResourceIdentifier(
"revanced_check_icon", "id");
private static final int CHECK_ICON_PLACEHOLDER_ID = Utils.getResourceIdentifier(
"revanced_check_icon_placeholder", "id");
private static final int ITEM_TEXT_ID = Utils.getResourceIdentifier(
"revanced_item_text", "id");
private int selectedPosition = -1;
public CustomQualityAdapter(@NonNull Context context, @NonNull List<String> objects) {
@@ -446,20 +441,14 @@ public class VideoQualityDialogButton {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(
Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"),
CUSTOM_LIST_ITEM_CHECKED_ID,
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")
);
viewHolder.checkIcon = convertView.findViewById(CHECK_ICON_ID);
viewHolder.placeholder = convertView.findViewById(CHECK_ICON_PLACEHOLDER_ID);
viewHolder.textView = convertView.findViewById(ITEM_TEXT_ID);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();

View File

@@ -1660,12 +1660,12 @@ public final class app/revanced/patches/youtube/video/quality/RememberVideoQuali
public static final fun getRememberVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/video/quality/VideoQualityPatchKt {
public static final fun getVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
public final class app/revanced/patches/youtube/video/quality/VideoQualityDialogButtonPatchKt {
public static final fun getVideoQualityDialogButtonPatch ()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/quality/VideoQualityPatchKt {
public static final fun getVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/video/speed/PlaybackSpeedPatchKt {

View File

@@ -282,7 +282,7 @@ val playerControlsPatch = bytecodePatch(
// The change to support this is simple and only requires adding buttons to both layout files,
// but for now force this different layout off since it's still an experimental test.
if (is_19_35_or_greater) {
playerBottomControlsExploderFeatureFlagFingerprint.method.returnEarly()
playerBottomControlsExploderFeatureFlagFingerprint.method.returnLate(false)
}
// A/B test of new top overlay controls. Two different layouts can be used:

View File

@@ -70,18 +70,14 @@ val forceOriginalAudioPatch = bytecodePatch(
)
}
val isDefaultAudioTrackMethod = formatStreamModelToStringFingerprint.originalMethod
.findMethodFromToString("isDefaultAudioTrack=")
val audioTrackDisplayNameMethod = formatStreamModelToStringFingerprint.originalMethod
.findMethodFromToString("audioTrackDisplayName=")
val audioTrackIdMethod = formatStreamModelToStringFingerprint.originalMethod
.findMethodFromToString("audioTrackId=")
formatStreamModelToStringFingerprint.let {
val isDefaultAudioTrackMethod = it.originalMethod.findMethodFromToString("isDefaultAudioTrack=")
val audioTrackDisplayNameMethod = it.originalMethod.findMethodFromToString("audioTrackDisplayName=")
val audioTrackIdMethod = it.originalMethod.findMethodFromToString("audioTrackId=")
proxy(classes.first {
it.type == audioTrackIdMethod.definingClass
}).mutableClass.apply {
it.classDef.apply {
// Add a new field to store the override.
val helperFieldName = "isDefaultAudioTrackOverride"
val helperFieldName = "patch_isDefaultAudioTrackOverride"
fields.add(
ImmutableField(
type,
@@ -155,3 +151,4 @@ val forceOriginalAudioPatch = bytecodePatch(
}
}
}
}

View File

@@ -133,3 +133,46 @@ internal val playbackSpeedClassFingerprint = fingerprint {
)
strings("PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT")
}
internal const val YOUTUBE_VIDEO_QUALITY_CLASS_TYPE = "Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;"
internal val videoQualityFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
parameters(
"I", // Resolution.
"Ljava/lang/String;", // Human readable resolution: "480p", "1080p Premium", etc
"Z",
"L"
)
custom { _, classDef ->
classDef.type == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
}
}
internal val videoQualitySetterFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
parameters("[L", "I", "Z")
opcodes(
Opcode.IF_EQZ,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.IPUT_BOOLEAN,
)
strings("menu_item_video_quality")
}
/**
* Matches with the class found in [videoQualitySetterFingerprint].
*/
internal val setVideoQualityFingerprint = fingerprint {
returns("V")
parameters("L")
opcodes(
Opcode.IGET_OBJECT,
Opcode.IPUT_OBJECT,
Opcode.IGET_OBJECT,
)
}

View File

@@ -38,7 +38,9 @@ import com.android.tools.smali.dexlib2.util.MethodUtil
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/VideoInformation;"
private const val EXTENSION_PLAYER_INTERFACE =
"Lapp/revanced/extension/youtube/patches/VideoInformation${'$'}PlaybackController;"
"Lapp/revanced/extension/youtube/patches/VideoInformation\$PlaybackController;"
private const val EXTENSION_VIDEO_QUALITY_MENU_INTERFACE =
"Lapp/revanced/extension/youtube/patches/VideoInformation\$VideoQualityMenuInterface;"
private lateinit var playerInitMethod: MutableMethod
private var playerInitInsertIndex = -1
@@ -83,7 +85,6 @@ val videoInformationPatch = bytecodePatch(
)
execute {
playerInitMethod = playerInitFingerprint.classDef.methods.first { MethodUtil.isConstructor(it) }
// Find the location of the first invoke-direct call and extract the register storing the 'this' object reference.
@@ -93,9 +94,6 @@ val videoInformationPatch = bytecodePatch(
playerInitInsertRegister = playerInitMethod.getInstruction<FiveRegisterInstruction>(initThisIndex).registerC
playerInitInsertIndex = initThisIndex + 1
// Hook the player controller for use through the extension.
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "initialize")
val seekFingerprintResultMethod = seekFingerprint.match(playerInitFingerprint.originalClassDef).method
val seekRelativeFingerprintResultMethod =
seekRelativeFingerprint.match(playerInitFingerprint.originalClassDef).method
@@ -272,6 +270,131 @@ val videoInformationPatch = bytecodePatch(
speedSelectionValueRegister = getInstruction<TwoRegisterInstruction>(index).registerA
}
videoQualityFingerprint.let {
// Fix bad data used by YouTube.
it.method.addInstructions(
0,
"""
invoke-static { p2, p1 }, $EXTENSION_CLASS_DESCRIPTOR->fixVideoQualityResolution(Ljava/lang/String;I)I
move-result p1
"""
)
// Add methods to access obfuscated quality fields.
it.classDef.apply {
methods.add(
ImmutableMethod(
type,
"patch_getQualityName",
listOf(),
"Ljava/lang/String;",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
// Only one string field.
val qualityNameField = fields.single { field ->
field.type == "Ljava/lang/String;"
}
addInstructions(
0,
"""
iget-object v0, p0, $qualityNameField
return-object v0
"""
)
}
)
methods.add(
ImmutableMethod(
type,
"patch_getResolution",
listOf(),
"I",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
val resolutionField = fields.single { field ->
field.type == "I"
}
addInstructions(
0,
"""
iget v0, p0, $resolutionField
return v0
"""
)
}
)
}
}
// Detect video quality changes and override the current quality.
setVideoQualityFingerprint.match(
videoQualitySetterFingerprint.originalClassDef
).let { match ->
// This instruction refers to the field with the type that contains the setQuality method.
val onItemClickListenerClassReference = match.method
.getInstruction<ReferenceInstruction>(0).reference
val setQualityFieldReference = match.method
.getInstruction<ReferenceInstruction>(1).reference as FieldReference
proxy(
classes.find { classDef ->
classDef.type == setQualityFieldReference.type
}!!
).mutableClass.apply {
// Add interface and helper methods to allow extension code to call obfuscated methods.
interfaces.add(EXTENSION_VIDEO_QUALITY_MENU_INTERFACE)
methods.add(
ImmutableMethod(
type,
"patch_setQuality",
listOf(
ImmutableMethodParameter(YOUTUBE_VIDEO_QUALITY_CLASS_TYPE, null, null)
),
"V",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
val setQualityMenuIndexMethod = methods.single { method ->
method.parameterTypes.firstOrNull() == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
}
addInstructions(
0,
"""
invoke-virtual { p0, p1 }, $setQualityMenuIndexMethod
return-void
"""
)
}
)
}
videoQualitySetterFingerprint.method.addInstructions(
0,
"""
# Get object instance to invoke setQuality method.
iget-object v0, p0, $onItemClickListenerClassReference
iget-object v0, v0, $setQualityFieldReference
invoke-static { p1, v0, p2 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([$YOUTUBE_VIDEO_QUALITY_CLASS_TYPE${EXTENSION_VIDEO_QUALITY_MENU_INTERFACE}I)I
move-result p2
"""
)
}
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "initialize")
videoSpeedChangedHook(EXTENSION_CLASS_DESCRIPTOR, "videoSpeedChanged")
userSelectedPlaybackSpeedHook(EXTENSION_CLASS_DESCRIPTOR, "userSelectedPlaybackSpeed")
}
@@ -282,8 +405,8 @@ private fun addSeekInterfaceMethods(targetClass: MutableClass, seekToMethod: Met
targetClass.interfaces.add(EXTENSION_PLAYER_INTERFACE)
arrayOf(
Triple(seekToMethod, "seekTo", true),
Triple(seekToRelativeMethod, "seekToRelative", false),
Triple(seekToMethod, "patch_seekTo", true),
Triple(seekToRelativeMethod, "patch_seekToRelative", false),
).forEach { (method, name, returnsBoolean) ->
// Add interface method.
// Get enum type for the seek helper method.

View File

@@ -5,34 +5,6 @@ import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
internal const val YOUTUBE_VIDEO_QUALITY_CLASS_TYPE = "Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;"
internal val videoQualityFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
parameters(
"I", // Resolution.
"Ljava/lang/String;", // Human readable resolution: "480p", "1080p Premium", etc
"Z",
"L"
)
custom { _, classDef ->
classDef.type == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
}
}
/**
* Matches with the class found in [videoQualitySetterFingerprint].
*/
internal val setVideoQualityFingerprint = fingerprint {
returns("V")
parameters("L")
opcodes(
Opcode.IGET_OBJECT,
Opcode.IPUT_OBJECT,
Opcode.IGET_OBJECT,
)
}
internal val videoQualityItemOnClickParentFingerprint = fingerprint {
returns("V")
strings("VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT")
@@ -54,19 +26,6 @@ internal val videoQualityItemOnClickFingerprint = fingerprint {
}
}
internal val videoQualitySetterFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
parameters("[L", "I", "Z")
opcodes(
Opcode.IF_EQZ,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.IPUT_BOOLEAN,
)
strings("menu_item_video_quality")
}
internal val videoQualityMenuOptionsFingerprint = fingerprint {
accessFlags(AccessFlags.STATIC)

View File

@@ -1,10 +1,8 @@
package app.revanced.patches.youtube.video.quality
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.ListPreference
@@ -15,18 +13,10 @@ import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.patches.youtube.shared.videoQualityChangedFingerprint
import app.revanced.patches.youtube.video.information.onCreateHook
import app.revanced.patches.youtube.video.information.videoInformationPatch
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch;"
private const val EXTENSION_VIDEO_QUALITY_MENU_INTERFACE =
"Lapp/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch\$VideoQualityMenuInterface;"
val rememberVideoQualityPatch = bytecodePatch {
dependsOn(
@@ -69,131 +59,6 @@ val rememberVideoQualityPatch = bytecodePatch {
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "newVideoStarted")
videoQualityFingerprint.let {
// Fix bad data used by YouTube.
it.method.addInstructions(
0,
"""
invoke-static { p2, p1 }, $EXTENSION_CLASS_DESCRIPTOR->fixVideoQualityResolution(Ljava/lang/String;I)I
move-result p1
"""
)
// Add methods to access obfuscated quality fields.
it.classDef.apply {
methods.add(
ImmutableMethod(
type,
"patch_getQualityName",
listOf(),
"Ljava/lang/String;",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
// Only one string field.
val qualityNameField = fields.single { field ->
field.type == "Ljava/lang/String;"
}
addInstructions(
0,
"""
iget-object v0, p0, $qualityNameField
return-object v0
"""
)
}
)
methods.add(
ImmutableMethod(
type,
"patch_getResolution",
listOf(),
"I",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
val resolutionField = fields.single { field ->
field.type == "I"
}
addInstructions(
0,
"""
iget v0, p0, $resolutionField
return v0
"""
)
}
)
}
}
// Inject a call to set the remembered quality once a video loads.
setVideoQualityFingerprint.match(
videoQualitySetterFingerprint.originalClassDef
).let { match ->
// 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
val setQualityFieldReference =
((instructions.elementAt(1) as ReferenceInstruction).reference) as FieldReference
proxy(
classes.find { classDef ->
classDef.type == setQualityFieldReference.type
}!!
).mutableClass.apply {
// Add interface and helper methods to allow extension code to call obfuscated methods.
interfaces.add(EXTENSION_VIDEO_QUALITY_MENU_INTERFACE)
methods.add(
ImmutableMethod(
type,
"patch_setQuality",
listOf(
ImmutableMethodParameter(YOUTUBE_VIDEO_QUALITY_CLASS_TYPE, null, null)
),
"V",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
val setQualityMenuIndexMethod = methods.single { method ->
method.parameterTypes.firstOrNull() == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
}
addInstructions(
0,
"""
invoke-virtual { p0, p1 }, $setQualityMenuIndexMethod
return-void
"""
)
}
)
}
videoQualitySetterFingerprint.method.addInstructions(
0,
"""
# Get object instance to invoke setQuality method.
iget-object v0, p0, $onItemClickListenerClassReference
iget-object v0, v0, $setQualityFieldReference
invoke-static { p1, v0, p2 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([$YOUTUBE_VIDEO_QUALITY_CLASS_TYPE${EXTENSION_VIDEO_QUALITY_MENU_INTERFACE}I)I
move-result p2
"""
)
}
// Inject a call to remember the selected quality for Shorts.
videoQualityItemOnClickFingerprint.match(
videoQualityItemOnClickParentFingerprint.classDef

View File

@@ -1,4 +1,4 @@
package app.revanced.patches.youtube.video.quality.button
package app.revanced.patches.youtube.video.quality
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
@@ -9,7 +9,6 @@ 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
@@ -21,14 +20,7 @@ private val videoQualityButtonResourcePatch = resourcePatch {
"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",
"revanced_video_quality_dialog_button_rectangle.xml",
),
)
@@ -39,7 +31,7 @@ private val videoQualityButtonResourcePatch = resourcePatch {
private const val QUALITY_BUTTON_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/videoplayer/VideoQualityDialogButton;"
val videoQualityButtonPatch = bytecodePatch(
val videoQualityDialogButtonPatch = bytecodePatch(
description = "Adds the option to display video quality dialog button in the video player.",
) {
dependsOn(
@@ -52,7 +44,7 @@ val videoQualityButtonPatch = bytecodePatch(
)
execute {
addResources("youtube", "video.quality.button.videoQualityButtonPatch")
addResources("youtube", "video.quality.button.videoQualityDialogButtonPatch")
PreferenceScreen.PLAYER.addPreferences(
SwitchPreference("revanced_video_quality_dialog_button"),

View File

@@ -5,7 +5,6 @@ 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.
@@ -20,7 +19,7 @@ val videoQualityPatch = bytecodePatch(
dependsOn(
rememberVideoQualityPatch,
advancedVideoQualityMenuPatch,
videoQualityButtonPatch,
videoQualityDialogButtonPatch,
)
compatibleWith(

View File

@@ -9,6 +9,9 @@ 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.information.userSelectedPlaybackSpeedHook
import app.revanced.patches.youtube.video.information.videoInformationPatch
import app.revanced.patches.youtube.video.information.videoSpeedChangedHook
import app.revanced.patches.youtube.video.speed.custom.customPlaybackSpeedPatch
import app.revanced.util.ResourceGroup
import app.revanced.util.copyResources
@@ -21,8 +24,8 @@ private val playbackSpeedButtonResourcePatch = resourcePatch {
"speedbutton",
ResourceGroup(
"drawable",
"revanced_playback_speed_dialog_button.xml",
),
"revanced_playback_speed_dialog_button_rectangle.xml"
)
)
addBottomControl("speedbutton")
@@ -42,6 +45,7 @@ val playbackSpeedButtonPatch = bytecodePatch(
customPlaybackSpeedPatch,
playbackSpeedButtonResourcePatch,
playerControlsPatch,
videoInformationPatch,
)
execute {
@@ -53,5 +57,8 @@ val playbackSpeedButtonPatch = bytecodePatch(
initializeBottomControl(SPEED_BUTTON_CLASS_DESCRIPTOR)
injectVisibilityCheckCall(SPEED_BUTTON_CLASS_DESCRIPTOR)
videoSpeedChangedHook(SPEED_BUTTON_CLASS_DESCRIPTOR, "videoSpeedChanged")
userSelectedPlaybackSpeedHook(SPEED_BUTTON_CLASS_DESCRIPTOR, "videoSpeedChanged")
}
}

View File

@@ -1543,7 +1543,7 @@ 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">
<patch id="video.quality.button.videoQualityDialogButtonPatch">
<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>

View File

@@ -1,9 +0,0 @@
<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

@@ -1,9 +0,0 @@
<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

@@ -1,9 +0,0 @@
<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

@@ -1,9 +0,0 @@
<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

@@ -1,9 +0,0 @@
<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

@@ -1,9 +0,0 @@
<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="M2.61759,5 C2.15666,5,1.76994,5.15234,1.46134,5.46094 C1.15275,5.76953,1.00041,6.15625,1.00041,6.61719 L1.00041,17.3828 C1.00041,17.8438,1.15275,18.2305,1.46134,18.5391 C1.76994,18.8477,2.15666,19,2.61759,19 L21.3824,19 C21.8433,19,22.2301,18.8477,22.5387,18.5391 C22.8473,18.2305,22.9996,17.8438,22.9996,17.3829 L22.9996,6.61719 C22.9996,6.15625,22.8473,5.76953,22.5387,5.46094 C22.2301,5.15234,21.8433,5,21.3824,5 Z M2.00041,6 L2.00041,6.61719 C2.00041,6.4375,2.059,6.28906,2.17228,6.17188 C2.28947,6.05859,2.43791,6,2.61759,6 L21.3824,6 C21.5621,6,21.7105,6.05859,21.8277,6.17188 C21.941,6.28907,21.9996,6.4375,21.9996,6.61719 L21.9996,17.3828 C21.9996,17.5625,21.941,17.7109,21.8277,17.8281 C21.7105,17.9414,21.5621,18,21.3824,18 L2.61759,18 C2.43791,18,2.28947,17.9414,2.17228,17.8281 C2.059,17.7109,2.00041,17.3828,2.00041,17.3828 L2.00041,17.3828 L2.00041,6.6172 Z" />
</vector>

View File

@@ -1,9 +0,0 @@
<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

@@ -1,11 +0,0 @@
<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

@@ -6,22 +6,38 @@
android:layout_height="wrap_content"
android:layoutDirection="ltr">
<FrameLayout
android:id="@+id/revanced_video_quality_dialog_button_container"
android:layout_width="48.0dip"
android:layout_height="60.0dip"
yt:layout_constraintBottom_toTopOf="@+id/quick_actions_container"
yt:layout_constraintRight_toLeftOf="@+id/fullscreen_button">
<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:layout_width="match_parent"
android:layout_height="match_parent"
android:longClickable="false"
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" />
android:src="@drawable/revanced_video_quality_dialog_button_rectangle" />
<TextView
android:id="@+id/revanced_video_quality_dialog_button_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-condensed"
android:paddingTop="5.5dp"
android:textColor="@android:color/white"
android:textSize="10dp" />
<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:visibility="gone" />
</FrameLayout>
</android.support.constraint.ConstraintLayout>

View File

@@ -1,28 +0,0 @@
<!--
https://github.com/google/material-design-icons/blob/9beae745bb758f3ad56654fb377ea5cf62be4915/symbols/android/slow_motion_video/materialsymbolsoutlined/slow_motion_video_wght200gradN25_24px.xml
Changes made: Icon has been resized.
Copyright 2022 Google
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<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="M 6.0798492,16.70271 Q 5.3667836,15.77782 4.9972798,14.87317 4.6275665,13.96852 4.5,12.83942 h 0.7949215 q 0.1275665,0.9249 0.4677271,1.7656 0.3401706,0.8407 0.8797497,1.53346 z M 4.5,11.16058 Q 4.6373409,10.04469 5.002167,9.14154 5.3667836,8.2386 6.0798492,7.3137 L 6.6423983,7.87793 Q 6.1028192,8.5707 5.7626486,9.40308 5.422488,10.23568 5.2949215,11.16058 Z M 11.110466,19.5 Q 9.8992727,19.32286 9.0431213,18.95346 8.1871793,18.58406 7.2572507,17.89299 l 0.5627586,-0.57382 q 0.6818173,0.52458 1.5126748,0.89527 0.8308479,0.37047 1.7777819,0.49836 z M 7.8625281,6.66442 7.2899951,6.09059 Q 8.2197042,5.39953 9.0758557,5.03823 9.9320071,4.67714 11.152995,4.5 V 5.2872 Q 10.19861,5.41509 9.3677624,5.77746 8.5371243,6.13983 7.8625281,6.66442 Z M 10.32873,14.97101 V 9.02899 L 14.953488,12 Z M 12.827456,19.5 v -0.7872 q 2.53571,-0.36237 4.211668,-2.26097 1.675948,-1.89882 1.675948,-4.45183 0,-2.55301 -1.675948,-4.45183 Q 15.363166,5.64957 12.827456,5.2872 V 4.5 Q 15.70589,4.80844 17.60294,6.95069 19.5,9.09294 19.5,12 q 0,2.8964 -1.89706,5.04398 -1.89705,2.14758 -4.775484,2.45602 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="M2.61759,5 C2.15666,5,1.76994,5.15234,1.46134,5.46094 C1.15275,5.76953,1.00041,6.15625,1.00041,6.61719 L1.00041,17.3828 C1.00041,17.8438,1.15275,18.2305,1.46134,18.5391 C1.76994,18.8477,2.15666,19,2.61759,19 L21.3824,19 C21.8433,19,22.2301,18.8477,22.5387,18.5391 C22.8473,18.2305,22.9996,17.8438,22.9996,17.3829 L22.9996,6.61719 C22.9996,6.15625,22.8473,5.76953,22.5387,5.46094 C22.2301,5.15234,21.8433,5,21.3824,5 Z M2.00041,6 L2.00041,6.61719 C2.00041,6.4375,2.059,6.28906,2.17228,6.17188 C2.28947,6.05859,2.43791,6,2.61759,6 L21.3824,6 C21.5621,6,21.7105,6.05859,21.8277,6.17188 C21.941,6.28907,21.9996,6.4375,21.9996,6.61719 L21.9996,17.3828 C21.9996,17.5625,21.941,17.7109,21.8277,17.8281 C21.7105,17.9414,21.5621,18,21.3824,18 L2.61759,18 C2.43791,18,2.28947,17.9414,2.17228,17.8281 C2.059,17.7109,2.00041,17.3828,2.00041,17.3828 L2.00041,17.3828 L2.00041,6.6172 Z" />
</vector>

View File

@@ -6,23 +6,38 @@
android:layout_height="wrap_content"
android:layoutDirection="ltr">
<FrameLayout
android:id="@+id/revanced_playback_speed_dialog_button_container"
android:layout_width="48.0dip"
android:layout_height="60.0dip"
yt:layout_constraintBottom_toTopOf="@+id/quick_actions_container"
yt:layout_constraintRight_toLeftOf="@+id/fullscreen_button">
<com.google.android.libraries.youtube.common.ui.TouchImageView
android:id="@+id/revanced_playback_speed_dialog_button"
style="@style/YouTubePlayerButton"
android:layout_width="48.0dip"
android:layout_height="60.0dip"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:longClickable="false"
android:paddingTop="6.0dp"
android:paddingBottom="0dp"
android:scaleType="center"
android:src="@drawable/revanced_playback_speed_dialog_button"
yt:layout_constraintBottom_toTopOf="@+id/quick_actions_container"
yt:layout_constraintRight_toLeftOf="@+id/fullscreen_button" />
android:src="@drawable/revanced_playback_speed_dialog_button_rectangle" />
<TextView
android:id="@+id/revanced_playback_speed_dialog_button_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:fontFamily="sans-serif-condensed"
android:paddingTop="5.5dp"
android:textColor="@android:color/white"
android:textSize="10dp" />
<View
android:id="@+id/revanced_playback_speed_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:visibility="gone" />
</FrameLayout>
</android.support.constraint.ConstraintLayout>