fix: Use encoded native byte array buffer to filter Litho components
This commit is contained in:
parent
5aec6cd3b7
commit
7495528815
31 changed files with 781 additions and 731 deletions
|
|
@ -0,0 +1,13 @@
|
|||
package app.revanced.extension.shared;
|
||||
|
||||
public final class ConversionContext {
|
||||
/**
|
||||
* Interface to use obfuscated methods.
|
||||
*/
|
||||
public interface ContextInterface {
|
||||
// Methods implemented by patch.
|
||||
StringBuilder patch_getPathBuilder();
|
||||
|
||||
String patch_getIdentifier();
|
||||
}
|
||||
}
|
||||
|
|
@ -376,7 +376,7 @@ public class Utils {
|
|||
/**
|
||||
* Checks if a specific app package is installed and enabled on the device.
|
||||
*
|
||||
* @param packageName The application package name to check (e.g., "app.morphe.android.apps.youtube.music").
|
||||
* @param packageName The application package name to check (e.g., "app.revanced.android.apps.youtube.music").
|
||||
* @return True if the package is installed and enabled, false otherwise.
|
||||
*/
|
||||
public static boolean isPackageEnabled(String packageName) {
|
||||
|
|
@ -396,6 +396,18 @@ public class Utils {
|
|||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean startsWithAny(String value, String...targets) {
|
||||
if (isNotEmpty(value)) {
|
||||
for (String string : targets) {
|
||||
if (isNotEmpty(string) && value.startsWith(string)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public interface MatchFilter<T> {
|
||||
boolean matches(T object);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,12 +5,10 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
|
|
@ -123,11 +121,6 @@ public final class LithoFilterPatch {
|
|||
*/
|
||||
private static final boolean EXTRACT_IDENTIFIER_FROM_BUFFER = false;
|
||||
|
||||
/**
|
||||
* Turns on additional logging, used for development purposes only.
|
||||
*/
|
||||
public static final boolean DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER = false;
|
||||
|
||||
/**
|
||||
* String suffix for components.
|
||||
* Can be any of: ".eml", ".eml-fe", ".e-b", ".eml-js", "e-js-b"
|
||||
|
|
@ -146,19 +139,6 @@ public final class LithoFilterPatch {
|
|||
*/
|
||||
private static final ThreadLocal<byte[]> bufferThreadLocal = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* Identifier to protocol buffer mapping. Only used for 20.22+.
|
||||
* Thread local is needed because filtering is multithreaded and each thread can load
|
||||
* a different component with the same identifier.
|
||||
*/
|
||||
private static final ThreadLocal<Map<String, byte[]>> identifierToBufferThread = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* Global shared buffer. Used only if the buffer is not found in the ThreadLocal.
|
||||
*/
|
||||
private static final Map<String, byte[]> identifierToBufferGlobal
|
||||
= Collections.synchronizedMap(createIdentifierToBufferMap());
|
||||
|
||||
private static final StringTrieSearch pathSearchTree = new StringTrieSearch();
|
||||
private static final StringTrieSearch identifierSearchTree = new StringTrieSearch();
|
||||
|
||||
|
|
@ -211,126 +191,11 @@ public final class LithoFilterPatch {
|
|||
}
|
||||
}
|
||||
|
||||
private static Map<String, byte[]> createIdentifierToBufferMap() {
|
||||
// It's unclear how many items should be cached. This is a guess.
|
||||
return Utils.createSizeRestrictedMap(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that differs from {@link Character#isDigit(char)}
|
||||
* as this only matches ascii and not Unicode numbers.
|
||||
*/
|
||||
private static boolean isAsciiNumber(byte character) {
|
||||
return '0' <= character && character <= '9';
|
||||
}
|
||||
|
||||
private static boolean isAsciiLowerCaseLetter(byte character) {
|
||||
return 'a' <= character && character <= 'z';
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
* Targets 20.22+
|
||||
*/
|
||||
public static void setProtoBuffer(byte[] buffer) {
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
LithoFilterParameters.findAsciiStrings(builder, buffer);
|
||||
Logger.printDebug(() -> "New buffer: " + builder);
|
||||
}
|
||||
|
||||
// The identifier always seems to start very close to the buffer start.
|
||||
// Highest identifier start index ever observed is 50, with most around 30 to 40.
|
||||
// The buffer can be very large with up to 200kb has been observed,
|
||||
// so the search is restricted to only the start.
|
||||
final int maxBufferStartIndex = 500; // 10x expected upper bound.
|
||||
|
||||
// Could use Boyer-Moore-Horspool since the string is ASCII and has a limited number of
|
||||
// unique characters, but it seems to be slower since the extra overhead of checking the
|
||||
// bad character array negates any performance gain of skipping a few extra subsearches.
|
||||
int emlIndex = -1;
|
||||
final int emlStringLength = LITHO_COMPONENT_EXTENSION_BYTES.length;
|
||||
final int lastBufferIndexToCheckFrom = Math.min(maxBufferStartIndex, buffer.length - emlStringLength);
|
||||
for (int i = 0; i < lastBufferIndexToCheckFrom; i++) {
|
||||
boolean match = true;
|
||||
for (int j = 0; j < emlStringLength; j++) {
|
||||
if (buffer[i + j] != LITHO_COMPONENT_EXTENSION_BYTES[j]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match) {
|
||||
emlIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (emlIndex < 0) {
|
||||
// Buffer is not used for creating a new litho component.
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
Logger.printDebug(() -> "Could not find eml index");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
int startIndex = emlIndex - 1;
|
||||
while (startIndex > 0) {
|
||||
final byte character = buffer[startIndex];
|
||||
int startIndexFinal = startIndex;
|
||||
if (isAsciiLowerCaseLetter(character) || isAsciiNumber(character) || character == '_') {
|
||||
// Valid character for the first path element.
|
||||
startIndex--;
|
||||
} else {
|
||||
startIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Strip away any numbers on the start of the identifier, which can
|
||||
// be from random data in the buffer before the identifier starts.
|
||||
while (true) {
|
||||
final byte character = buffer[startIndex];
|
||||
if (isAsciiNumber(character)) {
|
||||
startIndex++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Find the pipe character after the identifier.
|
||||
int endIndex = -1;
|
||||
for (int i = emlIndex, length = buffer.length; i < length; i++) {
|
||||
if (buffer[i] == '|') {
|
||||
endIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (endIndex < 0) {
|
||||
if (BaseSettings.DEBUG.get()) {
|
||||
Logger.printException(() -> "Debug: Could not find buffer identifier");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String identifier = new String(buffer, startIndex, endIndex - startIndex, StandardCharsets.US_ASCII);
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
Logger.printDebug(() -> "Found buffer for identifier: " + identifier);
|
||||
}
|
||||
identifierToBufferGlobal.put(identifier, buffer);
|
||||
|
||||
Map<String, byte[]> map = identifierToBufferThread.get();
|
||||
if (map == null) {
|
||||
map = createIdentifierToBufferMap();
|
||||
identifierToBufferThread.set(map);
|
||||
}
|
||||
map.put(identifier, buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
* Targets 20.21 and lower.
|
||||
*/
|
||||
public static void setProtoBuffer(@Nullable ByteBuffer buffer) {
|
||||
public static void setProtobufBuffer(@Nullable ByteBuffer buffer) {
|
||||
if (buffer == null || !buffer.hasArray()) {
|
||||
// It appears the buffer can be cleared out just before the call to #filter()
|
||||
// Ignore this null value and retain the last buffer that was set.
|
||||
|
|
@ -347,44 +212,18 @@ public final class LithoFilterPatch {
|
|||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean isFiltered(String identifier, @Nullable String accessibilityId,
|
||||
@Nullable String accessibilityText, StringBuilder pathBuilder) {
|
||||
public static boolean isFiltered(ContextInterface contextInterface, @Nullable byte[] bytes,
|
||||
@Nullable String accessibilityId, @Nullable String accessibilityText) {
|
||||
try {
|
||||
String identifier = contextInterface.patch_getIdentifier();
|
||||
StringBuilder pathBuilder = contextInterface.patch_getPathBuilder();
|
||||
if (identifier.isEmpty() || pathBuilder.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[] buffer = null;
|
||||
if (EXTRACT_IDENTIFIER_FROM_BUFFER) {
|
||||
final int pipeIndex = identifier.indexOf('|');
|
||||
if (pipeIndex >= 0) {
|
||||
// If the identifier contains no pipe, then it's not an ".eml" identifier
|
||||
// and the buffer is not uniquely identified. Typically, this only happens
|
||||
// for subcomponents where buffer filtering is not used.
|
||||
String identifierKey = identifier.substring(0, pipeIndex);
|
||||
|
||||
var map = identifierToBufferThread.get();
|
||||
if (map != null) {
|
||||
buffer = map.get(identifierKey);
|
||||
}
|
||||
|
||||
if (buffer == null) {
|
||||
// Buffer for thread local not found. Use the last buffer found from any thread.
|
||||
buffer = identifierToBufferGlobal.get(identifierKey);
|
||||
|
||||
if (DEBUG_EXTRACT_IDENTIFIER_FROM_BUFFER && buffer == null) {
|
||||
// No buffer is found for some components, such as
|
||||
// shorts_lockup_cell.eml on channel profiles.
|
||||
// For now, just ignore this and filter without a buffer.
|
||||
if (BaseSettings.DEBUG.get()) {
|
||||
Logger.printException(() -> "Debug: Could not find buffer for identifier: " + identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
buffer = bufferThreadLocal.get();
|
||||
}
|
||||
byte[] buffer = EXTRACT_IDENTIFIER_FROM_BUFFER
|
||||
? bytes
|
||||
: bufferThreadLocal.get();
|
||||
|
||||
// Potentially the buffer may have been null or never set up until now.
|
||||
// Use an empty buffer so the litho id/path filters that do not use a buffer still work.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
/**
|
||||
* Here is an unintended behavior:
|
||||
* <p>
|
||||
* 1. The user does not hide Shorts in the Subscriptions tab, but hides them otherwise.
|
||||
* 2. Goes to the Subscriptions tab and scrolls to where Shorts is.
|
||||
* 3. Opens a regular video.
|
||||
* 4. Minimizes the video and turns off the screen.
|
||||
* 5. Turns the screen on and maximizes the video.
|
||||
* 6. Shorts belonging to related videos are not hidden.
|
||||
* <p>
|
||||
* Here is an explanation of this special issue:
|
||||
* <p>
|
||||
* When the user minimizes the video, turns off the screen, and then turns it back on,
|
||||
* the components below the player are reloaded, and at this moment the PlayerType is [WATCH_WHILE_MINIMIZED].
|
||||
* (Shorts belonging to related videos are also reloaded)
|
||||
* Since the PlayerType is [WATCH_WHILE_MINIMIZED] at this moment, the navigation tab is checked.
|
||||
* (Even though PlayerType is [WATCH_WHILE_MINIMIZED], this is a Shorts belonging to a related video)
|
||||
* <p>
|
||||
* As a workaround for this special issue, if a video actionbar is detected, which is one of the components below the player,
|
||||
* it is treated as being in the same state as [WATCH_WHILE_MAXIMIZED].
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class LayoutReloadObserverPatch {
|
||||
private static final String COMPACTIFY_VIDEO_ACTION_BAR_PREFIX = "compactify_video_action_bar.e";
|
||||
private static final String VIDEO_ACTION_BAR_PREFIX = "video_action_bar.e";
|
||||
public static final AtomicBoolean isActionBarVisible = new AtomicBoolean(false);
|
||||
|
||||
public static void onLazilyConvertedElementLoaded(@NonNull String identifier,
|
||||
@NonNull List<Object> treeNodeResultList) {
|
||||
if (!Utils.startsWithAny(identifier, COMPACTIFY_VIDEO_ACTION_BAR_PREFIX, VIDEO_ACTION_BAR_PREFIX)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED &&
|
||||
isActionBarVisible.compareAndSet(false, true)) {
|
||||
Utils.runOnMainThreadDelayed(() -> isActionBarVisible.compareAndSet(true, false), 500);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class LazilyConvertedElementPatch {
|
||||
private static final String LAZILY_CONVERTED_ELEMENT = "LazilyConvertedElement";
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void onTreeNodeResultLoaded(ContextInterface contextInterface, List<Object> treeNodeResultList) {
|
||||
if (treeNodeResultList == null || treeNodeResultList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String firstElement = treeNodeResultList.get(0).toString();
|
||||
if (!LAZILY_CONVERTED_ELEMENT.equals(firstElement)) {
|
||||
return;
|
||||
}
|
||||
String identifier = contextInterface.patch_getIdentifier();
|
||||
if (Utils.isNotEmpty(identifier)) {
|
||||
onLazilyConvertedElementLoaded(identifier, treeNodeResultList);
|
||||
}
|
||||
}
|
||||
|
||||
private static void onLazilyConvertedElementLoaded(String identifier, List<Object> treeNodeResultList) {
|
||||
// Code added by patch.
|
||||
}
|
||||
}
|
||||
|
|
@ -54,6 +54,6 @@ public class PlayerControlsPatch {
|
|||
|
||||
// noinspection EmptyMethod
|
||||
private static void fullscreenButtonVisibilityChanged(boolean isVisible) {
|
||||
// Code added during patching.
|
||||
// Code added by patch.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,24 +19,25 @@ import app.revanced.extension.shared.Utils;
|
|||
import app.revanced.extension.youtube.patches.litho.ReturnYouTubeDislikeFilter;
|
||||
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.shared.ConversionContext.ContextInterface;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
/**
|
||||
* Handles all interaction of UI patch components.
|
||||
*
|
||||
* <p>
|
||||
* Known limitation:
|
||||
* The implementation of Shorts litho requires blocking the loading the first Short until RYD has completed.
|
||||
* This is because it modifies the dislikes text synchronously, and if the RYD fetch has
|
||||
* not completed yet then the UI will be temporarily frozen.
|
||||
*
|
||||
* <p>
|
||||
* A (yet to be implemented) solution that fixes this problem. Any one of:
|
||||
* - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes text asynchronously.
|
||||
* - Find a way to force Litho to rebuild it's component tree,
|
||||
* and use that hook to force the shorts dislikes to update after the fetch is completed.
|
||||
* and use that hook to force the shorts dislikes to update after the fetch is completed.
|
||||
* - Hook into the dislikes button image view, and replace the dislikes thumb down image with a
|
||||
* generated image of the number of dislikes, then update the image asynchronously. This Could
|
||||
* also be used for the regular video player to give a better UI layout and completely remove
|
||||
* the need for the Rolling Number patches.
|
||||
* generated image of the number of dislikes, then update the image asynchronously. This Could
|
||||
* also be used for the regular video player to give a better UI layout and completely remove
|
||||
* the need for the Rolling Number patches.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ReturnYouTubeDislikePatch {
|
||||
|
|
@ -100,7 +101,7 @@ public class ReturnYouTubeDislikePatch {
|
|||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* <p>
|
||||
* Logs if new litho text layout is used.
|
||||
*/
|
||||
public static boolean useNewLithoTextCreation(boolean useNewLithoTextCreation) {
|
||||
|
|
@ -113,28 +114,28 @@ public class ReturnYouTubeDislikePatch {
|
|||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* <p>
|
||||
* For Litho segmented buttons and Litho Shorts player.
|
||||
*/
|
||||
@NonNull
|
||||
public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
@NonNull CharSequence original) {
|
||||
return onLithoTextLoaded(conversionContext, original, false);
|
||||
public static CharSequence onLithoTextLoaded(ContextInterface contextInterface,
|
||||
CharSequence original) {
|
||||
return onLithoTextLoaded(contextInterface, original, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a litho text component is initially created,
|
||||
* and also when a Span is later reused again (such as scrolling off/on screen).
|
||||
*
|
||||
* <p>
|
||||
* This method is sometimes called on the main thread, but it is usually called _off_ the main thread.
|
||||
* This method can be called multiple times for the same UI element (including after dislikes was added).
|
||||
*
|
||||
* @param original Original char sequence was created or reused by Litho.
|
||||
* @param original Original char sequence was created or reused by Litho.
|
||||
* @param isRollingNumber If the span is for a Rolling Number.
|
||||
* @return The original char sequence (if nothing should change), or a replacement char sequence that contains dislikes.
|
||||
*/
|
||||
@NonNull
|
||||
private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext,
|
||||
private static CharSequence onLithoTextLoaded(ContextInterface contextInterface,
|
||||
@NonNull CharSequence original,
|
||||
boolean isRollingNumber) {
|
||||
try {
|
||||
|
|
@ -142,17 +143,16 @@ public class ReturnYouTubeDislikePatch {
|
|||
return original;
|
||||
}
|
||||
|
||||
String conversionContextString = conversionContext.toString();
|
||||
|
||||
if (Settings.RYD_ENABLED.get()) { // FIXME: Remove this.
|
||||
Logger.printDebug(() -> "RYD conversion context: " + conversionContext);
|
||||
}
|
||||
|
||||
if (isRollingNumber && !conversionContextString.contains("video_action_bar.e")) {
|
||||
String identifier = contextInterface.patch_getIdentifier();
|
||||
if (isRollingNumber && (identifier == null || !identifier.contains("video_action_bar.e"))) {
|
||||
return original;
|
||||
}
|
||||
|
||||
if (conversionContextString.contains("segmented_like_dislike_button.e")) {
|
||||
StringBuilder pathBuilder = contextInterface.patch_getPathBuilder();
|
||||
String path = pathBuilder.toString();
|
||||
|
||||
if (path.contains("segmented_like_dislike_button.e")) {
|
||||
|
||||
// Regular video.
|
||||
ReturnYouTubeDislike videoData = currentVideoData;
|
||||
if (videoData == null) {
|
||||
|
|
@ -169,13 +169,11 @@ public class ReturnYouTubeDislikePatch {
|
|||
return original; // No need to check for Shorts in the context.
|
||||
}
|
||||
|
||||
if (Utils.containsAny(conversionContextString,
|
||||
"|shorts_dislike_button.e", "|reel_dislike_button.e")) {
|
||||
if (Utils.containsAny(path, "|shorts_dislike_button.e", "|reel_dislike_button.e")) {
|
||||
return getShortsSpan(original, true);
|
||||
}
|
||||
|
||||
if (Utils.containsAny(conversionContextString,
|
||||
"|shorts_like_button.e", "|reel_like_button.e")) {
|
||||
if (Utils.containsAny(path, "|shorts_like_button.e", "|reel_like_button.e")) {
|
||||
if (!Utils.containsNumber(original)) {
|
||||
Logger.printDebug(() -> "Replacing hidden likes count");
|
||||
return getShortsSpan(original, false);
|
||||
|
|
@ -230,10 +228,9 @@ public class ReturnYouTubeDislikePatch {
|
|||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String onRollingNumberLoaded(@NonNull Object conversionContext,
|
||||
@NonNull String original) {
|
||||
public static String onRollingNumberLoaded(ContextInterface contextInterface, String original) {
|
||||
try {
|
||||
CharSequence replacement = onLithoTextLoaded(conversionContext, original, true);
|
||||
CharSequence replacement = onLithoTextLoaded(contextInterface, original, true);
|
||||
|
||||
String replacementString = replacement.toString();
|
||||
if (!replacementString.equals(original)) {
|
||||
|
|
@ -248,7 +245,7 @@ public class ReturnYouTubeDislikePatch {
|
|||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* <p>
|
||||
* Called for all usage of Rolling Number.
|
||||
* Modifies the measured String text width to include the left separator and padding, if needed.
|
||||
*/
|
||||
|
|
@ -489,7 +486,7 @@ public class ReturnYouTubeDislikePatch {
|
|||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* <p>
|
||||
* Called when the user likes or dislikes.
|
||||
*
|
||||
* @param vote int that matches {@link Vote#value}
|
||||
|
|
|
|||
|
|
@ -139,22 +139,14 @@ public final class DescriptionComponentsFilter extends Filter {
|
|||
// If the description panel is opened in a Shorts, PlayerType is 'HIDDEN',
|
||||
// so 'PlayerType.getCurrent().isMaximizedOrFullscreen()' does not guarantee that the description panel is open.
|
||||
// Instead, use the engagement id to check if the description panel is opened.
|
||||
if (!EngagementPanel.isDescription()
|
||||
// The user can minimize the player while the engagement panel is open.
|
||||
//
|
||||
// In this case, the engagement panel is treated as open.
|
||||
// (If the player is dismissed, the engagement panel is considered closed)
|
||||
//
|
||||
// Therefore, the following exceptions can occur:
|
||||
// 1. The user opened a regular video and opened the description panel.
|
||||
// 2. The 'horizontalShelf' elements were hidden.
|
||||
// 3. The user minimized the player.
|
||||
// 4. The user manually refreshed the library tab without dismissing the player.
|
||||
// 5. Since the engagement panel is treated as open, the history shelf is filtered.
|
||||
//
|
||||
// To handle these exceptions, filtering is not performed even when the player is minimized.
|
||||
|| PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED
|
||||
) {
|
||||
if (!EngagementPanel.isDescription()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// PlayerType when the description panel is opened: NONE, HIDDEN,
|
||||
// WATCH_WHILE_MAXIMIZED, WATCH_WHILE_FULLSCREEN, WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN.
|
||||
PlayerType playerType = PlayerType.getCurrent();
|
||||
if (!playerType.isNoneOrHidden() && !playerType.isMaximizedOrFullscreen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,102 @@
|
|||
package app.revanced.extension.youtube.patches.litho;
|
||||
|
||||
import static app.revanced.extension.youtube.patches.LayoutReloadObserverPatch.isActionBarVisible;
|
||||
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.EngagementPanel;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
final class HorizontalShelvesFilter extends Filter {
|
||||
private final ByteArrayFilterGroupList descriptionBuffers = new ByteArrayFilterGroupList();
|
||||
private final ByteArrayFilterGroupList generalBuffers = new ByteArrayFilterGroupList();
|
||||
|
||||
public HorizontalShelvesFilter() {
|
||||
StringFilterGroup horizontalShelves = new StringFilterGroup(null, "horizontal_shelf.e");
|
||||
addPathCallbacks(horizontalShelves);
|
||||
|
||||
descriptionBuffers.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_ATTRIBUTES_SECTION,
|
||||
// May no longer work on v20.31+, even though the component is still there.
|
||||
"cell_video_attribute"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_FEATURED_PLACES_SECTION,
|
||||
"yt_fill_experimental_star",
|
||||
"yt_fill_star"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_GAMING_SECTION,
|
||||
"yt_outline_experimental_gaming",
|
||||
"yt_outline_gaming"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_MUSIC_SECTION,
|
||||
"yt_outline_experimental_audio",
|
||||
"yt_outline_audio"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_QUIZZES_SECTION,
|
||||
"post_base_wrapper_slim"
|
||||
)
|
||||
);
|
||||
|
||||
generalBuffers.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_CREATOR_STORE_SHELF,
|
||||
"shopping_item_card_list"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYABLES,
|
||||
"FEmini_app_destination"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_TICKET_SHELF,
|
||||
"ticket_item.e"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private boolean hideShelves() {
|
||||
if (!Settings.HIDE_HORIZONTAL_SHELVES.get()) {
|
||||
return false;
|
||||
}
|
||||
// Must check player type first, as search bar can be active behind the player.
|
||||
if (PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get()) {
|
||||
return false;
|
||||
}
|
||||
// Must check second, as search can be from any tab.
|
||||
if (NavigationBar.isSearchBarActive()) {
|
||||
return true;
|
||||
}
|
||||
return NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFiltered(String identifier, String accessibility, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (contentIndex != 0) {
|
||||
return false;
|
||||
}
|
||||
if (generalBuffers.check(buffer).isFiltered()) {
|
||||
return true;
|
||||
}
|
||||
if (EngagementPanel.isDescription()) {
|
||||
PlayerType playerType = PlayerType.getCurrent();
|
||||
// PlayerType when the description panel is opened: NONE, HIDDEN,
|
||||
// WATCH_WHILE_MAXIMIZED, WATCH_WHILE_FULLSCREEN, WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN.
|
||||
if (!playerType.isMaximizedOrFullscreen() && !playerType.isNoneOrHidden()) {
|
||||
return false;
|
||||
}
|
||||
return descriptionBuffers.check(buffer).isFiltered();
|
||||
}
|
||||
return hideShelves();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@ import android.graphics.drawable.Drawable;
|
|||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
|
|
@ -16,16 +15,12 @@ import android.widget.TextView;
|
|||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import app.revanced.extension.shared.ByteTrieSearch;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.StringTrieSearch;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.StringFilterGroup;
|
||||
|
|
@ -38,10 +33,6 @@ import app.revanced.extension.youtube.shared.PlayerType;
|
|||
|
||||
@SuppressWarnings("unused")
|
||||
public final class LayoutComponentsFilter extends Filter {
|
||||
private static final StringTrieSearch mixPlaylistsContextExceptions = new StringTrieSearch(
|
||||
"V.ED", // Playlist browseId.
|
||||
"java.lang.ref.WeakReference"
|
||||
);
|
||||
private static final ByteArrayFilterGroup mixPlaylistsBufferExceptions = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"cell_description_body",
|
||||
|
|
@ -84,11 +75,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
private final StringFilterGroup chipBar;
|
||||
private final StringFilterGroup channelProfile;
|
||||
private final StringFilterGroupList channelProfileGroupList;
|
||||
private final StringFilterGroup horizontalShelves;
|
||||
private final ByteArrayFilterGroup playablesBuffer;
|
||||
private final ByteArrayFilterGroup ticketShelfBuffer;
|
||||
private final ByteArrayFilterGroup playerShoppingShelfBuffer;
|
||||
private final ByteTrieSearch descriptionSearch;
|
||||
|
||||
public LayoutComponentsFilter() {
|
||||
exceptions.addPatterns(
|
||||
|
|
@ -264,12 +250,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
"mini_game_card.e"
|
||||
);
|
||||
|
||||
// Playable horizontal shelf header.
|
||||
playablesBuffer = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"FEmini_app_destination"
|
||||
);
|
||||
|
||||
final var quickActions = new StringFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS,
|
||||
"quick_actions"
|
||||
|
|
@ -317,7 +297,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
);
|
||||
|
||||
final var forYouShelf = new StringFilterGroup(
|
||||
Settings.HIDE_FOR_YOU_SHELF,
|
||||
Settings.HIDE_HORIZONTAL_SHELVES,
|
||||
"mixed_content_shelf"
|
||||
);
|
||||
|
||||
|
|
@ -361,51 +341,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
)
|
||||
);
|
||||
|
||||
horizontalShelves = new StringFilterGroup(
|
||||
null, // Setting is checked in isFiltered()
|
||||
"horizontal_video_shelf.e",
|
||||
"horizontal_shelf.e",
|
||||
"horizontal_shelf_inline.e",
|
||||
"horizontal_tile_shelf.e"
|
||||
);
|
||||
|
||||
ticketShelfBuffer = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"ticket_item.e"
|
||||
);
|
||||
|
||||
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"shopping_item_card_list"
|
||||
);
|
||||
|
||||
// Work around for unique situation where filtering is based on the setting,
|
||||
// but it must not fall over to other filters if the setting is _not_ enabled.
|
||||
// This is only needed for the horizontal shelf that is used so extensively everywhere.
|
||||
descriptionSearch = new ByteTrieSearch();
|
||||
List.of(
|
||||
new Pair<>(Settings.HIDE_FEATURED_PLACES_SECTION, "yt_fill_star"),
|
||||
new Pair<>(Settings.HIDE_FEATURED_PLACES_SECTION, "yt_fill_experimental_star"),
|
||||
new Pair<>(Settings.HIDE_GAMING_SECTION, "yt_outline_gaming"),
|
||||
new Pair<>(Settings.HIDE_GAMING_SECTION, "yt_outline_experimental_gaming"),
|
||||
new Pair<>(Settings.HIDE_MUSIC_SECTION, "yt_outline_audio"),
|
||||
new Pair<>(Settings.HIDE_MUSIC_SECTION, "yt_outline_experimental_audio"),
|
||||
new Pair<>(Settings.HIDE_QUIZZES_SECTION, "post_base_wrapper_slim"),
|
||||
// May no longer work on v20.31+, even though the component is still there.
|
||||
new Pair<>(Settings.HIDE_ATTRIBUTES_SECTION, "cell_video_attribute")
|
||||
).forEach(pair -> {
|
||||
BooleanSetting setting = pair.first;
|
||||
descriptionSearch.addPattern(pair.second.getBytes(StandardCharsets.UTF_8),
|
||||
(textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
|
||||
//noinspection unchecked
|
||||
AtomicReference<Boolean> hide = (AtomicReference<Boolean>) callbackParameter;
|
||||
hide.set(setting.get());
|
||||
return true;
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
artistCard,
|
||||
audioTrackButton,
|
||||
|
|
@ -422,7 +357,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
emergencyBox,
|
||||
expandableMetadata,
|
||||
forYouShelf,
|
||||
horizontalShelves,
|
||||
imageShelf,
|
||||
infoPanel,
|
||||
latestPosts,
|
||||
|
|
@ -484,47 +418,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
&& joinMembershipButton.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
// Horizontal shelves are used everywhere in the app. And to prevent the generic "hide shelves"
|
||||
// from incorrectly hiding other stuff that has its own hide filters,
|
||||
// the more specific shelf filters must check first _and_ they must halt falling over
|
||||
// to other filters if the buffer matches but the setting is off.
|
||||
if (matchedGroup == horizontalShelves) {
|
||||
if (contentIndex != 0) return false;
|
||||
|
||||
AtomicReference<Boolean> descriptionFilterResult = new AtomicReference<>(null);
|
||||
if (descriptionSearch.matches(buffer, descriptionFilterResult)) {
|
||||
return descriptionFilterResult.get();
|
||||
}
|
||||
|
||||
// Check if others are off before searching.
|
||||
final boolean hideShelves = Settings.HIDE_HORIZONTAL_SHELVES.get();
|
||||
final boolean hideTickets = Settings.HIDE_TICKET_SHELF.get();
|
||||
final boolean hidePlayables = Settings.HIDE_PLAYABLES.get();
|
||||
final boolean hidePlayerShoppingShelf = Settings.HIDE_CREATOR_STORE_SHELF.get();
|
||||
if (!hideShelves && !hideTickets && !hidePlayables && !hidePlayerShoppingShelf)
|
||||
return false;
|
||||
|
||||
if (ticketShelfBuffer.check(buffer).isFiltered()) return hideTickets;
|
||||
if (playablesBuffer.check(buffer).isFiltered()) return hidePlayables;
|
||||
if (playerShoppingShelfBuffer.check(buffer).isFiltered())
|
||||
return hidePlayerShoppingShelf;
|
||||
|
||||
// 20.31+ when exiting fullscreen after watching for a while or when resuming the app,
|
||||
// then sometimes the buffer isn't correct and the player shopping shelf is shown.
|
||||
// If filtering reaches this point then there are no more shelves that could be in the player.
|
||||
// If shopping shelves are set to hidden and the player is active, then assume
|
||||
// it's the shopping shelf.
|
||||
if (hidePlayerShoppingShelf) {
|
||||
PlayerType type = PlayerType.getCurrent();
|
||||
if (type == PlayerType.WATCH_WHILE_MAXIMIZED || type == PlayerType.WATCH_WHILE_FULLSCREEN
|
||||
|| type == PlayerType.WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return hideShelves && hideShelves();
|
||||
}
|
||||
|
||||
if (matchedGroup == chipBar) {
|
||||
return contentIndex == 0 && NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY;
|
||||
}
|
||||
|
|
@ -536,7 +429,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
* Injection point.
|
||||
* Called from a different place then the other filters.
|
||||
*/
|
||||
public static boolean filterMixPlaylists(Object conversionContext, @Nullable byte[] buffer) {
|
||||
public static boolean filterMixPlaylists(@Nullable byte[] buffer) {
|
||||
// Edit: This hook may no longer be needed, and mix playlist filtering
|
||||
// might be possible using the existing litho filters.
|
||||
try {
|
||||
|
|
@ -551,13 +444,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
|
||||
if (mixPlaylists.check(buffer).isFiltered()
|
||||
// Prevent hiding the description of some videos accidentally.
|
||||
&& !mixPlaylistsBufferExceptions.check(buffer).isFiltered()
|
||||
// Prevent playlist items being hidden, if a mix playlist is present in it.
|
||||
// Check last since it requires creating a context string.
|
||||
//
|
||||
// FIXME: The conversion context passed in does not always generate a valid toString.
|
||||
// This string check may no longer be needed, or the patch may be broken.
|
||||
&& !mixPlaylistsContextExceptions.matches(conversionContext.toString())) {
|
||||
&& !mixPlaylistsBufferExceptions.check(buffer).isFiltered()) {
|
||||
Logger.printDebug(() -> "Filtered mix playlist");
|
||||
return true;
|
||||
}
|
||||
|
|
@ -757,31 +644,6 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
: original;
|
||||
}
|
||||
|
||||
private static boolean hideShelves() {
|
||||
// Horizontal shelves are used for music/game links in video descriptions,
|
||||
// such as https://youtube.com/watch?v=W8kI1na3S2M
|
||||
if (PlayerType.getCurrent().isMaximizedOrFullscreen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must check search bar after player type, since search results
|
||||
// can be in the background behind an open player.
|
||||
if (NavigationBar.isSearchBarActive()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Do not hide if the navigation back button is visible,
|
||||
// otherwise the content shelves in the explore/music/courses pages are hidden.
|
||||
if (NavigationBar.isBackButtonVisible()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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.
|
||||
return NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
|
|
@ -919,8 +781,8 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param typedString Keywords typed in the search bar.
|
||||
* @return Whether the setting is enabled and the typed string is empty.
|
||||
* @param typedString Keywords typed in the search bar.
|
||||
* @return Whether the setting is enabled and the typed string is empty.
|
||||
*/
|
||||
public static boolean hideYouMayLikeSection(String typedString) {
|
||||
return Settings.HIDE_YOU_MAY_LIKE_SECTION.get()
|
||||
|
|
@ -932,13 +794,13 @@ public final class LayoutComponentsFilter extends Filter {
|
|||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param searchTerm This class contains information related to search terms.
|
||||
* The {@code toString()} method of this class overrides the search term.
|
||||
* @param endpoint Endpoint related with the search term.
|
||||
* For search history, this value is:
|
||||
* '/complete/deleteitems?client=youtube-android-pb&delq=${searchTerm}&deltok=${token}'.
|
||||
* For search suggestions, this value is null or empty.
|
||||
* @return Whether search term is a search history or not.
|
||||
* @param searchTerm This class contains information related to search terms.
|
||||
* The {@code toString()} method of this class overrides the search term.
|
||||
* @param endpoint Endpoint related with the search term.
|
||||
* For search history, this value is:
|
||||
* '/complete/deleteitems?client=youtube-android-pb&delq=${searchTerm}&deltok=${token}'.
|
||||
* For search suggestions, this value is null or empty.
|
||||
* @return Whether search term is a search history or not.
|
||||
*/
|
||||
public static boolean isSearchHistory(Object searchTerm, String endpoint) {
|
||||
boolean isSearchHistory = endpoint != null && endpoint.contains("/delete");
|
||||
|
|
|
|||
|
|
@ -1,26 +1,22 @@
|
|||
package app.revanced.extension.youtube.patches.litho;
|
||||
|
||||
import static app.revanced.extension.youtube.patches.LayoutReloadObserverPatch.isActionBarVisible;
|
||||
import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.patches.litho.Filter;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.*;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroup.ByteArrayFilterGroup;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroupList.ByteArrayFilterGroupList;
|
||||
import app.revanced.extension.shared.patches.litho.LithoFilterPatch;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.patches.litho.FilterGroupList.StringFilterGroupList;
|
||||
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.patches.VersionCheckPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.EngagementPanel;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
|
|
@ -34,16 +30,6 @@ public final class ShortsFilter extends Filter {
|
|||
"reel_action_bar.", // Regular Shorts.
|
||||
"reels_player_overlay_layout." // Shorts ads.
|
||||
};
|
||||
private static final Map<Integer, BooleanSetting> REEL_ACTION_BUTTONS_MAP = new HashMap<>() {
|
||||
{
|
||||
// Like button and Dislike button can be hidden with Litho filter.
|
||||
// put(0, Settings.HIDE_SHORTS_LIKE_BUTTON);
|
||||
// put(1, Settings.HIDE_SHORTS_DISLIKE_BUTTON);
|
||||
put(2, Settings.HIDE_SHORTS_COMMENTS_BUTTON);
|
||||
put(3, Settings.HIDE_SHORTS_SHARE_BUTTON);
|
||||
put(4, Settings.HIDE_SHORTS_REMIX_BUTTON);
|
||||
}
|
||||
};
|
||||
private final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.e";
|
||||
|
||||
/**
|
||||
|
|
@ -90,8 +76,8 @@ public final class ShortsFilter extends Filter {
|
|||
private final ByteArrayFilterGroupList suggestedActionsBuffer = new ByteArrayFilterGroupList();
|
||||
|
||||
private final StringFilterGroup shortsActionBar;
|
||||
private final StringFilterGroup videoActionButton;
|
||||
private final ByteArrayFilterGroupList videoActionButtonBuffer = new ByteArrayFilterGroupList();
|
||||
private final StringFilterGroup shortsActionButton;
|
||||
private final StringFilterGroupList shortsActionButtonGroupList = new StringFilterGroupList();
|
||||
|
||||
public ShortsFilter() {
|
||||
//
|
||||
|
|
@ -289,7 +275,7 @@ public final class ShortsFilter extends Filter {
|
|||
"yt_outline_template_add_"
|
||||
);
|
||||
|
||||
videoActionButton = new StringFilterGroup(
|
||||
shortsActionButton = new StringFilterGroup(
|
||||
null,
|
||||
// Can be any of:
|
||||
// button.eml
|
||||
|
|
@ -308,36 +294,26 @@ public final class ShortsFilter extends Filter {
|
|||
shortsCompactFeedVideo, shelfHeaderPath, joinButton, subscribeButton, paidPromotionLabel,
|
||||
livePreview, suggestedAction, pausedOverlayButtons, channelBar, infoPanel, previewComment,
|
||||
autoDubbedLabel, fullVideoLinkLabel, videoTitle, useSoundButton, soundButton, stickers,
|
||||
reelCarousel, reelSoundMetadata, likeFountain, likeButton, dislikeButton
|
||||
reelCarousel, reelSoundMetadata, likeFountain, likeButton, dislikeButton, shortsActionBar
|
||||
);
|
||||
|
||||
// Legacy hiding of Shorts action buttons. Because of 20.31+ buffer changes
|
||||
// it's currently not possible to hide these using buffer filtering.
|
||||
// See alternative hiding strategy in hideActionButtons().
|
||||
if (!VersionCheckPatch.IS_20_22_OR_GREATER) {
|
||||
addPathCallbacks(shortsActionBar);
|
||||
|
||||
//
|
||||
// All other action buttons.
|
||||
//
|
||||
videoActionButtonBuffer.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
|
||||
"reel_comment_button",
|
||||
"youtube_shorts_comment_outline"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SHARE_BUTTON,
|
||||
"reel_share_button",
|
||||
"youtube_shorts_share_outline"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_REMIX_BUTTON,
|
||||
"reel_remix_button",
|
||||
"youtube_shorts_remix_outline"
|
||||
)
|
||||
);
|
||||
}
|
||||
//
|
||||
// All other action buttons.
|
||||
//
|
||||
shortsActionButtonGroupList.addAll(
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
|
||||
"id.reel_comment_button"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_SHARE_BUTTON,
|
||||
"id.reel_share_button"
|
||||
),
|
||||
new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_REMIX_BUTTON,
|
||||
"id.reel_remix_button"
|
||||
)
|
||||
);
|
||||
|
||||
//
|
||||
// Suggested actions.
|
||||
|
|
@ -482,8 +458,10 @@ public final class ShortsFilter extends Filter {
|
|||
// Video action buttons (comment, share, remix) have the same path.
|
||||
// Like and dislike are separate path filters and don't require buffer searching.
|
||||
if (matchedGroup == shortsActionBar) {
|
||||
return videoActionButton.check(path).isFiltered()
|
||||
&& videoActionButtonBuffer.check(buffer).isFiltered();
|
||||
if (shortsActionButton.check(path).isFiltered()) {
|
||||
return shortsActionButtonGroupList.check(accessibility).isFiltered();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (matchedGroup == suggestedAction) {
|
||||
|
|
@ -525,7 +503,7 @@ public final class ShortsFilter extends Filter {
|
|||
}
|
||||
|
||||
// Must check player type first, as search bar can be active behind the player.
|
||||
if (PlayerType.getCurrent().isMaximizedOrFullscreen()) {
|
||||
if (PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get()) {
|
||||
return EngagementPanel.isDescription()
|
||||
? hideVideoDescription // Player video description panel opened.
|
||||
: hideHome; // For now, consider Shorts under video player the same as the home feed.
|
||||
|
|
@ -555,58 +533,6 @@ public final class ShortsFilter extends Filter {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* <p>
|
||||
* Hide action buttons by index.
|
||||
* <p>
|
||||
* Regular video action buttons vary in order by video, country, and account.
|
||||
* Therefore, hiding buttons by index may hide unintended buttons.
|
||||
* <p>
|
||||
* Shorts action buttons are almost always in the same order.
|
||||
* (From top to bottom: Like, Dislike, Comment, Share, Remix).
|
||||
* Therefore, we can hide Shorts action buttons by index.
|
||||
*
|
||||
* @param pathBuilder Same as pathBuilder used in {@link LithoFilterPatch}.
|
||||
* @param treeNodeResultList List containing Litho components.
|
||||
*/
|
||||
public static void hideActionButtons(StringBuilder pathBuilder, List<Object> treeNodeResultList) {
|
||||
try {
|
||||
if (pathBuilder == null || pathBuilder.length() == 0 || treeNodeResultList == null) {
|
||||
return;
|
||||
}
|
||||
int size = treeNodeResultList.size();
|
||||
|
||||
// The minimum size of the target List is 4.
|
||||
if (size < 4) {
|
||||
return;
|
||||
}
|
||||
String path = pathBuilder.toString();
|
||||
|
||||
if (!Utils.containsAny(path, REEL_ACTION_BAR_PATHS)
|
||||
// Regular Shorts: [ComponentType, ComponentType, ComponentType, ComponentType, ComponentType]
|
||||
// Shorts ads: [ComponentType, ComponentType, ComponentType, ComponentType] (No Remix button)
|
||||
|| !COMPONENT_TYPE.equals(treeNodeResultList.get(0).toString())) {
|
||||
return;
|
||||
}
|
||||
// Removing elements without iterating through the list in reverse order will throw an exception.
|
||||
for (int i = size - 1; i > -1; i--) {
|
||||
// treeNodeResult is each button.
|
||||
Object treeNodeResult = treeNodeResultList.get(i);
|
||||
if (treeNodeResult != null) {
|
||||
BooleanSetting setting = REEL_ACTION_BUTTONS_MAP.get(i);
|
||||
if (setting != null && setting.get()) {
|
||||
int finalI = i;
|
||||
Logger.printDebug(() -> "Hiding action button by index: " + finalI + ", key: " + setting.key);
|
||||
treeNodeResultList.remove(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "hideActionButtons failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -147,7 +147,6 @@ public class Settings extends YouTubeAndMusicSettings {
|
|||
public static final BooleanSetting HIDE_CHANNEL_TAB = new BooleanSetting("revanced_hide_channel_tab", FALSE);
|
||||
public static final StringSetting HIDE_CHANNEL_TAB_FILTER_STRINGS = new StringSetting("revanced_hide_channel_tab_filter_strings", "", true, parent(HIDE_CHANNEL_TAB));
|
||||
public static final BooleanSetting HIDE_COMMUNITY_BUTTON = new BooleanSetting("revanced_hide_community_button", TRUE);
|
||||
public static final BooleanSetting HIDE_FOR_YOU_SHELF = new BooleanSetting("revanced_hide_for_you_shelf", FALSE);
|
||||
public static final BooleanSetting HIDE_JOIN_BUTTON = new BooleanSetting("revanced_hide_join_button", FALSE);
|
||||
public static final BooleanSetting HIDE_LINKS_PREVIEW = new BooleanSetting("revanced_hide_links_preview", TRUE);
|
||||
public static final BooleanSetting HIDE_MEMBERS_SHELF = new BooleanSetting("revanced_hide_members_shelf", TRUE);
|
||||
|
|
|
|||
|
|
@ -138,5 +138,6 @@ enum class PlayerType {
|
|||
|
||||
fun isMaximizedOrFullscreen(): Boolean {
|
||||
return this == WATCH_WHILE_MAXIMIZED || this == WATCH_WHILE_FULLSCREEN
|
||||
|| this == WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue