1
mirror of https://github.com/revanced/revanced-integrations synced 2024-11-22 05:02:19 +01:00

feat(YouTube): Support versions 19.25 and 19.34 (#689)

Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
Co-authored-by: kitadai31 <90122968+kitadai31@users.noreply.github.com>
This commit is contained in:
Zain 2024-10-19 19:26:27 +07:00 committed by GitHub
parent 99d17bf026
commit 61569ba111
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 989 additions and 521 deletions

View File

@ -1,10 +1,7 @@
package app.revanced.integrations.shared;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.*;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
@ -268,6 +265,20 @@ public class Utils {
boolean matches(T object);
}
/**
* Includes sub children.
*
* @noinspection unchecked
*/
public static <R extends View> R getChildViewByResourceName(@NonNull View view, @NonNull String str) {
var child = view.findViewById(Utils.getResourceIdentifier(str, "id"));
if (child != null) {
return (R) child;
}
throw new IllegalArgumentException("View with resource name '" + str + "' not found");
}
/**
* @param searchRecursively If children ViewGroups should also be
* recursively searched using depth first search.
@ -710,4 +721,21 @@ public class Utils {
pref.setOrder(order);
}
}
/**
* If {@link Fragment} uses [Android library] rather than [AndroidX library],
* the Dialog theme corresponding to [Android library] should be used.
* <p>
* If not, the following issues will occur:
* <a href="https://github.com/ReVanced/revanced-patches/issues/3061">ReVanced/revanced-patches#3061</a>
* <p>
* To prevent these issues, apply the Dialog theme corresponding to [Android library].
*/
public static void setEditTextDialogTheme(AlertDialog.Builder builder) {
final int editTextDialogStyle = getResourceIdentifier(
"revanced_edit_text_dialog_style", "style");
if (editTextDialogStyle != 0) {
builder.getContext().setTheme(editTextDialogStyle);
}
}
}

View File

@ -11,6 +11,7 @@ import androidx.annotation.Nullable;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.shared.settings.BooleanSetting;
import app.revanced.integrations.shared.settings.Setting;
@ -141,8 +142,13 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
} else if (pref.hasKey()) {
String key = pref.getKey();
Setting<?> setting = Setting.getSettingFromPath(key);
if (setting != null) {
updatePreference(pref, setting, syncSettingValue, applySettingToPreference);
} else if (BaseSettings.DEBUG.get() && (pref instanceof SwitchPreference
|| pref instanceof EditTextPreference || pref instanceof ListPreference)) {
// Probably a typo in the patches preference declaration.
Logger.printException(() -> "Preference key has no setting: " + key);
}
}
}

View File

@ -66,6 +66,8 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try {
Utils.setEditTextDialogTheme(builder);
// Show the user the settings in JSON format.
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
Utils.setClipboard(getEditText().getText().toString());

View File

@ -7,6 +7,8 @@ import android.preference.EditTextPreference;
import android.util.AttributeSet;
import android.widget.Button;
import android.widget.EditText;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.shared.Logger;
@ -33,6 +35,8 @@ public class ResettableEditTextPreference extends EditTextPreference {
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
super.onPrepareDialogBuilder(builder);
Utils.setEditTextDialogTheme(builder);
Setting<?> setting = Setting.getSettingFromPath(getKey());
if (setting != null) {
builder.setNeutralButton(str("revanced_settings_reset"), null);

View File

@ -8,7 +8,9 @@ public class BackgroundPlaybackPatch {
/**
* Injection point.
*/
public static boolean playbackIsNotShort() {
public static boolean allowBackgroundPlayback(boolean original) {
if (original) return true;
// Steps to verify most edge cases:
// 1. Open a regular video
// 2. Minimize app (PIP should appear)

View File

@ -1,21 +1,129 @@
package app.revanced.integrations.youtube.patches;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class ChangeStartPagePatch {
public static void changeIntent(final Intent intent) {
final var startPage = Settings.START_PAGE.get();
if (startPage.isEmpty()) return;
Logger.printDebug(() -> "Changing start page to " + startPage);
public enum StartPage {
/**
* Unmodified type, and same as un-patched.
*/
ORIGINAL("", null),
if (startPage.startsWith("www"))
intent.setData(Uri.parse(startPage));
else
intent.setAction("com.google.android.youtube.action." + startPage);
/**
* Browse id.
*/
BROWSE("FEguide_builder", TRUE),
EXPLORE("FEexplore", TRUE),
HISTORY("FEhistory", TRUE),
LIBRARY("FElibrary", TRUE),
MOVIE("FEstorefront", TRUE),
SUBSCRIPTIONS("FEsubscriptions", TRUE),
TRENDING("FEtrending", TRUE),
/**
* Channel id, this can be used as a browseId.
*/
GAMING("UCOpNcN46UbXVtpKMrmU4Abg", TRUE),
LIVE("UC4R8DWoMoI7CAwX8_LjQHig", TRUE),
MUSIC("UC-9-kyTW8ZkZNDHQJ6FgpwQ", TRUE),
SPORTS("UCEgdi0XIXXZ-qJOFPf4JSKw", TRUE),
/**
* Playlist id, this can be used as a browseId.
*/
LIKED_VIDEO("VLLL", TRUE),
WATCH_LATER("VLWL", TRUE),
/**
* Intent action.
*/
SEARCH("com.google.android.youtube.action.open.search", FALSE),
SHORTS("com.google.android.youtube.action.open.shorts", FALSE);
@Nullable
final Boolean isBrowseId;
@NonNull
final String id;
StartPage(@NonNull String id, @Nullable Boolean isBrowseId) {
this.id = id;
this.isBrowseId = isBrowseId;
}
private boolean isBrowseId() {
return TRUE.equals(isBrowseId);
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private boolean isIntentAction() {
return FALSE.equals(isBrowseId);
}
}
/**
* Intent action when YouTube is cold started from the launcher.
* <p>
* If you don't check this, the hooking will also apply in the following cases:
* Case 1. The user clicked Shorts button on the YouTube shortcut.
* Case 2. The user clicked Shorts button on the YouTube widget.
* In this case, instead of opening Shorts, the start page specified by the user is opened.
*/
private static final String ACTION_MAIN = "android.intent.action.MAIN";
private static final StartPage START_PAGE = Settings.CHANGE_START_PAGE.get();
/**
* There is an issue where the back button on the toolbar doesn't work properly.
* As a workaround for this issue, instead of overriding the browserId multiple times, just override it once.
*/
private static boolean appLaunched = false;
public static String overrideBrowseId(@NonNull String original) {
if (!START_PAGE.isBrowseId()) {
return original;
}
if (appLaunched) {
Logger.printDebug(() -> "Ignore override browseId as the app already launched");
return original;
}
appLaunched = true;
Logger.printDebug(() -> "Changing browseId to " + START_PAGE.id);
return START_PAGE.id;
}
public static void overrideIntentAction(@NonNull Intent intent) {
if (!START_PAGE.isIntentAction()) {
return;
}
if (!ACTION_MAIN.equals(intent.getAction())) {
Logger.printDebug(() -> "Ignore override intent action" +
" as the current activity is not the entry point of the application");
return;
}
if (appLaunched) {
Logger.printDebug(() -> "Ignore override intent action as the app already launched");
return;
}
appLaunched = true;
final String intentAction = START_PAGE.id;
Logger.printDebug(() -> "Changing intent action to " + intentAction);
intent.setAction(intentAction);
}
}

View File

@ -1,17 +1,47 @@
package app.revanced.integrations.youtube.patches;
import android.view.View;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class HidePlayerButtonsPatch {
private static final boolean HIDE_PLAYER_BUTTONS_ENABLED = Settings.HIDE_PLAYER_BUTTONS.get();
private static final int PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID =
Utils.getResourceIdentifier("player_control_previous_button_touch_area", "id");
private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID =
Utils.getResourceIdentifier("player_control_next_button_touch_area", "id");
/**
* Injection point.
*/
public static boolean previousOrNextButtonIsVisible(boolean previousOrNextButtonVisible) {
if (Settings.HIDE_PLAYER_BUTTONS.get()) {
return false;
public static void hidePreviousNextButtons(View parentView) {
if (!HIDE_PLAYER_BUTTONS_ENABLED) {
return;
}
return previousOrNextButtonVisible;
// Must use a deferred call to main thread to hide the button.
// Otherwise the layout crashes if set to hidden now.
Utils.runOnMainThread(() -> {
hideView(parentView, PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID);
hideView(parentView, PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID);
});
}
private static void hideView(View parentView, int resourceId) {
View nextPreviousButton = parentView.findViewById(resourceId);
if (nextPreviousButton == null) {
Logger.printException(() -> "Could not find player previous/next button");
return;
}
Logger.printDebug(() -> "Hiding previous/next button");
Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton);
}
}

View File

@ -2,20 +2,22 @@ package app.revanced.integrations.youtube.patches;
import static app.revanced.integrations.shared.StringRef.str;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*;
import static app.revanced.integrations.youtube.patches.VersionCheckPatch.*;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.Nullable;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.shared.settings.Setting;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
@SuppressWarnings({"unused", "SpellCheckingInspection"})
public final class MiniplayerPatch {
/**
@ -28,7 +30,12 @@ public final class MiniplayerPatch {
TABLET(true, null),
MODERN_1(null, 1),
MODERN_2(null, 2),
MODERN_3(null, 3);
MODERN_3(null, 3),
/**
* Half broken miniplayer, that might be work in progress or left over abandoned code.
* Can force this type by editing the import/export settings.
*/
MODERN_4(null, 4);
/**
* Legacy tablet hook value.
@ -52,6 +59,35 @@ public final class MiniplayerPatch {
}
}
private static final int MINIPLAYER_SIZE;
static {
// YT appears to use the device screen dip width, plus an unknown fixed horizontal padding size.
DisplayMetrics displayMetrics = Utils.getContext().getResources().getDisplayMetrics();
final int deviceDipWidth = (int) (displayMetrics.widthPixels / displayMetrics.density);
// YT seems to use a minimum height to calculate the minimum miniplayer width based on the video.
// 170 seems to be the smallest that can be used and using less makes no difference.
final int WIDTH_DIP_MIN = 170; // Seems to be the smallest that works.
final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding.
// Round down to the nearest 5 pixels, to keep any error toasts easier to read.
final int WIDTH_DIP_MAX = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5);
Logger.printDebug(() -> "Screen dip width: " + deviceDipWidth + " maxWidth: " + WIDTH_DIP_MAX);
int dipWidth = Settings.MINIPLAYER_WIDTH_DIP.get();
if (dipWidth < WIDTH_DIP_MIN || dipWidth > WIDTH_DIP_MAX) {
Utils.showToastLong(str("revanced_miniplayer_width_dip_invalid_toast",
WIDTH_DIP_MIN, WIDTH_DIP_MAX));
// Instead of resetting, clamp the size at the bounds.
dipWidth = Math.max(WIDTH_DIP_MIN, Math.min(dipWidth, WIDTH_DIP_MAX));
Settings.MINIPLAYER_WIDTH_DIP.save(dipWidth);
}
MINIPLAYER_SIZE = dipWidth;
}
/**
* Modern subtitle overlay for {@link MiniplayerType#MODERN_2}.
* Resource is not present in older targets, and this field will be zero.
@ -61,8 +97,21 @@ public final class MiniplayerPatch {
private static final MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get();
/**
* Cannot turn off double tap with modern 2 or 3 with later targets,
* as forcing it off breakings tapping the miniplayer.
*/
private static final boolean DOUBLE_TAP_ACTION_ENABLED =
// 19.29+ is very broken if double tap is not enabled.
IS_19_29_OR_GREATER ||
(CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get());
private static final boolean DRAG_AND_DROP_ENABLED =
CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get();
private static final boolean HIDE_EXPAND_CLOSE_ENABLED =
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get();
Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get()
&& Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.isAvailable();
private static final boolean HIDE_SUBTEXT_ENABLED =
(CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get();
@ -70,8 +119,29 @@ public final class MiniplayerPatch {
private static final boolean HIDE_REWIND_FORWARD_ENABLED =
CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get();
private static final boolean MINIPLAYER_ROUNDED_CORNERS_ENABLED =
Settings.MINIPLAYER_ROUNDED_CORNERS.get();
/**
* Remove a broken and always present subtitle text that is only
* present with {@link MiniplayerType#MODERN_2}. Bug was fixed in 19.21.
*/
private static final boolean HIDE_BROKEN_MODERN_2_SUBTITLE =
CURRENT_TYPE == MODERN_2 && !IS_19_21_OR_GREATER;
private static final int OPACITY_LEVEL;
public static final class MiniplayerHideExpandCloseAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
MiniplayerType type = Settings.MINIPLAYER_TYPE.get();
return (!IS_19_20_OR_GREATER && (type == MODERN_1 || type == MODERN_3))
|| (!IS_19_26_OR_GREATER && type == MODERN_1
&& !Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() && !Settings.MINIPLAYER_DRAG_AND_DROP.get())
|| (IS_19_29_OR_GREATER && type == MODERN_3);
}
}
static {
int opacity = Settings.MINIPLAYER_OPACITY.get();
@ -122,6 +192,90 @@ public final class MiniplayerPatch {
}
}
/**
* Injection point.
*/
public static boolean getModernFeatureFlagsActiveOverride(boolean original) {
if (original) Logger.printDebug(() -> "getModernFeatureFlagsActiveOverride original: " + original);
if (CURRENT_TYPE == ORIGINAL) {
return original;
}
return CURRENT_TYPE.isModern();
}
/**
* Injection point.
*/
public static boolean enableMiniplayerDoubleTapAction(boolean original) {
if (original) Logger.printDebug(() -> "enableMiniplayerDoubleTapAction original: " + true);
if (CURRENT_TYPE == ORIGINAL) {
return original;
}
return DOUBLE_TAP_ACTION_ENABLED;
}
/**
* Injection point.
*/
public static boolean enableMiniplayerDragAndDrop(boolean original) {
if (original) Logger.printDebug(() -> "enableMiniplayerDragAndDrop original: " + true);
if (CURRENT_TYPE == ORIGINAL) {
return original;
}
return DRAG_AND_DROP_ENABLED;
}
/**
* Injection point.
*/
public static boolean setRoundedCorners(boolean original) {
if (original) Logger.printDebug(() -> "setRoundedCorners original: " + true);
if (CURRENT_TYPE.isModern()) {
return MINIPLAYER_ROUNDED_CORNERS_ENABLED;
}
return original;
}
/**
* Injection point.
*/
public static int setMiniplayerDefaultSize(int original) {
if (CURRENT_TYPE.isModern()) {
return MINIPLAYER_SIZE;
}
return original;
}
/**
* Injection point.
*/
public static float setMovementBoundFactor(float original) {
// Not clear if customizing this is useful or not.
// So for now just log this and use the original value.
if (original != 1.0) Logger.printDebug(() -> "setMovementBoundFactor original: " + original);
return original;
}
/**
* Injection point.
*/
public static boolean setDropShadow(boolean original) {
if (original) Logger.printDebug(() -> "setViewElevation original: " + true);
return original;
}
/**
* Injection point.
*/
@ -140,27 +294,35 @@ public final class MiniplayerPatch {
* Injection point.
*/
public static void hideMiniplayerSubTexts(View view) {
// Different subviews are passed in, but only TextView and layouts are of interest here.
final boolean hideView = HIDE_SUBTEXT_ENABLED && (view instanceof TextView || view instanceof LinearLayout);
Utils.hideViewByRemovingFromParentUnderCondition(hideView, view);
try {
// Different subviews are passed in, but only TextView is of interest here.
if (HIDE_SUBTEXT_ENABLED && view instanceof TextView) {
Logger.printDebug(() -> "Hiding subtext view");
Utils.hideViewByRemovingFromParentUnderCondition(true, view);
}
} catch (Exception ex) {
Logger.printException(() -> "hideMiniplayerSubTexts failure", ex);
}
}
/**
* Injection point.
*/
public static void playerOverlayGroupCreated(View group) {
// Modern 2 has an half broken subtitle that is always present.
// Always hide it to make the miniplayer mostly usable.
if (CURRENT_TYPE == MODERN_2 && MODERN_OVERLAY_SUBTITLE_TEXT != 0) {
if (group instanceof ViewGroup) {
View subtitleText = Utils.getChildView((ViewGroup) group, true,
view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT);
try {
if (HIDE_BROKEN_MODERN_2_SUBTITLE && MODERN_OVERLAY_SUBTITLE_TEXT != 0) {
if (group instanceof ViewGroup) {
View subtitleText = Utils.getChildView((ViewGroup) group, true,
view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT);
if (subtitleText != null) {
subtitleText.setVisibility(View.GONE);
Logger.printDebug(() -> "Modern overlay subtitle view set to hidden");
if (subtitleText != null) {
subtitleText.setVisibility(View.GONE);
Logger.printDebug(() -> "Modern overlay subtitle view set to hidden");
}
}
}
} catch (Exception ex) {
Logger.printException(() -> "playerOverlayGroupCreated failure", ex);
}
}
}

View File

@ -699,10 +699,12 @@ public class ReturnYouTubeDislikePatch {
if (!Settings.RYD_ENABLED.get()) {
return;
}
final boolean isNoneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized();
if (isNoneHiddenOrMinimized && !Settings.RYD_SHORTS.get()) {
return;
}
ReturnYouTubeDislike videoData = currentVideoData;
if (videoData == null) {
Logger.printDebug(() -> "Cannot send vote, as current video data is null");
@ -723,6 +725,7 @@ public class ReturnYouTubeDislikePatch {
return;
}
}
Logger.printException(() -> "Unknown vote type: " + vote);
} catch (Exception ex) {
Logger.printException(() -> "sendVote failure", ex);

View File

@ -4,7 +4,11 @@ import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class SlideToSeekPatch {
public static boolean isSlideToSeekDisabled() {
return !Settings.SLIDE_TO_SEEK.get();
private static final Boolean SLIDE_TO_SEEK_DISABLED = !Settings.SLIDE_TO_SEEK.get();
public static boolean isSlideToSeekDisabled(boolean isDisabled) {
if (!isDisabled) return isDisabled;
return SLIDE_TO_SEEK_DISABLED;
}
}

View File

@ -0,0 +1,10 @@
package app.revanced.integrations.youtube.patches;
import app.revanced.integrations.shared.Utils;
public class VersionCheckPatch {
public static final boolean IS_19_20_OR_GREATER = Utils.getAppVersionName().compareTo("19.20.00") >= 0;
public static final boolean IS_19_21_OR_GREATER = Utils.getAppVersionName().compareTo("19.21.00") >= 0;
public static final boolean IS_19_26_OR_GREATER = Utils.getAppVersionName().compareTo("19.26.00") >= 0;
public static final boolean IS_19_29_OR_GREATER = Utils.getAppVersionName().compareTo("19.29.00") >= 0;
}

View File

@ -18,7 +18,7 @@ public final class VideoInformation {
public interface PlaybackController {
// Methods are added to YT classes during patching.
boolean seekTo(long videoTime);
boolean seekToRelative(long videoTimeOffset);
void seekToRelative(long videoTimeOffset);
}
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
@ -229,21 +229,19 @@ public final class VideoInformation {
/**
* Seeks a relative amount. Should always be used over {@link #seekTo(long)}
* when the desired seek time is an offset of the current time.
*
* @noinspection UnusedReturnValue
*/
public static boolean seekToRelative(long seekTime) {
public static void seekToRelative(long seekTime) {
Utils.verifyOnMainThread();
try {
Logger.printDebug(() -> "Seeking relative to: " + seekTime);
// Try regular playback controller first, and it will not succeed if casting.
// 19.39+ does not have a boolean return type for relative seek.
// But can call both methods and it works correctly for both situations.
PlaybackController controller = playerControllerRef.get();
if (controller == null) {
Logger.printDebug(() -> "Cannot seek relative as player controller is null");
} else {
if (controller.seekToRelative(seekTime)) return true;
Logger.printDebug(() -> "seekToRelative did not succeeded. Trying MXD.");
controller.seekToRelative(seekTime);
}
// Adjust the fine adjustment function so it's at least 1 second before/after.
@ -258,13 +256,11 @@ public final class VideoInformation {
controller = mdxPlayerDirectorRef.get();
if (controller == null) {
Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null");
return false;
} else {
controller.seekToRelative(adjustedSeekTime);
}
return controller.seekToRelative(adjustedSeekTime);
} catch (Exception ex) {
Logger.printException(() -> "Failed to seek relative", ex);
return false;
}
}

View File

@ -0,0 +1,90 @@
package app.revanced.integrations.youtube.patches.components;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.settings.BaseSettings;
/**
* Filters litho based components.
*
* Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
* and {@link #addPathCallbacks(StringFilterGroup...)}.
*
* To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to
* either an identifier or a path.
* Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
* or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern).
*
* All callbacks must be registered before the constructor completes.
*/
abstract class Filter {
public enum FilterContentType {
IDENTIFIER,
PATH,
PROTOBUFFER
}
/**
* Identifier callbacks. Do not add to this instance,
* and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}.
*/
protected final List<StringFilterGroup> identifierCallbacks = new ArrayList<>();
/**
* Path callbacks. Do not add to this instance,
* and instead use {@link #addPathCallbacks(StringFilterGroup...)}.
*/
protected final List<StringFilterGroup> pathCallbacks = new ArrayList<>();
/**
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* if any of the groups are found.
*/
protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
identifierCallbacks.addAll(Arrays.asList(groups));
}
/**
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* if any of the groups are found.
*/
protected final void addPathCallbacks(StringFilterGroup... groups) {
pathCallbacks.addAll(Arrays.asList(groups));
}
/**
* Called after an enabled filter has been matched.
* Default implementation is to always filter the matched component and log the action.
* Subclasses can perform additional or different checks if needed.
* <p>
* If the content is to be filtered, subclasses should always
* call this method (and never return a plain 'true').
* That way the logs will always show when a component was filtered and which filter hide it.
* <p>
* Method is called off the main thread.
*
* @param matchedGroup The actual filter that matched.
* @param contentType The type of content matched.
* @param contentIndex Matched index of the identifier or path.
* @return True if the litho component should be filtered out.
*/
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (BaseSettings.DEBUG.get()) {
String filterSimpleName = getClass().getSimpleName();
if (contentType == FilterContentType.IDENTIFIER) {
Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier);
} else {
Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path);
}
}
return true;
}
}

View File

@ -0,0 +1,214 @@
package app.revanced.integrations.youtube.patches.components;
import androidx.annotation.NonNull;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.settings.BooleanSetting;
import app.revanced.integrations.youtube.ByteTrieSearch;
abstract class FilterGroup<T> {
final static class FilterGroupResult {
private BooleanSetting setting;
private int matchedIndex;
private int matchedLength;
// In the future it might be useful to include which pattern matched,
// but for now that is not needed.
FilterGroupResult() {
this(null, -1, 0);
}
FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
setValues(setting, matchedIndex, matchedLength);
}
public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
this.setting = setting;
this.matchedIndex = matchedIndex;
this.matchedLength = matchedLength;
}
/**
* A null value if the group has no setting,
* or if no match is returned from {@link FilterGroupList#check(Object)}.
*/
public BooleanSetting getSetting() {
return setting;
}
public boolean isFiltered() {
return matchedIndex >= 0;
}
/**
* Matched index of first pattern that matched, or -1 if nothing matched.
*/
public int getMatchedIndex() {
return matchedIndex;
}
/**
* Length of the matched filter pattern.
*/
public int getMatchedLength() {
return matchedLength;
}
}
protected final BooleanSetting setting;
protected final T[] filters;
/**
* Initialize a new filter group.
*
* @param setting The associated setting.
* @param filters The filters.
*/
@SafeVarargs
public FilterGroup(final BooleanSetting setting, final T... filters) {
this.setting = setting;
this.filters = filters;
if (filters.length == 0) {
throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
}
}
public boolean isEnabled() {
return setting == null || setting.get();
}
/**
* @return If {@link FilterGroupList} should include this group when searching.
* By default, all filters are included except non enabled settings that require reboot.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean includeInSearch() {
return isEnabled() || !setting.rebootApp;
}
@NonNull
@Override
public String toString() {
return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
}
public abstract FilterGroupResult check(final T stack);
}
class StringFilterGroup extends FilterGroup<String> {
public StringFilterGroup(final BooleanSetting setting, final String... filters) {
super(setting, filters);
}
@Override
public FilterGroupResult check(final String string) {
int matchedIndex = -1;
int matchedLength = 0;
if (isEnabled()) {
for (String pattern : filters) {
if (!string.isEmpty()) {
final int indexOf = string.indexOf(pattern);
if (indexOf >= 0) {
matchedIndex = indexOf;
matchedLength = pattern.length();
break;
}
}
}
}
return new FilterGroupResult(setting, matchedIndex, matchedLength);
}
}
/**
* If you have more than 1 filter patterns, then all instances of
* this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])},
* which uses a prefix tree to give better performance.
*/
class ByteArrayFilterGroup extends FilterGroup<byte[]> {
private volatile int[][] failurePatterns;
// Modified implementation from https://stackoverflow.com/a/1507813
private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) {
// Finds the first occurrence of the pattern in the byte array using
// KMP matching algorithm.
int patternLength = pattern.length;
for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) {
while (j > 0 && pattern[j] != data[i]) {
j = failure[j - 1];
}
if (pattern[j] == data[i]) {
j++;
}
if (j == patternLength) {
return i - patternLength + 1;
}
}
return -1;
}
private static int[] createFailurePattern(byte[] pattern) {
// Computes the failure function using a boot-strapping process,
// where the pattern is matched against itself.
final int patternLength = pattern.length;
final int[] failure = new int[patternLength];
for (int i = 1, j = 0; i < patternLength; i++) {
while (j > 0 && pattern[j] != pattern[i]) {
j = failure[j - 1];
}
if (pattern[j] == pattern[i]) {
j++;
}
failure[i] = j;
}
return failure;
}
public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) {
super(setting, filters);
}
/**
* Converts the Strings into byte arrays. Used to search for text in binary data.
*/
public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
super(setting, ByteTrieSearch.convertStringsToBytes(filters));
}
private synchronized void buildFailurePatterns() {
if (failurePatterns != null) return; // Thread race and another thread already initialized the search.
Logger.printDebug(() -> "Building failure array for: " + this);
int[][] failurePatterns = new int[filters.length][];
int i = 0;
for (byte[] pattern : filters) {
failurePatterns[i++] = createFailurePattern(pattern);
}
this.failurePatterns = failurePatterns; // Must set after initialization finishes.
}
@Override
public FilterGroupResult check(final byte[] bytes) {
int matchedLength = 0;
int matchedIndex = -1;
if (isEnabled()) {
int[][] failures = failurePatterns;
if (failures == null) {
buildFailurePatterns(); // Lazy load.
failures = failurePatterns;
}
for (int i = 0, length = filters.length; i < length; i++) {
byte[] filter = filters[i];
matchedIndex = indexOf(bytes, filter, failures[i]);
if (matchedIndex >= 0) {
matchedLength = filter.length;
break;
}
}
}
return new FilterGroupResult(setting, matchedIndex, matchedLength);
}
}

View File

@ -0,0 +1,85 @@
package app.revanced.integrations.youtube.patches.components;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import java.util.*;
import java.util.function.Consumer;
import app.revanced.integrations.youtube.ByteTrieSearch;
import app.revanced.integrations.youtube.StringTrieSearch;
import app.revanced.integrations.youtube.TrieSearch;
abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
private final List<T> filterGroups = new ArrayList<>();
private final TrieSearch<V> search = createSearchGraph();
@SafeVarargs
protected final void addAll(final T... groups) {
filterGroups.addAll(Arrays.asList(groups));
for (T group : groups) {
if (!group.includeInSearch()) {
continue;
}
for (V pattern : group.filters) {
search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
if (group.isEnabled()) {
FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
result.setValues(group.setting, matchedStartIndex, matchedLength);
return true;
}
return false;
});
}
}
}
@NonNull
@Override
public Iterator<T> iterator() {
return filterGroups.iterator();
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void forEach(@NonNull Consumer<? super T> action) {
filterGroups.forEach(action);
}
@RequiresApi(api = Build.VERSION_CODES.N)
@NonNull
@Override
public Spliterator<T> spliterator() {
return filterGroups.spliterator();
}
protected FilterGroup.FilterGroupResult check(V stack) {
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
search.matches(stack, result);
return result;
}
protected abstract TrieSearch<V> createSearchGraph();
}
final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
protected StringTrieSearch createSearchGraph() {
return new StringTrieSearch();
}
}
/**
* If searching for a single byte pattern, then it is slightly better to use
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
* than a prefix tree to search for only 1 pattern.
*/
final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
protected ByteTrieSearch createSearchGraph() {
return new ByteTrieSearch();
}
}

View File

@ -423,7 +423,6 @@ public final class LayoutComponentsFilter extends Filter {
// 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();
return NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY;
}
}

View File

@ -1,389 +1,15 @@
package app.revanced.integrations.youtube.patches.components;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Spliterator;
import java.util.function.Consumer;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.settings.BooleanSetting;
import app.revanced.integrations.shared.settings.BaseSettings;
import app.revanced.integrations.youtube.ByteTrieSearch;
import app.revanced.integrations.youtube.StringTrieSearch;
import app.revanced.integrations.youtube.TrieSearch;
import app.revanced.integrations.youtube.settings.Settings;
abstract class FilterGroup<T> {
final static class FilterGroupResult {
private BooleanSetting setting;
private int matchedIndex;
private int matchedLength;
// In the future it might be useful to include which pattern matched,
// but for now that is not needed.
FilterGroupResult() {
this(null, -1, 0);
}
FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) {
setValues(setting, matchedIndex, matchedLength);
}
public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) {
this.setting = setting;
this.matchedIndex = matchedIndex;
this.matchedLength = matchedLength;
}
/**
* A null value if the group has no setting,
* or if no match is returned from {@link FilterGroupList#check(Object)}.
*/
public BooleanSetting getSetting() {
return setting;
}
public boolean isFiltered() {
return matchedIndex >= 0;
}
/**
* Matched index of first pattern that matched, or -1 if nothing matched.
*/
public int getMatchedIndex() {
return matchedIndex;
}
/**
* Length of the matched filter pattern.
*/
public int getMatchedLength() {
return matchedLength;
}
}
protected final BooleanSetting setting;
protected final T[] filters;
/**
* Initialize a new filter group.
*
* @param setting The associated setting.
* @param filters The filters.
*/
@SafeVarargs
public FilterGroup(final BooleanSetting setting, final T... filters) {
this.setting = setting;
this.filters = filters;
if (filters.length == 0) {
throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)");
}
}
public boolean isEnabled() {
return setting == null || setting.get();
}
/**
* @return If {@link FilterGroupList} should include this group when searching.
* By default, all filters are included except non enabled settings that require reboot.
*/
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
public boolean includeInSearch() {
return isEnabled() || !setting.rebootApp;
}
@NonNull
@Override
public String toString() {
return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting);
}
public abstract FilterGroupResult check(final T stack);
}
class StringFilterGroup extends FilterGroup<String> {
public StringFilterGroup(final BooleanSetting setting, final String... filters) {
super(setting, filters);
}
@Override
public FilterGroupResult check(final String string) {
int matchedIndex = -1;
int matchedLength = 0;
if (isEnabled()) {
for (String pattern : filters) {
if (!string.isEmpty()) {
final int indexOf = string.indexOf(pattern);
if (indexOf >= 0) {
matchedIndex = indexOf;
matchedLength = pattern.length();
break;
}
}
}
}
return new FilterGroupResult(setting, matchedIndex, matchedLength);
}
}
/**
* If you have more than 1 filter patterns, then all instances of
* this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])},
* which uses a prefix tree to give better performance.
*/
class ByteArrayFilterGroup extends FilterGroup<byte[]> {
private volatile int[][] failurePatterns;
// Modified implementation from https://stackoverflow.com/a/1507813
private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) {
// Finds the first occurrence of the pattern in the byte array using
// KMP matching algorithm.
int patternLength = pattern.length;
for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) {
while (j > 0 && pattern[j] != data[i]) {
j = failure[j - 1];
}
if (pattern[j] == data[i]) {
j++;
}
if (j == patternLength) {
return i - patternLength + 1;
}
}
return -1;
}
private static int[] createFailurePattern(byte[] pattern) {
// Computes the failure function using a boot-strapping process,
// where the pattern is matched against itself.
final int patternLength = pattern.length;
final int[] failure = new int[patternLength];
for (int i = 1, j = 0; i < patternLength; i++) {
while (j > 0 && pattern[j] != pattern[i]) {
j = failure[j - 1];
}
if (pattern[j] == pattern[i]) {
j++;
}
failure[i] = j;
}
return failure;
}
public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) {
super(setting, filters);
}
/**
* Converts the Strings into byte arrays. Used to search for text in binary data.
*/
public ByteArrayFilterGroup(BooleanSetting setting, String... filters) {
super(setting, ByteTrieSearch.convertStringsToBytes(filters));
}
private synchronized void buildFailurePatterns() {
if (failurePatterns != null) return; // Thread race and another thread already initialized the search.
Logger.printDebug(() -> "Building failure array for: " + this);
int[][] failurePatterns = new int[filters.length][];
int i = 0;
for (byte[] pattern : filters) {
failurePatterns[i++] = createFailurePattern(pattern);
}
this.failurePatterns = failurePatterns; // Must set after initialization finishes.
}
@Override
public FilterGroupResult check(final byte[] bytes) {
int matchedLength = 0;
int matchedIndex = -1;
if (isEnabled()) {
int[][] failures = failurePatterns;
if (failures == null) {
buildFailurePatterns(); // Lazy load.
failures = failurePatterns;
}
for (int i = 0, length = filters.length; i < length; i++) {
byte[] filter = filters[i];
matchedIndex = indexOf(bytes, filter, failures[i]);
if (matchedIndex >= 0) {
matchedLength = filter.length;
break;
}
}
}
return new FilterGroupResult(setting, matchedIndex, matchedLength);
}
}
abstract class FilterGroupList<V, T extends FilterGroup<V>> implements Iterable<T> {
private final List<T> filterGroups = new ArrayList<>();
private final TrieSearch<V> search = createSearchGraph();
@SafeVarargs
protected final void addAll(final T... groups) {
filterGroups.addAll(Arrays.asList(groups));
for (T group : groups) {
if (!group.includeInSearch()) {
continue;
}
for (V pattern : group.filters) {
search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
if (group.isEnabled()) {
FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter;
result.setValues(group.setting, matchedStartIndex, matchedLength);
return true;
}
return false;
});
}
}
}
@NonNull
@Override
public Iterator<T> iterator() {
return filterGroups.iterator();
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void forEach(@NonNull Consumer<? super T> action) {
filterGroups.forEach(action);
}
@RequiresApi(api = Build.VERSION_CODES.N)
@NonNull
@Override
public Spliterator<T> spliterator() {
return filterGroups.spliterator();
}
protected FilterGroup.FilterGroupResult check(V stack) {
FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult();
search.matches(stack, result);
return result;
}
protected abstract TrieSearch<V> createSearchGraph();
}
final class StringFilterGroupList extends FilterGroupList<String, StringFilterGroup> {
protected StringTrieSearch createSearchGraph() {
return new StringTrieSearch();
}
}
/**
* If searching for a single byte pattern, then it is slightly better to use
* {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster
* than a prefix tree to search for only 1 pattern.
*/
final class ByteArrayFilterGroupList extends FilterGroupList<byte[], ByteArrayFilterGroup> {
protected ByteTrieSearch createSearchGraph() {
return new ByteTrieSearch();
}
}
/**
* Filters litho based components.
*
* Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)}
* and {@link #addPathCallbacks(StringFilterGroup...)}.
*
* To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to
* either an identifier or a path.
* Then inside {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern)
* or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern).
*
* All callbacks must be registered before the constructor completes.
*/
abstract class Filter {
public enum FilterContentType {
IDENTIFIER,
PATH,
PROTOBUFFER
}
/**
* Identifier callbacks. Do not add to this instance,
* and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}.
*/
protected final List<StringFilterGroup> identifierCallbacks = new ArrayList<>();
/**
* Path callbacks. Do not add to this instance,
* and instead use {@link #addPathCallbacks(StringFilterGroup...)}.
*/
protected final List<StringFilterGroup> pathCallbacks = new ArrayList<>();
/**
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* if any of the groups are found.
*/
protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
identifierCallbacks.addAll(Arrays.asList(groups));
}
/**
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* if any of the groups are found.
*/
protected final void addPathCallbacks(StringFilterGroup... groups) {
pathCallbacks.addAll(Arrays.asList(groups));
}
/**
* Called after an enabled filter has been matched.
* Default implementation is to always filter the matched component and log the action.
* Subclasses can perform additional or different checks if needed.
* <p>
* If the content is to be filtered, subclasses should always
* call this method (and never return a plain 'true').
* That way the logs will always show when a component was filtered and which filter hide it.
* <p>
* Method is called off the main thread.
*
* @param matchedGroup The actual filter that matched.
* @param contentType The type of content matched.
* @param contentIndex Matched index of the identifier or path.
* @return True if the litho component should be filtered out.
*/
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (BaseSettings.DEBUG.get()) {
String filterSimpleName = getClass().getSimpleName();
if (contentType == FilterContentType.IDENTIFIER) {
Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier);
} else {
Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path);
}
}
return true;
}
}
/**
* Placeholder for actual filters.
*/
final class DummyFilter extends Filter { }
@SuppressWarnings("unused")
public final class LithoFilterPatch {
/**
@ -520,9 +146,9 @@ public final class LithoFilterPatch {
@SuppressWarnings("unused")
public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) {
try {
// It is assumed that protobufBuffer is empty as well in this case.
if (pathBuilder.length() == 0)
if (pathBuilder.length() == 0) {
return false;
}
ByteBuffer protobufBuffer = bufferThreadLocal.get();
final byte[] bufferArray;
@ -542,14 +168,22 @@ public final class LithoFilterPatch {
pathBuilder.toString(), bufferArray);
Logger.printDebug(() -> "Searching " + parameter);
if (parameter.identifier != null) {
if (identifierSearchTree.matches(parameter.identifier, parameter)) return true;
if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) {
return true;
}
if (pathSearchTree.matches(parameter.path, parameter)) {
return true;
}
if (pathSearchTree.matches(parameter.path, parameter)) return true;
} catch (Exception ex) {
Logger.printException(() -> "Litho filter failure", ex);
}
return false;
}
}
}
/**
* Placeholder for actual filters.
*/
final class DummyFilter extends Filter { }

View File

@ -9,6 +9,11 @@ import androidx.annotation.Nullable;
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.List;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.settings.Settings;
import app.revanced.integrations.youtube.shared.NavigationBar;
@ -16,14 +21,26 @@ import app.revanced.integrations.youtube.shared.PlayerType;
@SuppressWarnings("unused")
public final class ShortsFilter extends Filter {
public static PivotBar pivotBar; // Set by patch.
public static final Boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get();
private final static String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml";
/**
* For paid promotion label and subscribe button that appears in the channel bar.
*/
private final static String REEL_METAPANEL_PATH = "reel_metapanel.eml";
/**
* Tags that appears when opening the Shorts player.
*/
private static final List<String> REEL_WATCH_FRAGMENT_INIT_PLAYBACK = Arrays.asList("r_fs", "r_ts");
/**
* Vertical padding between the bottom of the screen and the seekbar, when the Shorts navigation bar is hidden.
*/
public static final int HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT = 100;
private static WeakReference<PivotBar> pivotBarRef = new WeakReference<>(null);
private final StringFilterGroup shortsCompactFeedVideoPath;
private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer;
@ -241,9 +258,7 @@ public final class ShortsFilter extends Filter {
if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
// Selectively filter to avoid false positive filtering of other subscribe/join buttons.
if (path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH)) {
return super.isFiltered(
identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex
);
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
@ -258,9 +273,7 @@ public final class ShortsFilter extends Filter {
// Video action buttons (like, dislike, comment, share, remix) have the same path.
if (matchedGroup == actionBar) {
if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(
identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex
);
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
@ -268,9 +281,7 @@ public final class ShortsFilter extends Filter {
if (matchedGroup == suggestedAction) {
// Suggested actions can be at the start or in the middle of a path.
if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(
identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex
);
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
@ -343,6 +354,14 @@ public final class ShortsFilter extends Filter {
}
}
public static int getSoundButtonSize(int original) {
if (Settings.HIDE_SHORTS_SOUND_BUTTON.get()) {
return 0;
}
return original;
}
// region Hide the buttons in older versions of YouTube. New versions use Litho.
public static void hideLikeButton(final View likeButtonView) {
@ -374,17 +393,30 @@ public final class ShortsFilter extends Filter {
// endregion
public static void hideNavigationBar() {
if (!Settings.HIDE_SHORTS_NAVIGATION_BAR.get()) return;
if (pivotBar == null) return;
pivotBar.setVisibility(View.GONE);
public static void setNavigationBar(PivotBar view) {
Logger.printDebug(() -> "Setting navigation bar");
pivotBarRef = new WeakReference<>(view);
}
public static View hideNavigationBar(final View navigationBarView) {
if (Settings.HIDE_SHORTS_NAVIGATION_BAR.get())
return null; // Hides the navigation bar.
public static void hideNavigationBar(String tag) {
if (HIDE_SHORTS_NAVIGATION_BAR) {
if (REEL_WATCH_FRAGMENT_INIT_PLAYBACK.contains(tag)) {
var pivotBar = pivotBarRef.get();
if (pivotBar == null) return;
return navigationBarView;
Logger.printDebug(() -> "Hiding navbar by setting to GONE");
pivotBar.setVisibility(View.GONE);
} else {
Logger.printDebug(() -> "Ignoring tag: " + tag);
}
}
}
public static int getNavigationBarHeight(int original) {
if (HIDE_SHORTS_NAVIGATION_BAR) {
return HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT;
}
return original;
}
}

View File

@ -4,20 +4,32 @@ import static app.revanced.integrations.shared.StringRef.str;
import android.graphics.Color;
import app.revanced.integrations.youtube.settings.Settings;
import java.util.Arrays;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
import app.revanced.integrations.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class SeekbarColorPatch {
private static final boolean USE_SEEKBAR_CUSTOM_COLOR = Settings.SEEKBAR_CUSTOM_COLOR.get();
private static final boolean SEEKBAR_CUSTOM_COLOR_ENABLED = Settings.SEEKBAR_CUSTOM_COLOR.get();
/**
* Default color of the seekbar.
*/
private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000;
/**
* Default colors of the gradient seekbar.
*/
private static final int[] ORIGINAL_SEEKBAR_GRADIENT_COLORS = { 0xFFFF0033, 0xFFFF2791 };
/**
* Default positions of the gradient seekbar.
*/
private static final float[] ORIGINAL_SEEKBAR_GRADIENT_POSITIONS = { 0.8f, 1.0f };
/**
* Default YouTube seekbar color brightness.
*/
@ -40,7 +52,7 @@ public final class SeekbarColorPatch {
Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv);
ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2];
if (USE_SEEKBAR_CUSTOM_COLOR) {
if (SEEKBAR_CUSTOM_COLOR_ENABLED) {
loadCustomSeekbarColor();
}
}
@ -60,6 +72,14 @@ public final class SeekbarColorPatch {
return seekbarColor;
}
public static boolean playerSeekbarGradientEnabled(boolean original) {
if (original) {
Logger.printDebug(() -> "playerSeekbarGradientEnabled original: " + true);
if (SEEKBAR_CUSTOM_COLOR_ENABLED) return false;
}
return original;
}
/**
* Injection point.
@ -74,17 +94,42 @@ public final class SeekbarColorPatch {
if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) {
return 0x00000000;
}
return getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR);
}
return colorValue;
}
/**
* Injection point.
*/
public static void setLinearGradient(int[] colors, float[] positions) {
if (SEEKBAR_CUSTOM_COLOR_ENABLED) {
// Most litho usage of linear gradients is hooked here,
// so must only change if the values are those for the seekbar.
if (Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_COLORS, colors)
&& Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_POSITIONS, positions)) {
Arrays.fill(colors, Settings.HIDE_SEEKBAR_THUMBNAIL.get()
? 0x00000000
: seekbarColor);
return;
}
Logger.printDebug(() -> "Ignoring gradient colors: " + Arrays.toString(colors)
+ " positions: " + Arrays.toString(positions));
}
}
/**
* Injection point.
*
* Overrides color when video player seekbar is clicked.
*/
public static int getVideoPlayerSeekbarClickedColor(int colorValue) {
if (!SEEKBAR_CUSTOM_COLOR_ENABLED) {
return colorValue;
}
return colorValue == ORIGINAL_SEEKBAR_COLOR
? getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR)
: colorValue;
@ -96,6 +141,10 @@ public final class SeekbarColorPatch {
* Overrides color used for the video player seekbar.
*/
public static int getVideoPlayerSeekbarColor(int originalColor) {
if (!SEEKBAR_CUSTOM_COLOR_ENABLED) {
return originalColor;
}
return getSeekbarColorValue(originalColor);
}
@ -105,9 +154,10 @@ public final class SeekbarColorPatch {
*/
private static int getSeekbarColorValue(int originalColor) {
try {
if (!USE_SEEKBAR_CUSTOM_COLOR || originalColor == seekbarColor) {
if (!SEEKBAR_CUSTOM_COLOR_ENABLED || originalColor == seekbarColor) {
return originalColor; // nothing to do
}
final int alphaDifference = Color.alpha(originalColor) - Color.alpha(ORIGINAL_SEEKBAR_COLOR);
// The seekbar uses the same color but different brightness for different situations.
@ -131,11 +181,13 @@ public final class SeekbarColorPatch {
}
}
static int clamp(int value, int lower, int upper) {
/** @noinspection SameParameterValue */
private static int clamp(int value, int lower, int upper) {
return Math.max(lower, Math.min(value, upper));
}
static float clamp(float value, float lower, float upper) {
/** @noinspection SameParameterValue */
private static float clamp(float value, float lower, float upper) {
return Math.max(lower, Math.min(value, upper));
}
}

View File

@ -44,7 +44,7 @@ public class Requester {
String line;
while ((line = reader.readLine()) != null) {
jsonBuilder.append(line);
jsonBuilder.append("\n");
jsonBuilder.append('\n');
}
return jsonBuilder.toString();
}

View File

@ -585,8 +585,13 @@ public class ReturnYouTubeDislike {
public void sendVote(@NonNull Vote vote) {
Utils.verifyOnMainThread();
Objects.requireNonNull(vote);
try {
if (isShort != PlayerType.getCurrent().isNoneOrHidden()) {
PlayerType currentType = PlayerType.getCurrent();
if (isShort != currentType.isNoneHiddenOrMinimized()) {
Logger.printDebug(() -> "Cannot vote for video: " + videoId
+ " as current player type does not match: " + currentType);
// Shorts was loaded with regular video present, then Shorts was closed.
// and then user voted on the now visible original video.
// Cannot send a vote, because this instance is for the wrong video.

View File

@ -1,5 +1,18 @@
package app.revanced.integrations.youtube.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.integrations.shared.settings.Setting.*;
import static app.revanced.integrations.youtube.patches.ChangeStartPagePatch.StartPage;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerHideExpandCloseAvailability;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.*;
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.settings.*;
import app.revanced.integrations.shared.settings.preference.SharedPrefCategory;
@ -12,18 +25,6 @@ import app.revanced.integrations.youtube.patches.spoof.SpoofAppVersionPatch;
import app.revanced.integrations.youtube.patches.spoof.SpoofVideoStreamsPatch;
import app.revanced.integrations.youtube.sponsorblock.SponsorBlockSettings;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import static app.revanced.integrations.shared.settings.Setting.*;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_1;
import static app.revanced.integrations.youtube.patches.MiniplayerPatch.MiniplayerType.MODERN_3;
import static app.revanced.integrations.youtube.sponsorblock.objects.CategoryBehaviour.*;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
@SuppressWarnings("deprecation")
public class Settings extends BaseSettings {
// Video
@ -130,16 +131,21 @@ public class Settings extends BaseSettings {
public static final BooleanSetting DISABLE_LIKE_SUBSCRIBE_GLOW = new BooleanSetting("revanced_disable_like_subscribe_glow", FALSE);
public static final BooleanSetting HIDE_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_autoplay_button", TRUE, true);
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE, true);
public static final BooleanSetting HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE);
public static final BooleanSetting HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE, true);
public static final BooleanSetting COPY_VIDEO_URL = new BooleanSetting("revanced_copy_video_url", FALSE);
public static final BooleanSetting COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_copy_video_url_timestamp", TRUE);
public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE);
// Miniplayer
public static final EnumSetting<MiniplayerType> MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.ORIGINAL, true);
public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3));
private static final Availability MINIPLAYER_ANY_MODERN = MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3, MODERN_4);
public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, MINIPLAYER_ANY_MODERN);
public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, MINIPLAYER_ANY_MODERN);
public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, new MiniplayerHideExpandCloseAvailability());
public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3));
public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1));
public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, MINIPLAYER_ANY_MODERN);
public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_ANY_MODERN);
public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1));
// External downloader
@ -188,7 +194,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_VIDEO_QUALITY_MENU_FOOTER = new BooleanSetting("revanced_hide_video_quality_menu_footer", FALSE);
// General layout
public static final StringSetting START_PAGE = new StringSetting("revanced_start_page", "");
public static final EnumSetting<StartPage> CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.ORIGINAL, true);
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message");
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "17.33.42", true, parent(SPOOF_APP_VERSION));
public static final BooleanSetting TABLET_LAYOUT = new BooleanSetting("revanced_tablet_layout", FALSE, true, "revanced_tablet_layout_user_dialog_message");
@ -239,12 +245,12 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE);
public static final BooleanSetting HIDE_SHORTS_SOUND_METADATA_LABEL = new BooleanSetting("revanced_hide_shorts_sound_metadata_label", FALSE);
public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", FALSE);
public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", TRUE, true);
public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", FALSE, true);
// Seekbar
public static final BooleanSetting DISABLE_PRECISE_SEEKING_GESTURE = new BooleanSetting("revanced_disable_precise_seeking_gesture", TRUE);
public static final BooleanSetting SEEKBAR_TAPPING = new BooleanSetting("revanced_seekbar_tapping", TRUE);
public static final BooleanSetting SLIDE_TO_SEEK = new BooleanSetting("revanced_slide_to_seek", FALSE);
public static final BooleanSetting SLIDE_TO_SEEK = new BooleanSetting("revanced_slide_to_seek", FALSE, true);
public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails", TRUE);
public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true);
public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE);

View File

@ -381,6 +381,8 @@ public class SponsorBlockPreferenceFragment extends PreferenceFragment {
importExport = new EditTextPreference(context) {
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
Utils.setEditTextDialogTheme(builder);
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
Utils.setClipboard(getEditText().getText().toString());
});

View File

@ -8,6 +8,8 @@ import android.view.View;
import androidx.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.CountDownLatch;
@ -156,11 +158,11 @@ public final class NavigationBar {
try {
String lastEnumName = lastYTNavigationEnumName;
for (NavigationButton button : NavigationButton.values()) {
if (button.ytEnumName.equals(lastEnumName)) {
for (NavigationButton buttonType : NavigationButton.values()) {
if (buttonType.ytEnumNames.contains(lastEnumName)) {
Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName);
viewToButtonMap.put(navigationButtonGroup, button);
navigationTabCreatedCallback(button, navigationButtonGroup);
viewToButtonMap.put(navigationButtonGroup, buttonType);
navigationTabCreatedCallback(buttonType, navigationButtonGroup);
return;
}
}
@ -184,10 +186,10 @@ public final class NavigationBar {
public static void navigationImageResourceTabLoaded(View view) {
// 'You' tab has no YT enum name and the enum hook is not called for it.
// Compare the last enum to figure out which tab this actually is.
if (CREATE.ytEnumName.equals(lastYTNavigationEnumName)) {
if (CREATE.ytEnumNames.contains(lastYTNavigationEnumName)) {
navigationTabLoaded(view);
} else {
lastYTNavigationEnumName = NavigationButton.LIBRARY_YOU.ytEnumName;
lastYTNavigationEnumName = NavigationButton.LIBRARY.ytEnumNames.get(0);
navigationTabLoaded(view);
}
}
@ -237,44 +239,39 @@ public final class NavigationBar {
}
public enum NavigationButton {
HOME("PIVOT_HOME"),
SHORTS("TAB_SHORTS"),
HOME("PIVOT_HOME", "TAB_HOME_CAIRO"),
SHORTS("TAB_SHORTS", "TAB_SHORTS_CAIRO"),
/**
* Create new video tab.
* This tab will never be in a selected state, even if the create video UI is on screen.
*/
CREATE("CREATION_TAB_LARGE"),
SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS"),
CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"),
SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"),
/**
* Notifications tab. Only present when
* {@link Settings#SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON} is active.
*/
NOTIFICATIONS("TAB_ACTIVITY"),
NOTIFICATIONS("TAB_ACTIVITY", "TAB_ACTIVITY_CAIRO"),
/**
* Library tab when the user is not logged in.
* Library tab, including if the user is in incognito mode or when logged out.
*/
LIBRARY_LOGGED_OUT("ACCOUNT_CIRCLE"),
/**
* User is logged in with incognito mode enabled.
*/
LIBRARY_INCOGNITO("INCOGNITO_CIRCLE"),
/**
* Old library tab (pre 'You' layout), only present when version spoofing.
*/
LIBRARY_OLD_UI("VIDEO_LIBRARY_WHITE"),
/**
* 'You' library tab that is sometimes momentarily loaded.
* When this is loaded, {@link #LIBRARY_YOU} is also present.
*
* This might be a temporary tab while the user profile photo is loading,
* but its exact purpose is not entirely clear.
*/
LIBRARY_PIVOT_UNKNOWN("PIVOT_LIBRARY"),
/**
* Modern library tab with 'You' layout.
*/
// The hooked YT code does not use an enum, and a dummy name is used here.
LIBRARY_YOU("YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME");
LIBRARY(
// Modern library tab with 'You' layout.
// The hooked YT code does not use an enum, and a dummy name is used here.
"YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME",
// User is logged out.
"ACCOUNT_CIRCLE",
"ACCOUNT_CIRCLE_CAIRO",
// User is logged in with incognito mode enabled.
"INCOGNITO_CIRCLE",
"INCOGNITO_CAIRO",
// Old library tab (pre 'You' layout), only present when version spoofing.
"VIDEO_LIBRARY_WHITE",
// 'You' library tab that is sometimes momentarily loaded.
// This might be a temporary tab while the user profile photo is loading,
// but its exact purpose is not entirely clear.
"PIVOT_LIBRARY"
);
@Nullable
private static volatile NavigationButton selectedNavigationButton;
@ -303,16 +300,10 @@ public final class NavigationBar {
/**
* YouTube enum name for this tab.
*/
private final String ytEnumName;
private final List<String> ytEnumNames;
NavigationButton(String ytEnumName) {
this.ytEnumName = ytEnumName;
}
public boolean isLibraryOrYouTab() {
return this == LIBRARY_YOU || this == LIBRARY_PIVOT_UNKNOWN
|| this == LIBRARY_OLD_UI || this == LIBRARY_INCOGNITO
|| this == LIBRARY_LOGGED_OUT;
NavigationButton(String... ytEnumNames) {
this.ytEnumNames = Arrays.asList(ytEnumNames);
}
}
}

View File

@ -21,6 +21,7 @@ import java.util.Objects;
import app.revanced.integrations.shared.Logger;
import app.revanced.integrations.shared.Utils;
@SuppressWarnings("deprecation")
public class SegmentCategoryListPreference extends ListPreference {
private final SegmentCategory category;
private EditText mEditText;
@ -45,6 +46,8 @@ public class SegmentCategoryListPreference extends ListPreference {
@Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try {
Utils.setEditTextDialogTheme(builder);
Context context = builder.getContext();
TableLayout table = new TableLayout(context);
table.setOrientation(LinearLayout.HORIZONTAL);

View File

@ -25,8 +25,8 @@ public class CreateSegmentButtonController {
public static void initialize(View youtubeControlsLayout) {
try {
Logger.printDebug(() -> "initializing new segment button");
ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById(
getResourceIdentifier("revanced_sb_create_segment_button", "id")));
ImageView imageView = Objects.requireNonNull(Utils.getChildViewByResourceName(
youtubeControlsLayout, "revanced_sb_create_segment_button"));
imageView.setVisibility(View.GONE);
imageView.setOnClickListener(v -> SponsorBlockViewController.toggleNewSegmentLayoutVisibility());

View File

@ -27,8 +27,8 @@ public class VotingButtonController {
public static void initialize(View youtubeControlsLayout) {
try {
Logger.printDebug(() -> "initializing voting button");
ImageView imageView = Objects.requireNonNull(youtubeControlsLayout.findViewById(
getResourceIdentifier("revanced_sb_voting_button", "id")));
ImageView imageView = Objects.requireNonNull(Utils.getChildViewByResourceName(
youtubeControlsLayout, "revanced_sb_voting_button"));
imageView.setVisibility(View.GONE);
imageView.setOnClickListener(v -> SponsorBlockUtils.onVotingClicked(v.getContext()));