diff --git a/extensions/shared/src/main/java/app/morphe/extension/youtube/patches/components/ActionButtonsFilter.java b/extensions/shared/src/main/java/app/morphe/extension/youtube/patches/components/ActionButtonsFilter.java
index c9cf45c53..2f427a22d 100644
--- a/extensions/shared/src/main/java/app/morphe/extension/youtube/patches/components/ActionButtonsFilter.java
+++ b/extensions/shared/src/main/java/app/morphe/extension/youtube/patches/components/ActionButtonsFilter.java
@@ -1,161 +1,324 @@
+/*
+ * Copyright 2026 Morphe.
+ * https://github.com/MorpheApp/morphe-patches
+ *
+ * Original hard forked code:
+ * https://github.com/ReVanced/revanced-patches/commit/724e6d61b2ecd868c1a9a37d465a688e83a74799
+ */
+
package app.morphe.extension.youtube.patches.components;
-import static app.morphe.extension.youtube.utils.ExtendedUtils.IS_19_26_OR_GREATER;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+
+import com.google.protobuf.MessageLite;
import org.apache.commons.lang3.StringUtils;
-import app.morphe.extension.shared.patches.components.ByteArrayFilterGroup;
-import app.morphe.extension.shared.patches.components.ByteArrayFilterGroupList;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
import app.morphe.extension.shared.patches.components.Filter;
import app.morphe.extension.shared.patches.components.StringFilterGroup;
+import app.morphe.extension.shared.utils.Logger;
+import app.morphe.extension.shared.utils.Utils;
+import app.morphe.extension.youtube.innertube.NextResponseOuterClass.ActionButtons;
+import app.morphe.extension.youtube.innertube.NextResponseOuterClass.NewElement;
+import app.morphe.extension.youtube.innertube.NextResponseOuterClass.SecondaryContents;
+import app.morphe.extension.youtube.innertube.NextResponseOuterClass.SingleColumnWatchNextResults;
import app.morphe.extension.youtube.settings.Settings;
+import app.morphe.extension.youtube.shared.VideoInformation;
-@SuppressWarnings({"deprecation", "unused"})
+@SuppressWarnings("unused")
public final class ActionButtonsFilter extends Filter {
+ public enum ActionButton {
+ UNKNOWN(false),
+ ASK(
+ Settings.HIDE_ASK_BUTTON.get(),
+ "yt_fill_experimental_spark",
+ "yt_fill_spark"
+ ),
+ CHANNEL_PROFILE(false),
+ CLIP(Settings.HIDE_CLIP_BUTTON.get()),
+ COMMENTS(
+ Settings.HIDE_COMMENTS_BUTTON.get(),
+ "yt_outline_experimental_text_bubble",
+ "yt_outline_message_bubble",
+ "yt_outline_message_bubble_right"
+ ),
+ DOWNLOAD(Settings.HIDE_DOWNLOAD_BUTTON.get()),
+ HYPE(
+ Settings.HIDE_HYPE_BUTTON.get(),
+ "yt_outline_experimental_hype",
+ "yt_outline_star_shooting"
+ ),
+ LIKE_DISLIKE(Settings.HIDE_LIKE_DISLIKE_BUTTON.get()),
+ PLAYLIST(Settings.HIDE_PLAYLIST_BUTTON.get()),
+ PROMOTE(
+ Settings.HIDE_PROMOTE_BUTTON.get(),
+ "yt_outline_experimental_megaphone",
+ "yt_outline_megaphone"
+ ),
+ REMIX(
+ Settings.HIDE_REMIX_BUTTON.get(),
+ "yt_outline_youtube_shorts_plus",
+ "yt_outline_experimental_remix"
+ ),
+ REPORT(
+ Settings.HIDE_REPORT_BUTTON.get(),
+ "yt_outline_experimental_flag",
+ "yt_outline_flag"
+ ),
+ REWARDS(
+ Settings.HIDE_REWARDS_BUTTON.get(),
+ "yt_outline_experimental_account_link",
+ "yt_outline_account_link"
+ ),
+ SHARE(
+ Settings.HIDE_SHARE_BUTTON.get(),
+ "yt_outline_experimental_share",
+ "yt_outline_share"
+ ),
+ SHOP(
+ Settings.HIDE_SHOP_BUTTON.get(),
+ "yt_outline_experimental_bag",
+ "yt_outline_bag"
+ ),
+ STOP_ADS(
+ Settings.HIDE_STOP_ADS_BUTTON.get(),
+ "yt_outline_experimental_circle_slash",
+ "yt_outline_slash_circle_left"
+ ),
+ THANKS(
+ Settings.HIDE_THANKS_BUTTON.get(),
+ "yt_outline_experimental_dollar_sign_heart",
+ "yt_outline_dollar_sign_heart"
+ );
+
+ public final boolean shouldHide;
+ @NonNull
+ public final List iconNames;
+
+ ActionButton(boolean shouldHide) {
+ this.shouldHide = shouldHide;
+ this.iconNames = Collections.emptyList();
+ }
+
+ ActionButton(boolean shouldHide, @NonNull String... iconNames) {
+ this.shouldHide = shouldHide;
+ this.iconNames = Arrays.asList(iconNames);
+ }
+ }
+
/**
- * Video bar path when the video information is collapsed. Seems to shown only with 20.14+
+ * Whether to parse watch next results and remove action buttons from the tree node list.
*/
- private static final String COMPACTIFY_VIDEO_ACTION_BAR_PATH = "compactify_video_action_bar.";
- private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.";
- private static final String ANIMATED_VECTOR_TYPE_PATH = "AnimatedVectorType";
+ private static final boolean HIDE_ACTION_BUTTON;
+
+ static {
+ boolean hideActionButton = false;
+ for (ActionButton button : ActionButton.values()) {
+ if (button.shouldHide) {
+ hideActionButton = true;
+ break;
+ }
+ }
+ HIDE_ACTION_BUTTON = !Settings.HIDE_ACTION_BUTTON_INDEX.get() && hideActionButton;
+ }
+
+ /**
+ * Caches a list of action buttons based on video ID.
+ */
+ @GuardedBy("itself")
+ private static final Map> actionButtonLookup =
+ Utils.createSizeRestrictedMap(10);
+
+ private static final String COMPACT_CHANNEL_BAR_PREFIX = "compact_channel_bar.";
+ private static final String COMPACTIFY_VIDEO_ACTION_BAR_PREFIX = "compactify_video_action_bar.";
+ private static final String VIDEO_ACTION_BAR_PREFIX = "video_action_bar.";
- private final StringFilterGroup actionBarRule;
- private final StringFilterGroup bufferFilterPathRule;
private final StringFilterGroup likeSubscribeGlow;
- private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList();
-
- private static final boolean HIDE_ACTION_BUTTON_INDEX = Settings.HIDE_ACTION_BUTTON_INDEX.get();
public ActionButtonsFilter() {
- actionBarRule = new StringFilterGroup(
- null,
- VIDEO_ACTION_BAR_PATH
+ StringFilterGroup actionBarGroup = new StringFilterGroup(
+ Settings.HIDE_ACTION_BAR,
+ VIDEO_ACTION_BAR_PREFIX
);
- addIdentifierCallbacks(actionBarRule);
+ addIdentifierCallbacks(actionBarGroup);
- bufferFilterPathRule = new StringFilterGroup(
- null,
- "|ContainerType|button."
- );
likeSubscribeGlow = new StringFilterGroup(
Settings.DISABLE_LIKE_DISLIKE_GLOW,
"animated_button_border."
);
- addPathCallbacks(
- new StringFilterGroup(
- Settings.HIDE_LIKE_DISLIKE_BUTTON,
- "|segmented_like_dislike_button"
- ),
- new StringFilterGroup(
- Settings.HIDE_DOWNLOAD_BUTTON,
- "|download_button."
- ),
- new StringFilterGroup(
- Settings.HIDE_CLIP_BUTTON,
- "|clip_button."
- ),
- new StringFilterGroup(
- Settings.HIDE_PLAYLIST_BUTTON,
- "|save_to_playlist_button"
- ),
- new StringFilterGroup(
- Settings.HIDE_REWARDS_BUTTON,
- "account_link_button"
- ),
- bufferFilterPathRule,
- likeSubscribeGlow
- );
-
- bufferButtonsGroupList.addAll(
- new ByteArrayFilterGroup(
- Settings.HIDE_COMMENTS_BUTTON,
- "yt_outline_message_bubble"
- ),
- new ByteArrayFilterGroup(
- Settings.HIDE_REPORT_BUTTON,
- "yt_outline_flag"
- ),
- new ByteArrayFilterGroup(
- Settings.HIDE_SHARE_BUTTON,
- "yt_outline_share"
- ),
- new ByteArrayFilterGroup(
- Settings.HIDE_REMIX_BUTTON,
- "yt_outline_youtube_shorts_plus"
- ),
- new ByteArrayFilterGroup(
- Settings.HIDE_THANKS_BUTTON,
- "yt_outline_dollar_sign_heart"
- ),
- new ByteArrayFilterGroup(
- Settings.HIDE_ASK_BUTTON,
- "yt_fill_spark"
- ),
- new ByteArrayFilterGroup(
- Settings.HIDE_SHOP_BUTTON,
- "yt_outline_bag"
- ),
- new ByteArrayFilterGroup(
- Settings.HIDE_STOP_ADS_BUTTON,
- "yt_outline_slash_circle_left"
- ),
- new ByteArrayFilterGroup(
- Settings.HIDE_CLIP_BUTTON,
- "yt_outline_scissors"
- ),
- // 1. YouTube 19.25.39 can be used without the 'Disable update screen' patch.
- // This means that even if you use an unpatched YouTube 19.25.39, the 'Update your app' pop-up will not appear.
- // 2. Due to a server-side change, the Hype button is now available on YouTube 19.25.39 and earlier.
- // 3. Google did not add the Hype icon (R.drawable.yt_outline_star_shooting_black_24) to YouTube 19.25.39 or earlier,
- // So no icon appears on the Hype button when using YouTube 19.25.39.
- // 4. For the same reason, the 'buttonViewModel.iconName' value in the '/next' endpoint response from YouTube 19.25.39 is also empty.
- // 5. Therefore, in YouTube 19.25.39 or earlier versions, you cannot hide the Hype button with the keyword 'yt_outline_star_shooting',
- // but you can hide it with the keyword 'Hype'.
- new ByteArrayFilterGroup(
- Settings.HIDE_HYPE_BUTTON,
- IS_19_26_OR_GREATER || Settings.FIX_HYPE_BUTTON_ICON.get()
- ? "yt_outline_star_shooting"
- : "Hype"
- ),
- new ByteArrayFilterGroup(
- Settings.HIDE_PROMOTE_BUTTON,
- "yt_outline_megaphone"
- )
- );
+ addPathCallbacks(likeSubscribeGlow);
}
- private boolean isEveryFilterGroupEnabled() {
- for (StringFilterGroup group : pathCallbacks)
- if (!group.isEnabled()) return false;
+ private static boolean isVideoActionBar(@NonNull String identifier) {
+ return StringUtils.startsWithAny(identifier, COMPACTIFY_VIDEO_ACTION_BAR_PREFIX, VIDEO_ACTION_BAR_PREFIX);
+ }
- for (ByteArrayFilterGroup group : bufferButtonsGroupList)
- if (!group.isEnabled()) return false;
-
- return true;
+ private static ActionButton getActionButton(@NonNull String iconName) {
+ for (ActionButton button : ActionButton.values()) {
+ for (String icon : button.iconNames) {
+ if (iconName.contains(icon)) {
+ return button;
+ }
+ }
+ }
+ return ActionButton.UNKNOWN;
}
@Override
public boolean isFiltered(String path, String identifier, String allValue, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
- if (HIDE_ACTION_BUTTON_INDEX) {
- return false;
- }
- if (!StringUtils.startsWithAny(path, COMPACTIFY_VIDEO_ACTION_BAR_PATH, VIDEO_ACTION_BAR_PATH)) {
- return false;
- }
- if (matchedGroup == actionBarRule && !isEveryFilterGroupEnabled()) {
- return false;
- }
if (matchedGroup == likeSubscribeGlow) {
- if (!path.contains(ANIMATED_VECTOR_TYPE_PATH)) {
- return false;
- }
- }
- if (matchedGroup == bufferFilterPathRule) {
- // In case the group list has no match, return false.
- return bufferButtonsGroupList.check(buffer).isFiltered();
+ return StringUtils.startsWithAny(
+ path,
+ COMPACT_CHANNEL_BAR_PREFIX,
+ COMPACTIFY_VIDEO_ACTION_BAR_PREFIX,
+ VIDEO_ACTION_BAR_PREFIX
+ );
}
return true;
}
+
+ /**
+ * Injection point.
+ * Called after {@link #onSingleColumnWatchNextResultsLoaded(MessageLite)}.
+ */
+ public static void onLazilyConvertedElementLoaded(@NonNull List
"Action buttons are hidden by identifier filter.
Info:
-• Right action buttons are hidden.
-• Hiding action buttons leaves empty space."
+• Supported action buttons are removed without leaving empty space."
"Action buttons are hidden by index.
Info:
• Wrong action buttons may be hidden, or action buttons may not be hidden.
• Hiding action buttons leaves no empty space."
Hide action button by index
+ Action bar is shown.
+ Action bar is hidden.
+ Hide action bar
AI chat summary is shown.
AI chat summary is hidden.
Hide AI chat summary
diff --git a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
index 5c6584d1c..f7aa30385 100644
--- a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
+++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
@@ -360,6 +360,7 @@