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 treeNodeResultList, + @NonNull String identifier) { + if (!HIDE_ACTION_BUTTON || !isVideoActionBar(identifier)) { + return; + } + + synchronized (actionButtonLookup) { + String videoId = VideoInformation.getVideoId(); + List actionButtons = actionButtonLookup.get(videoId); + if (actionButtons == null) { + return; + } + + int actionButtonSize = actionButtons.size(); + int treeNodeResultListSize = treeNodeResultList.size(); + if (actionButtonSize != treeNodeResultListSize) { + Logger.printDebug(() -> "The sizes of the lists do not match, actionButtonSize: " + + actionButtonSize + ", treeNodeResultListSize: " + treeNodeResultListSize); + return; + } + + for (int i = actionButtonSize - 1; i > -1; i--) { + ActionButton actionButton = actionButtons.get(i); + if (actionButton.shouldHide && i < treeNodeResultListSize) { + treeNodeResultList.remove(i); + } + } + } + } + + /** + * Injection point. + * Invoke as soon as the endpoint response is received. + */ + public static void onSingleColumnWatchNextResultsLoaded(@NonNull MessageLite messageLite) { + if (!HIDE_ACTION_BUTTON) { + return; + } + + synchronized (actionButtonLookup) { + try { + var singleColumnWatchNextResults = SingleColumnWatchNextResults.parseFrom(messageLite.toByteArray()); + var primaryResults = singleColumnWatchNextResults.getPrimaryResults(); + var secondaryResults = primaryResults.getSecondaryResults(); + + SecondaryContents finalSecondaryContents = null; + for (var secondaryContents : secondaryResults.getSecondaryContentsList()) { + if (secondaryContents.hasSlimVideoMetadataSectionRenderer()) { + finalSecondaryContents = secondaryContents; + break; + } + } + if (finalSecondaryContents == null) { + return; + } + + var slimVideoMetadataSectionRenderer = finalSecondaryContents.getSlimVideoMetadataSectionRenderer(); + String videoId = slimVideoMetadataSectionRenderer.getVideoId(); + if (actionButtonLookup.containsKey(videoId)) { + return; + } + + NewElement finalNewElement = null; + for (var tertiaryContents : slimVideoMetadataSectionRenderer.getTertiaryContentsList()) { + var newElement = tertiaryContents.getElementRenderer().getNewElement(); + String identifier = newElement.getProperties().getIdentifierProperties().getIdentifier(); + if (isVideoActionBar(identifier)) { + finalNewElement = newElement; + break; + } + } + if (finalNewElement == null) { + return; + } + + var model = finalNewElement.getType().getComponentType().getModel(); + List finalActionButtons = null; + + if (model.hasYoutubeModel()) { + finalActionButtons = model.getYoutubeModel() + .getViewModel() + .getCompactifyVideoActionBarViewModel() + .getActionButtonsList(); + } else if (model.hasVideoActionBarModel()) { + finalActionButtons = model.getVideoActionBarModel() + .getVideoActionBarData() + .getActionButtonsList(); + } else { + Logger.printDebug(() -> "Unknown model: " + model + ", videoId: " + videoId); + } + if (finalActionButtons == null || finalActionButtons.isEmpty()) { + return; + } + + List actionButtons = new ArrayList<>(finalActionButtons.size()); + for (var buttons : finalActionButtons) { + ActionButton actionButton = ActionButton.UNKNOWN; + var primaryButtonViewModel = buttons.getPrimaryButtonViewModel(); + + if (primaryButtonViewModel.hasSecondaryButtonViewModel()) { + String iconName = primaryButtonViewModel.getSecondaryButtonViewModel().getIconName(); + if (iconName != null) { + actionButton = getActionButton(iconName); + if (actionButton == ActionButton.UNKNOWN) { + Logger.printDebug(() -> "Unknown iconName: " + iconName + ", videoId: " + videoId); + } + } + } else if (primaryButtonViewModel.hasAddToPlaylistButtonViewModel()) { + actionButton = ActionButton.PLAYLIST; + } else if (primaryButtonViewModel.hasClipButtonViewModel()) { + actionButton = ActionButton.CLIP; + } else if (primaryButtonViewModel.hasCompactChannelBarViewModel()) { + actionButton = ActionButton.CHANNEL_PROFILE; + } else if (primaryButtonViewModel.hasDownloadButtonViewModel()) { + actionButton = ActionButton.DOWNLOAD; + } else if (primaryButtonViewModel.hasSegmentedLikeDislikeButtonViewModel()) { + actionButton = ActionButton.LIKE_DISLIKE; + } else { + Logger.printDebug(() -> "Unknown buttonViewModel: " + primaryButtonViewModel + ", videoId: " + videoId); + } + + actionButtons.add(actionButton); + } + + Logger.printDebug(() -> "New video id: " + videoId + ", action buttons: " + actionButtons); + actionButtonLookup.put(videoId, actionButtons); + } catch (Exception ex) { + Logger.printException(() -> "Failed to parse SingleColumnWatchNextResults", ex); + } + } + } } diff --git a/extensions/shared/src/main/java/app/morphe/extension/youtube/settings/Settings.java b/extensions/shared/src/main/java/app/morphe/extension/youtube/settings/Settings.java index 3a89b5126..8da88c33c 100644 --- a/extensions/shared/src/main/java/app/morphe/extension/youtube/settings/Settings.java +++ b/extensions/shared/src/main/java/app/morphe/extension/youtube/settings/Settings.java @@ -327,23 +327,24 @@ public class Settings extends SharedYouTubeSettings { // PreferenceScreen: Player - Action buttons public static final BooleanSetting DISABLE_LIKE_DISLIKE_GLOW = new BooleanSetting("revanced_disable_like_dislike_glow", FALSE); - public static final BooleanSetting HIDE_ASK_BUTTON = new BooleanSetting("revanced_hide_ask_button", FALSE); - public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", FALSE); - public static final BooleanSetting HIDE_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_comments_button", FALSE); - public static final BooleanSetting HIDE_DOWNLOAD_BUTTON = new BooleanSetting("revanced_hide_download_button", FALSE); - public static final BooleanSetting HIDE_HYPE_BUTTON = new BooleanSetting("revanced_hide_hype_button", FALSE); - public static final BooleanSetting HIDE_LIKE_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_like_dislike_button", FALSE); - public static final BooleanSetting HIDE_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_playlist_button", FALSE); - public static final BooleanSetting HIDE_PROMOTE_BUTTON = new BooleanSetting("revanced_hide_promote_button", FALSE); - public static final BooleanSetting HIDE_REMIX_BUTTON = new BooleanSetting("revanced_hide_remix_button", FALSE); - public static final BooleanSetting HIDE_REWARDS_BUTTON = new BooleanSetting("revanced_hide_rewards_button", FALSE); - public static final BooleanSetting HIDE_REPORT_BUTTON = new BooleanSetting("revanced_hide_report_button", FALSE); - public static final BooleanSetting HIDE_SHARE_BUTTON = new BooleanSetting("revanced_hide_share_button", FALSE); - public static final BooleanSetting HIDE_SHOP_BUTTON = new BooleanSetting("revanced_hide_shop_button", FALSE); - public static final BooleanSetting HIDE_STOP_ADS_BUTTON = new BooleanSetting("revanced_hide_stop_ads_button", FALSE); - public static final BooleanSetting HIDE_THANKS_BUTTON = new BooleanSetting("revanced_hide_thanks_button", FALSE); + public static final BooleanSetting HIDE_ACTION_BAR = new BooleanSetting("revanced_hide_action_bar", FALSE, true); + public static final BooleanSetting HIDE_ASK_BUTTON = new BooleanSetting("revanced_hide_ask_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_comments_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_DOWNLOAD_BUTTON = new BooleanSetting("revanced_hide_download_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_HYPE_BUTTON = new BooleanSetting("revanced_hide_hype_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_LIKE_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_like_dislike_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_playlist_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_PROMOTE_BUTTON = new BooleanSetting("revanced_hide_promote_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_REMIX_BUTTON = new BooleanSetting("revanced_hide_remix_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_REWARDS_BUTTON = new BooleanSetting("revanced_hide_rewards_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_REPORT_BUTTON = new BooleanSetting("revanced_hide_report_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_SHARE_BUTTON = new BooleanSetting("revanced_hide_share_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_SHOP_BUTTON = new BooleanSetting("revanced_hide_shop_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_STOP_ADS_BUTTON = new BooleanSetting("revanced_hide_stop_ads_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); + public static final BooleanSetting HIDE_THANKS_BUTTON = new BooleanSetting("revanced_hide_thanks_button", FALSE, true, parentInverted(HIDE_ACTION_BAR)); - public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX = new BooleanSetting("revanced_hide_action_button_index", FALSE, true); + public static final BooleanSetting HIDE_ACTION_BUTTON_INDEX = new BooleanSetting("revanced_hide_action_button_index", FALSE, true, parentInverted(HIDE_ACTION_BAR)); public static final IntegerSetting REMIX_BUTTON_INDEX = new IntegerSetting("revanced_remix_button_index", 3, true, parent(HIDE_ACTION_BUTTON_INDEX)); // PreferenceScreen: Player - Ambient mode diff --git a/extensions/shared/src/main/proto/app/morphe/extension/youtube/innertube/next_response.proto b/extensions/shared/src/main/proto/app/morphe/extension/youtube/innertube/next_response.proto new file mode 100644 index 000000000..3227d527d --- /dev/null +++ b/extensions/shared/src/main/proto/app/morphe/extension/youtube/innertube/next_response.proto @@ -0,0 +1,169 @@ +syntax = "proto3"; + +package app.morphe.extension.youtube.innertube; + +option optimize_for = LITE_RUNTIME; +option java_package = "app.morphe.extension.youtube.innertube"; + +message NextResponse { + PrimaryContents primaryContents = 7; +} + +message PrimaryContents { + SingleColumnWatchNextResults singleColumnWatchNextResults = 51779735; +} + +message SingleColumnWatchNextResults { + PrimaryResults primaryResults = 1; +} + +message PrimaryResults { + SecondaryResults secondaryResults = 49399797; +} + +message SecondaryResults { + repeated SecondaryContents secondaryContents = 1; +} + +message SecondaryContents { + oneof data { + SlimVideoMetadataSectionRenderer slimVideoMetadataSectionRenderer = 216561405; + ItemSectionRenderer itemSectionRenderer = 50195462; + ShelfRenderer shelfRenderer = 51845067; + } +} + +message SlimVideoMetadataSectionRenderer { + repeated TertiaryContents tertiaryContents = 1; + string videoId = 2; +} + +message ItemSectionRenderer { + repeated TertiaryContents tertiaryContents = 1; + string sectionIdentifier = 7; +} + +message ShelfRenderer { + Content content = 5; +} + +message Content { + HorizontalListRenderer horizontalListRenderer = 51431404; +} + +message HorizontalListRenderer { + int32 visibleItemCount = 7; +} + +message TertiaryContents { + ElementRenderer elementRenderer = 153515154; +} + +message ElementRenderer { + NewElement newElement = 172660663; +} + +message NewElement { + Type type = 1; + Properties properties = 2; +} + +message Properties { + IdentifierProperties identifierProperties = 183314536; +} + +message IdentifierProperties { + string identifier = 1; +} + +message Type { + ComponentType componentType = 168777401; +} + +message ComponentType { + Model model = 5; +} + +message Model { + oneof data { + VideoActionBarModel videoActionBarModel = 389155117; + VideoMetadataCarouselModel videoMetadataCarouselModel = 408379376; + YoutubeModel youtubeModel = 413471385; + } +} + +message VideoMetadataCarouselModel { + Data data = 1; +} + +message Data { + repeated CarouselTitleDatas carouselTitleDatas = 1; + repeated CarouselItemDatas carouselItemDatas = 2; +} + +message CarouselTitleDatas { + string title = 1; +} + +message CarouselItemDatas { +} + +message YoutubeModel { + ViewModel viewModel = 1; +} + +message ViewModel { + CompactifyVideoActionBarViewModel compactifyVideoActionBarViewModel = 2185; +} + +message CompactifyVideoActionBarViewModel { + repeated ActionButtons actionButtons = 1; +} + +message VideoActionBarModel { + VideoActionBarData videoActionBarData = 17; +} + +message VideoActionBarData { + repeated ActionButtons actionButtons = 1; +} + +message ActionButtons { + PrimaryButtonViewModel primaryButtonViewModel = 10; +} + +message PrimaryButtonViewModel { + oneof data { + AddToPlaylistButtonViewModel addToPlaylistButtonViewModel = 484598023; + SecondaryButtonViewModel secondaryButtonViewModel = 461054335; + ClipButtonViewModel clipButtonViewModel = 490025863; + CompactChannelBarViewModel compactChannelBarViewModel = 1107; + DownloadButtonViewModel downloadButtonViewModel = 33561776; + SegmentedLikeDislikeButtonViewModel segmentedLikeDislikeButtonViewModel = 484586709; + } +} + +message AddToPlaylistButtonViewModel { + string addToPlaylistEntityKey = 2; +} + +message SecondaryButtonViewModel { + string iconName = 2; + optional string accessibilityId = 17; +} + +message ClipButtonViewModel { + string videoActionButtonEnablementEntityKey = 2; +} + +message CompactChannelBarViewModel { + bool enableCompactChannelBarInnerRefactor = 29; +} + +message DownloadButtonViewModel { + string videoId = 1; +} + +message SegmentedLikeDislikeButtonViewModel { + string teasersOrderEntityKey = 10; +} diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/player/action/ActionButtonsPatch.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/player/action/ActionButtonsPatch.kt index 6cc9d3d63..7df16b7c9 100644 --- a/patches/src/main/kotlin/app/morphe/patches/youtube/player/action/ActionButtonsPatch.kt +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/player/action/ActionButtonsPatch.kt @@ -1,8 +1,12 @@ package app.morphe.patches.youtube.player.action +import app.morphe.patcher.extensions.InstructionExtensions.addInstruction +import app.morphe.patcher.extensions.InstructionExtensions.getInstruction import app.morphe.patcher.patch.bytecodePatch +import app.morphe.patches.shared.misc.fix.proto.fixProtoLibraryPatch import app.morphe.patches.shared.litho.addLithoFilter import app.morphe.patches.shared.litho.lithoFilterPatch +import app.morphe.patches.youtube.shared.WatchNextResponseParserFingerprint import app.morphe.patches.youtube.utils.auth.authHookPatch import app.morphe.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.morphe.patches.youtube.utils.componentlist.hookElementList @@ -17,6 +21,7 @@ import app.morphe.patches.youtube.utils.settings.settingsPatch import app.morphe.patches.youtube.video.information.videoInformationPatch import app.morphe.patches.youtube.video.videoid.hookPlayerResponseVideoId import app.morphe.patches.youtube.video.videoid.videoIdPatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction private const val FILTER_CLASS_DESCRIPTOR = "$COMPONENTS_PATH/ActionButtonsFilter;" @@ -35,6 +40,7 @@ val actionButtonsPatch = bytecodePatch( lithoFilterPatch, lithoLayoutPatch, lazilyConvertedElementHookPatch, + fixProtoLibraryPatch, videoInformationPatch, videoIdPatch, authHookPatch, @@ -43,6 +49,7 @@ val actionButtonsPatch = bytecodePatch( execute { addLithoFilter(FILTER_CLASS_DESCRIPTOR) + hookElementList("$FILTER_CLASS_DESCRIPTOR->onLazilyConvertedElementLoaded") // region patch for hide action buttons by index @@ -51,6 +58,20 @@ val actionButtonsPatch = bytecodePatch( // endregion + WatchNextResponseParserFingerprint.let { + it.clearMatch() + it.method.apply { + val index = it.instructionMatches[5].index + val register = getInstruction(index).registerA + + addInstruction( + index + 1, + "invoke-static { v$register }, $FILTER_CLASS_DESCRIPTOR->" + + "onSingleColumnWatchNextResultsLoaded(Lcom/google/protobuf/MessageLite;)V" + ) + } + } + // region add settings addPreference( diff --git a/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Fingerprints.kt b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Fingerprints.kt new file mode 100644 index 000000000..f5e705e73 --- /dev/null +++ b/patches/src/main/kotlin/app/morphe/patches/youtube/shared/Fingerprints.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-patches + */ + +package app.morphe.patches.youtube.shared + +import app.morphe.patcher.Fingerprint +import app.morphe.patcher.InstructionLocation.MatchAfterImmediately +import app.morphe.patcher.InstructionLocation.MatchAfterWithin +import app.morphe.patcher.fieldAccess +import app.morphe.patcher.literal +import app.morphe.patcher.opcode +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal object WatchNextResponseParserFingerprint : Fingerprint( + accessFlags = listOf(AccessFlags.PUBLIC, AccessFlags.FINAL), + parameters = listOf("Ljava/lang/Object;"), + returnType = "Ljava/util/List;", + filters = listOf( + literal(49399797L), + opcode(Opcode.SGET_OBJECT), + fieldAccess( + opcode = Opcode.IGET_OBJECT, + location = MatchAfterImmediately() + ), + literal(51779735L), + fieldAccess( + opcode = Opcode.IGET_OBJECT, + type = "Ljava/lang/Object;", + location = MatchAfterWithin(5) + ), + opcode( + Opcode.CHECK_CAST, + location = MatchAfterImmediately() + ), + literal(46659098L), + ) +) diff --git a/patches/src/main/resources/youtube/settings/host/values/strings.xml b/patches/src/main/resources/youtube/settings/host/values/strings.xml index bee654d46..067c81315 100644 --- a/patches/src/main/resources/youtube/settings/host/values/strings.xml +++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml @@ -659,14 +659,16 @@ Some components may not be hidden." "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 @@