diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java index 82e75058b3..33725653c8 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java @@ -1,14 +1,29 @@ 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 static app.revanced.extension.shared.Utils.dipToPixels; +import static app.revanced.extension.shared.Utils.showToastShort; +import android.app.Dialog; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.RoundRectShape; +import android.util.DisplayMetrics; +import android.util.TypedValue; +import android.view.*; +import android.view.animation.Animation; +import android.view.animation.TranslateAnimation; +import android.widget.*; + +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import app.revanced.extension.shared.Logger; import app.revanced.extension.shared.Utils; @@ -17,6 +32,7 @@ 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 { @@ -45,6 +61,38 @@ public class RememberVideoQualityPatch { @Nullable private static List videoQualities; + /** + * Mapping of filtered quality indices (used in dialog) to original quality indices. + */ + @Nullable + private static Map filteredToOriginalIndexMap; + + /** + * Quality interface and method for setting quality. + */ + private static Object qInterface; + private static String qIndexMethod; + + /** + * Tracks the last applied quality index. + */ + private static int lastAppliedQualityIndex = -1; // Initialize to -1 (invalid index) + + /** + * Getter for lastAppliedQualityIndex. + */ + public static int getLastAppliedQualityIndex() { + return lastAppliedQualityIndex; + } + + /** + * Getter for videoQualities. + */ + @Nullable + public static List getVideoQualities() { + return videoQualities; + } + private static boolean shouldRememberVideoQuality() { BooleanSetting preference = ShortsPlayerState.isOpen() ? Settings.REMEMBER_SHORTS_QUALITY_LAST_SELECTED @@ -55,7 +103,7 @@ private static boolean shouldRememberVideoQuality() { private static void changeDefaultQuality(int defaultQuality) { String networkTypeMessage; boolean useShortsPreference = ShortsPlayerState.isOpen(); - if (Utils.getNetworkType() == NetworkType.MOBILE) { + if (Utils.getNetworkType() == Utils.NetworkType.MOBILE) { if (useShortsPreference) shortsQualityMobile.save(defaultQuality); else videoQualityMobile.save(defaultQuality); networkTypeMessage = str("revanced_remember_video_quality_mobile"); @@ -65,7 +113,7 @@ private static void changeDefaultQuality(int defaultQuality) { networkTypeMessage = str("revanced_remember_video_quality_wifi"); } if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) - Utils.showToastShort(str( + showToastShort(str( useShortsPreference ? "revanced_remember_video_quality_toast_shorts" : "revanced_remember_video_quality_toast", networkTypeMessage, (defaultQuality + "p") )); @@ -77,32 +125,49 @@ private static void changeDefaultQuality(int defaultQuality) { * @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(Object[] qualities, final int originalQualityIndex, Object qInterface, String qIndexMethod) { + public static int setVideoQuality(Object[] qualities, final int originalQualityIndex, + Object qInterfaceArg, String qIndexMethodArg) { try { + // Store qInterface and qIndexMethod for use in dialog. + qInterface = qInterfaceArg; + qIndexMethod = qIndexMethodArg; + boolean useShortsPreference = ShortsPlayerState.isOpen(); - final int preferredQuality = Utils.getNetworkType() == NetworkType.MOBILE + final int preferredQuality = Utils.getNetworkType() == Utils.NetworkType.MOBILE ? (useShortsPreference ? shortsQualityMobile : videoQualityMobile).get() : (useShortsPreference ? shortsQualityWifi : videoQualityWifi).get(); if (!userChangedDefaultQuality && preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) { + lastAppliedQualityIndex = originalQualityIndex; return originalQualityIndex; // Nothing to do. } if (videoQualities == null || videoQualities.size() != qualities.length) { videoQualities = new ArrayList<>(qualities.length); - for (Object streamQuality : qualities) { + filteredToOriginalIndexMap = new HashMap<>(); + Set seenQualities = new LinkedHashSet<>(); // Maintains insertion order. + int filteredIndex = 0; + + for (int i = 0; i < qualities.length; i++) { + Object streamQuality = qualities[i]; for (Field field : streamQuality.getClass().getFields()) { if (field.getType().isAssignableFrom(Integer.TYPE) && field.getName().length() <= 2) { - videoQualities.add(field.getInt(streamQuality)); + int quality = field.getInt(streamQuality); + if (quality == AUTOMATIC_VIDEO_QUALITY_VALUE || quality > 0) { + videoQualities.add(quality); + if (seenQualities.add(quality)) { + filteredToOriginalIndexMap.put(filteredIndex++, i); + } + } } } } - + // 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); + Logger.printDebug(() -> "VideoQualities: " + videoQualities + ", IndexMap: " + filteredToOriginalIndexMap); } if (userChangedDefaultQuality) { @@ -110,10 +175,12 @@ public static int setVideoQuality(Object[] qualities, final int originalQualityI final int quality = videoQualities.get(userSelectedQualityIndex); Logger.printDebug(() -> "User changed default quality to: " + quality); changeDefaultQuality(quality); + lastAppliedQualityIndex = userSelectedQualityIndex; return userSelectedQualityIndex; } if (!qualityNeedsUpdating) { + lastAppliedQualityIndex = originalQualityIndex; return originalQualityIndex; } qualityNeedsUpdating = false; @@ -123,7 +190,7 @@ public static int setVideoQuality(Object[] qualities, final int originalQualityI int qualityIndexToUse = 0; int i = 0; for (Integer quality : videoQualities) { - if (quality <= preferredQuality && qualityToUse < quality) { + if (quality <= preferredQuality && qualityToUse < quality) { qualityToUse = quality; qualityIndexToUse = i; } @@ -148,9 +215,12 @@ public static int setVideoQuality(Object[] qualities, final int originalQualityI Method m = qInterface.getClass().getMethod(qIndexMethod, Integer.TYPE); m.invoke(qInterface, qualityToUse); + lastAppliedQualityIndex = qualityIndexToUse; + VideoQualityDialogButton.updateButtonIcon(); return qualityIndexToUse; } catch (Exception ex) { Logger.printException(() -> "Failed to set quality", ex); + lastAppliedQualityIndex = originalQualityIndex; // Fallback to original index return originalQualityIndex; } } @@ -162,6 +232,7 @@ public static void userChangedQuality(int selectedQualityIndex) { if (shouldRememberVideoQuality()) { userSelectedQualityIndex = selectedQualityIndex; userChangedDefaultQuality = true; + VideoQualityDialogButton.updateButtonIcon(); } } @@ -172,6 +243,7 @@ public static void userChangedQualityInNewFlyout(int selectedQuality) { if (!shouldRememberVideoQuality()) return; changeDefaultQuality(selectedQuality); // Quality is human readable resolution (ie: 1080). + VideoQualityDialogButton.updateButtonIcon(); } /** @@ -181,5 +253,281 @@ public static void newVideoStarted(VideoInformation.PlaybackController ignoredPl Logger.printDebug(() -> "newVideoStarted"); qualityNeedsUpdating = true; videoQualities = null; + filteredToOriginalIndexMap = null; + lastAppliedQualityIndex = -1; // Reset on new video. + } + + /** + * Shows a dialog with available video qualities, excluding duplicates. + */ + public static void showVideoQualityDialog(@NonNull Context context) { + try { + if (videoQualities == null || videoQualities.isEmpty()) { + showToastShort(str("revanced_video_quality_no_qualities_available")); + return; + } + + // Create dialog without a theme for custom appearance. + Dialog dialog = new Dialog(context); + dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + dialog.setCanceledOnTouchOutside(true); + dialog.setCancelable(true); + + // Preset size constants. + 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 dip20 = dipToPixels(20); // Margin below handle. + final int dip40 = dipToPixels(40); // Width for handle bar. + + // Create main vertical LinearLayout. + LinearLayout mainLayout = new LinearLayout(context); + mainLayout.setOrientation(LinearLayout.VERTICAL); + mainLayout.setPadding(dip5, dip8, dip5, dip8); + + // Set rounded rectangle background. + ShapeDrawable background = new ShapeDrawable(new RoundRectShape( + Utils.createCornerRadii(12), null, null)); + background.getPaint().setColor(Utils.getDialogBackgroundColor()); + mainLayout.setBackground(background); + + // Add handle bar at the top. + View handleBar = new View(context); + ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape( + Utils.createCornerRadii(4), null, null)); + handleBackground.getPaint().setColor(getAdjustedHandleBarBackgroundColor()); + 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); + + // Prepare dialog items, removing duplicates. + List qualityLabels = new ArrayList<>(); + List filteredQualities = new ArrayList<>(); + Set seenQualities = new LinkedHashSet<>(); + int filteredIndex = 0; + for (Integer quality : videoQualities) { + if (quality != AUTOMATIC_VIDEO_QUALITY_VALUE && seenQualities.add(quality)) { + String label = quality + "p"; + qualityLabels.add(label); + filteredQualities.add(quality); + filteredToOriginalIndexMap.put(filteredIndex++, videoQualities.indexOf(quality)); + } + } + + // Determine pre-selected quality index. + int selectedIndex; + if (lastAppliedQualityIndex >= 0 && lastAppliedQualityIndex < videoQualities.size()) { + int originalQuality = videoQualities.get(lastAppliedQualityIndex); + selectedIndex = filteredQualities.indexOf(originalQuality); + } else { + int preferredQuality = Utils.getNetworkType() == Utils.NetworkType.MOBILE + ? videoQualityMobile.get() + : videoQualityWifi.get(); + selectedIndex = filteredQualities.indexOf(preferredQuality); + if (selectedIndex < 0) selectedIndex = 0; + } + + // Create ListView for quality options. + ListView listView = new ListView(context); + CustomQualityAdapter adapter = new CustomQualityAdapter(context, qualityLabels); + adapter.setSelectedPosition(selectedIndex); + listView.setAdapter(adapter); + listView.setDivider(null); + listView.setPadding(0, 0, 0, 0); + + // Handle item click. + listView.setOnItemClickListener((parent, view, which, id) -> { + try { + int selectedQuality = filteredQualities.get(which); + int originalIndex = filteredToOriginalIndexMap.get(filteredQualities.indexOf(selectedQuality)); + if (qInterface != null && qIndexMethod != null) { + Method m = qInterface.getClass().getMethod(qIndexMethod, Integer.TYPE); + m.invoke(qInterface, selectedQuality); + lastAppliedQualityIndex = originalIndex; + VideoQualityDialogButton.updateButtonIcon(); + Logger.printDebug(() -> "Applied dialog quality: " + selectedQuality + " (original index: " + originalIndex + ")"); + } else { + Logger.printDebug(() -> "Cannot apply quality: qInterface or qIndexMethod is null"); + showToastShort(str("revanced_video_quality_apply_failed")); + } + + // Update saved setting if remembrance is enabled. + if (shouldRememberVideoQuality()) { + changeDefaultQuality(selectedQuality); + } + + showToastShort(str("revanced_video_quality_selected_toast", qualityLabels.get(which))); + dialog.dismiss(); + } catch (Exception ex) { + Logger.printException(() -> "Video quality selection failure", ex); + showToastShort(str("revanced_video_quality_apply_failed")); + } + }); + + // Add ListView to main layout. + 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); + + // Wrap mainLayout in another LinearLayout for side margins. + LinearLayout wrapperLayout = new LinearLayout(context); + wrapperLayout.setOrientation(LinearLayout.VERTICAL); + wrapperLayout.setPadding(dip8, 0, dip8, 0); + wrapperLayout.addView(mainLayout); + dialog.setContentView(wrapperLayout); + + // Configure dialog window. + 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); + } + + // Apply slide-in animation. + final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast"); + Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom"); + slideInABottomAnimation.setDuration(fadeDurationFast); + mainLayout.startAnimation(slideInABottomAnimation); + + // Set touch listener for drag-to-dismiss. + mainLayout.setOnTouchListener(new View.OnTouchListener() { + final float dismissThreshold = Utils.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) { + 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); + } + } + + public static class CustomQualityAdapter extends ArrayAdapter { + private int selectedPosition = -1; + + public CustomQualityAdapter(@NonNull Context context, @NonNull List objects) { + super(context, 0, objects); + } + + public 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(); + } + + // Set text. + viewHolder.textView.setText(getItem(position)); + + // Show check icon for selected item. + 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; + } + } + + /** + * Adjusts the HandleBar background color based on the current theme. + */ + public static int getAdjustedHandleBarBackgroundColor() { + final int baseColor = Utils.getDialogBackgroundColor(); + return Utils.isDarkModeEnabled() + ? Utils.adjustColorBrightness(baseColor, 1.25f) // Lighten for dark theme. + : Utils.adjustColorBrightness(baseColor, 0.9f); // Darken for light theme. } } diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java index 564dd5f31f..d3d5b1b8f4 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -171,6 +171,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_VIDEO_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE); public static final BooleanSetting OPEN_VIDEOS_FULLSCREEN_PORTRAIT = new BooleanSetting("revanced_open_videos_fullscreen_portrait", FALSE); public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE); + public static final BooleanSetting VIDEO_QUALITY_DIALOG_BUTTON = new BooleanSetting("revanced_video_quality_dialog_button", FALSE); public static final IntegerSetting PLAYER_OVERLAY_OPACITY = new IntegerSetting("revanced_player_overlay_opacity", 100, true); public static final BooleanSetting PLAYER_POPUP_PANELS = new BooleanSetting("revanced_hide_player_popup_panels", FALSE); diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java index 6d3c320171..5d3ae3a543 100644 --- a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java @@ -187,4 +187,25 @@ public void hide() { if (view != null) view.setVisibility(View.GONE); isVisible = false; } + + /** + * Sets the icon of the button. + * @param resourceName The name of the drawable resource. + */ + public void setIcon(String resourceName) { + try { + View button = buttonRef.get(); + if (button instanceof ImageView) { + int resourceId = Utils.getResourceIdentifier(resourceName, "drawable"); + if (resourceId != 0) { + ((ImageView) button).setImageResource(resourceId); + Logger.printDebug(() -> "Set button icon to: " + resourceName); + } else { + Logger.printDebug(() -> "Resource not found: " + resourceName); + } + } + } catch (Exception ex) { + Logger.printException(() -> "setIcon failure", ex); + } + } } \ No newline at end of file diff --git a/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java new file mode 100644 index 0000000000..13d49f5804 --- /dev/null +++ b/extensions/youtube/src/main/java/app/revanced/extension/youtube/videoplayer/VideoQualityDialogButton.java @@ -0,0 +1,113 @@ +package app.revanced.extension.youtube.videoplayer; + +import android.view.View; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch; +import app.revanced.extension.youtube.settings.Settings; + +import java.util.List; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.showToastShort; + +@SuppressWarnings("unused") +public class VideoQualityDialogButton { + @Nullable + private static PlayerControlButton instance; + + /** + * Updates the button icon based on the current video quality. + */ + public static void updateButtonIcon() { + if (instance == null) return; + + try { + // Get the current quality. + int lastAppliedQualityIndex = RememberVideoQualityPatch.getLastAppliedQualityIndex(); + List videoQualities = RememberVideoQualityPatch.getVideoQualities(); + + if (videoQualities == null || lastAppliedQualityIndex < 0 || lastAppliedQualityIndex >= videoQualities.size()) { + // Default to a generic icon or "Auto". + instance.setIcon("revanced_video_quality_dialog_button"); + return; + } + + int quality = videoQualities.get(lastAppliedQualityIndex); + String iconResource = switch (quality) { + case 144, 240, 360, 480 -> "revanced_video_quality_dialog_button_lhd"; + case 720 -> "revanced_video_quality_dialog_button_hd"; + case 1080 -> "revanced_video_quality_dialog_button_fhd"; + case 1440 -> "revanced_video_quality_dialog_button_2k"; + case 2160 -> "revanced_video_quality_dialog_button_4k"; + default -> "revanced_video_quality_dialog_button"; + }; + + instance.setIcon(iconResource); + Logger.printDebug(() -> "Updated button icon to: " + iconResource); + } catch (Exception ex) { + Logger.printException(() -> "Failed to update button icon", 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 { + RememberVideoQualityPatch.showVideoQualityDialog(view.getContext()); + updateButtonIcon(); // Update icon after dialog interaction + } catch (Exception ex) { + Logger.printException(() -> "Video quality button onClick failure", ex); + } + }, + view -> { + try { + // Reset to automatic quality. + final int autoQuality = -2; // Auto. + RememberVideoQualityPatch.userChangedQualityInNewFlyout(autoQuality); + updateButtonIcon(); // Update icon after reset. + showToastShort(str("revanced_video_quality_reset_toast")); + return true; + } catch (Exception ex) { + Logger.printException(() -> "Video quality button reset failure", ex); + return false; + } + } + ); + updateButtonIcon(); // Set initial icon. + } catch (Exception ex) { + Logger.printException(() -> "initializeButton failure", ex); + } + } + + /** + * Injection point. + */ + public static void setVisibilityImmediate(boolean visible) { + if (instance != null) { + instance.setVisibilityImmediate(visible); + if (visible) updateButtonIcon(); + } + } + + /** + * Injection point. + */ + public static void setVisibility(boolean visible, boolean animated) { + if (instance != null) { + instance.setVisibility(visible, animated); + if (visible) updateButtonIcon(); + } + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt index 2fb92c848c..432b4f8f39 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/VideoQualityPatch.kt @@ -5,6 +5,7 @@ import app.revanced.patches.shared.misc.settings.preference.BasePreference import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.video.quality.button.videoQualityButtonPatch /** * Video quality settings. Used to organize all speed related settings together. @@ -19,6 +20,7 @@ val videoQualityPatch = bytecodePatch( dependsOn( rememberVideoQualityPatch, advancedVideoQualityMenuPatch, + videoQualityButtonPatch, ) compatibleWith( diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatch.kt new file mode 100644 index 0000000000..1a9bc25904 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/button/VideoQualityDialogButtonPatch.kt @@ -0,0 +1,62 @@ +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.xml", + "revanced_video_quality_dialog_button_4k.xml", + "revanced_video_quality_dialog_button_fhd.xml", + "revanced_video_quality_dialog_button_hd.xml", + "revanced_video_quality_dialog_button_lhd.xml", + "revanced_video_quality_dialog_button_qhd.xml", + ), + ) + + addBottomControl("qualitybutton") + } +} + +private const val QUALITY_BUTTON_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/videoplayer/VideoQualityDialogButton;" + +val videoQualityButtonPatch = bytecodePatch( + description = "Adds the option to display video quality dialog button in the video player.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + rememberVideoQualityPatch, + videoQualityButtonResourcePatch, + playerControlsPatch, + ) + + execute { + addResources("youtube", "video.quality.button.videoQualityButtonPatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_video_quality_dialog_button"), + ) + + initializeBottomControl(QUALITY_BUTTON_CLASS_DESCRIPTOR) + injectVisibilityCheckCall(QUALITY_BUTTON_CLASS_DESCRIPTOR) + } +} diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index f2e0a1ff3e..333ab5e55d 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -1538,6 +1538,14 @@ Enabling this can unlock higher video qualities" Button is shown. Tap and hold to reset playback speed to default Button is not shown + + Show video quality button + Button is shown. Tap and hold to reset video quality to Auto + Button is not shown + No quality available + Changed video quality to: %s + Reset video quality to Auto + Custom playback speed menu Custom speed menu is shown diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button.xml new file mode 100644 index 0000000000..f4be45538b --- /dev/null +++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button.xml @@ -0,0 +1,9 @@ + + + diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_4k.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_4k.xml new file mode 100644 index 0000000000..22051e3fb8 --- /dev/null +++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_4k.xml @@ -0,0 +1,9 @@ + + + diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd.xml new file mode 100644 index 0000000000..b7a8f5c73e --- /dev/null +++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_fhd.xml @@ -0,0 +1,9 @@ + + + diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_hd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_hd.xml new file mode 100644 index 0000000000..9b3fe6d097 --- /dev/null +++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_hd.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_lhd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_lhd.xml new file mode 100644 index 0000000000..400a95044d --- /dev/null +++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_lhd.xml @@ -0,0 +1,9 @@ + + + diff --git a/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_qhd.xml b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_qhd.xml new file mode 100644 index 0000000000..7609d368ec --- /dev/null +++ b/patches/src/main/resources/qualitybutton/drawable/revanced_video_quality_dialog_button_qhd.xml @@ -0,0 +1,9 @@ + + + diff --git a/patches/src/main/resources/qualitybutton/host/layout/youtube_controls_bottom_ui_container.xml b/patches/src/main/resources/qualitybutton/host/layout/youtube_controls_bottom_ui_container.xml new file mode 100644 index 0000000000..e26adf5a65 --- /dev/null +++ b/patches/src/main/resources/qualitybutton/host/layout/youtube_controls_bottom_ui_container.xml @@ -0,0 +1,29 @@ + + + + + + +