package app.revanced.integrations.youtube.patches.components; import static app.revanced.integrations.youtube.shared.NavigationBar.NavigationButton; import android.os.Build; import android.view.View; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import app.revanced.integrations.shared.Logger; import app.revanced.integrations.shared.Utils; import app.revanced.integrations.youtube.StringTrieSearch; import app.revanced.integrations.youtube.settings.Settings; import app.revanced.integrations.youtube.shared.NavigationBar; import app.revanced.integrations.youtube.shared.PlayerType; @SuppressWarnings("unused") public final class LayoutComponentsFilter extends Filter { private final StringTrieSearch exceptions = new StringTrieSearch(); private static final StringTrieSearch mixPlaylistsExceptions = new StringTrieSearch(); private static final ByteArrayFilterGroup mixPlaylistsExceptions2 = new ByteArrayFilterGroup( null, "cell_description_body" ); private static final ByteArrayFilterGroup mixPlaylists = new ByteArrayFilterGroup( Settings.HIDE_MIX_PLAYLISTS, "&list=" ); private final StringFilterGroup searchResultShelfHeader; private final StringFilterGroup inFeedSurvey; private final StringFilterGroup notifyMe; private final StringFilterGroup expandableMetadata; private final ByteArrayFilterGroup searchResultRecommendations; private final StringFilterGroup searchResultVideo; private final StringFilterGroup compactChannelBarInner; private final StringFilterGroup compactChannelBarInnerButton; private final ByteArrayFilterGroup joinMembershipButton; private final StringFilterGroup horizontalShelves; static { mixPlaylistsExceptions.addPatterns( "V.ED", // Playlist browse id. "java.lang.ref.WeakReference" ); } @RequiresApi(api = Build.VERSION_CODES.N) public LayoutComponentsFilter() { exceptions.addPatterns( "home_video_with_context", "related_video_with_context", "search_video_with_context", "comment_thread", // Whitelist comments "|comment.", // Whitelist comment replies "library_recent_shelf" ); // Identifiers. final var graySeparator = new StringFilterGroup( Settings.HIDE_GRAY_SEPARATOR, "cell_divider" // layout residue (gray line above the buttoned ad), ); final var chipsShelf = new StringFilterGroup( Settings.HIDE_CHIPS_SHELF, "chips_shelf" ); addIdentifierCallbacks( graySeparator, chipsShelf ); // Paths. final var communityPosts = new StringFilterGroup( Settings.HIDE_COMMUNITY_POSTS, "post_base_wrapper", "image_post_root.eml" ); final var communityGuidelines = new StringFilterGroup( Settings.HIDE_COMMUNITY_GUIDELINES, "community_guidelines" ); final var subscribersCommunityGuidelines = new StringFilterGroup( Settings.HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES, "sponsorships_comments_upsell" ); final var channelMemberShelf = new StringFilterGroup( Settings.HIDE_CHANNEL_MEMBER_SHELF, "member_recognition_shelf" ); final var compactBanner = new StringFilterGroup( Settings.HIDE_COMPACT_BANNER, "compact_banner" ); inFeedSurvey = new StringFilterGroup( Settings.HIDE_FEED_SURVEY, "in_feed_survey", "slimline_survey" ); final var medicalPanel = new StringFilterGroup( Settings.HIDE_MEDICAL_PANELS, "medical_panel" ); final var paidPromotion = new StringFilterGroup( Settings.HIDE_PAID_PROMOTION_LABEL, "paid_content_overlay" ); final var infoPanel = new StringFilterGroup( Settings.HIDE_HIDE_INFO_PANELS, "publisher_transparency_panel", "single_item_information_panel" ); final var latestPosts = new StringFilterGroup( Settings.HIDE_HIDE_LATEST_POSTS, "post_shelf" ); final var channelGuidelines = new StringFilterGroup( Settings.HIDE_HIDE_CHANNEL_GUIDELINES, "channel_guidelines_entry_banner" ); final var emergencyBox = new StringFilterGroup( Settings.HIDE_EMERGENCY_BOX, "emergency_onebox" ); // The player audio track button does the exact same function as the audio track flyout menu option. // But if the copy url button is shown, these button clashes and the the audio button does not work. // Previously this was a setting to show/hide the player button. // But it was decided it's simpler to always hide this button because: // - it doesn't work with copy video url feature // - the button is rare // - always hiding makes the ReVanced settings simpler and easier to understand // - nobody is going to notice the redundant button is always hidden final var audioTrackButton = new StringFilterGroup( null, "multi_feed_icon_button" ); final var artistCard = new StringFilterGroup( Settings.HIDE_ARTIST_CARDS, "official_card" ); expandableMetadata = new StringFilterGroup( Settings.HIDE_EXPANDABLE_CHIP, "inline_expander" ); final var videoQualityMenuFooter = new StringFilterGroup( Settings.HIDE_VIDEO_QUALITY_MENU_FOOTER, "quality_sheet_footer" ); final var channelBar = new StringFilterGroup( Settings.HIDE_CHANNEL_BAR, "channel_bar" ); final var relatedVideos = new StringFilterGroup( Settings.HIDE_RELATED_VIDEOS, "fullscreen_related_videos" ); final var playables = new StringFilterGroup( Settings.HIDE_PLAYABLES, "horizontal_gaming_shelf.eml" ); final var quickActions = new StringFilterGroup( Settings.HIDE_QUICK_ACTIONS, "quick_actions" ); final var imageShelf = new StringFilterGroup( Settings.HIDE_IMAGE_SHELF, "image_shelf" ); final var timedReactions = new StringFilterGroup( Settings.HIDE_TIMED_REACTIONS, "emoji_control_panel", "timed_reaction" ); searchResultShelfHeader = new StringFilterGroup( Settings.HIDE_SEARCH_RESULT_SHELF_HEADER, "shelf_header.eml" ); notifyMe = new StringFilterGroup( Settings.HIDE_NOTIFY_ME_BUTTON, "set_reminder_button" ); compactChannelBarInner = new StringFilterGroup( Settings.HIDE_JOIN_MEMBERSHIP_BUTTON, "compact_channel_bar_inner" ); compactChannelBarInnerButton = new StringFilterGroup( null, "|button.eml|" ); joinMembershipButton = new ByteArrayFilterGroup( null, "sponsorships" ); final var channelWatermark = new StringFilterGroup( Settings.HIDE_VIDEO_CHANNEL_WATERMARK, "featured_channel_watermark_overlay" ); final var forYouShelf = new StringFilterGroup( Settings.HIDE_FOR_YOU_SHELF, "mixed_content_shelf" ); searchResultVideo = new StringFilterGroup( Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS, "search_video_with_context.eml" ); searchResultRecommendations = new ByteArrayFilterGroup( Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS, "endorsement_header_footer" ); horizontalShelves = new StringFilterGroup( Settings.HIDE_HORIZONTAL_SHELVES, "horizontal_video_shelf.eml", "horizontal_shelf.eml", "horizontal_tile_shelf.eml" ); addPathCallbacks( expandableMetadata, inFeedSurvey, notifyMe, channelBar, communityPosts, paidPromotion, searchResultVideo, latestPosts, channelWatermark, communityGuidelines, playables, quickActions, relatedVideos, compactBanner, compactChannelBarInner, medicalPanel, videoQualityMenuFooter, infoPanel, emergencyBox, subscribersCommunityGuidelines, channelGuidelines, audioTrackButton, artistCard, timedReactions, imageShelf, channelMemberShelf, forYouShelf, horizontalShelves ); } @Override boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { if (matchedGroup == searchResultVideo) { if (searchResultRecommendations.check(protobufBufferArray).isFiltered()) { return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } return false; } // The groups are excluded from the filter due to the exceptions list below. // Filter them separately here. if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) { return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } if (exceptions.matches(path)) return false; // Exceptions are not filtered. if (matchedGroup == compactChannelBarInner) { if (compactChannelBarInnerButton.check(path).isFiltered()) { // The filter may be broad, but in the context of a compactChannelBarInnerButton, // it's safe to assume that the button is the only thing that should be hidden. if (joinMembershipButton.check(protobufBufferArray).isFiltered()) { return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } } return false; } // TODO: This also hides the feed Shorts shelf header if (matchedGroup == searchResultShelfHeader && contentIndex != 0) return false; if (matchedGroup == horizontalShelves) { if (contentIndex == 0 && hideShelves()) { return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex); } return false; } return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); } /** * Injection point. * Called from a different place then the other filters. */ public static boolean filterMixPlaylists(final Object conversionContext, @Nullable final byte[] bytes) { if (bytes == null) { Logger.printDebug(() -> "bytes is null"); return false; } // Prevent playlist items being hidden, if a mix playlist is present in it. if (mixPlaylistsExceptions.matches(conversionContext.toString())) return false; if (!mixPlaylists.check(bytes).isFiltered()) return false; // Prevent hiding the description of some videos accidentally. if (mixPlaylistsExceptions2.check(bytes).isFiltered()) return false; Logger.printDebug(() -> "Filtered mix playlist"); return true; } /** * Injection point. */ public static boolean showWatermark() { return !Settings.HIDE_VIDEO_CHANNEL_WATERMARK.get(); } private static final boolean HIDE_SHOW_MORE_BUTTON_ENABLED = Settings.HIDE_SHOW_MORE_BUTTON.get(); /** * Injection point. */ public static void hideShowMoreButton(View view) { if (HIDE_SHOW_MORE_BUTTON_ENABLED && NavigationBar.isSearchBarActive() // Search bar can be active but behind the player. && !PlayerType.getCurrent().isMaximizedOrFullscreen()) { Utils.hideViewByLayoutParams(view); } } private static boolean hideShelves() { // If the player is opened while library is selected, // then filter any recommendations below the player. if (PlayerType.getCurrent().isMaximizedOrFullscreen() // Or if the search is active while library is selected, then also filter. || NavigationBar.isSearchBarActive()) { return true; } // Check navigation button last. // Only filter if the library tab is not selected. // This check is important as the shelf layout is used for the library tab playlists. NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); return selectedNavButton != null && !selectedNavButton.isLibraryOrYouTab(); } }