diff --git a/app/src/main/java/app/revanced/integrations/shared/Utils.java b/app/src/main/java/app/revanced/integrations/shared/Utils.java
index 0192c571..c00886a3 100644
--- a/app/src/main/java/app/revanced/integrations/shared/Utils.java
+++ b/app/src/main/java/app/revanced/integrations/shared/Utils.java
@@ -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);
+        }
+    }
 }
diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java
index b7fa68ac..e6d07ccd 100644
--- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java
+++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/AbstractPreferenceFragment.java
@@ -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);
                 }
             }
         }
diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java
index 5c8e7c9b..1b86c3cd 100644
--- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java
+++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ImportExportPreference.java
@@ -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());
diff --git a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java
index 4cf1f277..b62331fe 100644
--- a/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java
+++ b/app/src/main/java/app/revanced/integrations/shared/settings/preference/ResettableEditTextPreference.java
@@ -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);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java
index e6e449a0..d84813d0 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/BackgroundPlaybackPatch.java
@@ -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)
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java
index f31af61b..d87dca10 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/ChangeStartPagePatch.java
@@ -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);
     }
 }
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java
index aa501280..bc876cc3 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/HidePlayerButtonsPatch.java
@@ -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);
     }
 }
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java
index 22ea9f10..9af9fd5e 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/MiniplayerPatch.java
@@ -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);
         }
     }
 }
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java
index ec52ef07..3a47ef52 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/ReturnYouTubeDislikePatch.java
@@ -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);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java
index 5a6f5629..7d6b2090 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/SlideToSeekPatch.java
@@ -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;
     }
 }
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/VersionCheckPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/VersionCheckPatch.java
new file mode 100644
index 00000000..4ede0101
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/VersionCheckPatch.java
@@ -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;
+}
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java b/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java
index 0d7a29aa..9df12b57 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/VideoInformation.java
@@ -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;
         }
     }
 
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/Filter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/Filter.java
new file mode 100644
index 00000000..ed1c56c9
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/Filter.java
@@ -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;
+    }
+}
+
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroup.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroup.java
new file mode 100644
index 00000000..5cc10159
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroup.java
@@ -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);
+    }
+}
+
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java
new file mode 100644
index 00000000..9babba0e
--- /dev/null
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/FilterGroupList.java
@@ -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();
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java
index 9e3ed7db..d0026ecb 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LayoutComponentsFilter.java
@@ -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;
     }
 }
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java
index a7c99720..5664ea8d 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/LithoFilterPatch.java
@@ -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;
     }
-}
\ No newline at end of file
+}
+
+/**
+ * Placeholder for actual filters.
+ */
+final class DummyFilter extends Filter { }
\ No newline at end of file
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java
index 191b6ae4..f48d10cf 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/components/ShortsFilter.java
@@ -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;
     }
 }
diff --git a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java
index 17caa5f6..2e97a27d 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/patches/theme/SeekbarColorPatch.java
@@ -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));
     }
 }
diff --git a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java
index c62e34f5..d4c8e532 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/requests/Requester.java
@@ -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();
         }
diff --git a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
index b63d0484..8b97077c 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
@@ -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.
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
index 78a89f6a..85af3e46 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/Settings.java
@@ -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);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java
index 2de654a2..7b009319 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/settings/preference/SponsorBlockPreferenceFragment.java
@@ -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());
                 });
diff --git a/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java b/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java
index 5123e78d..4c220d09 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/shared/NavigationBar.java
@@ -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);
         }
     }
 }
diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java
index 09235e85..61c40c05 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/objects/SegmentCategoryListPreference.java
@@ -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);
diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java
index 9b5c7376..48c97e8e 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/CreateSegmentButtonController.java
@@ -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());
 
diff --git a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java
index 52a4660d..02d3e279 100644
--- a/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java
+++ b/app/src/main/java/app/revanced/integrations/youtube/sponsorblock/ui/VotingButtonController.java
@@ -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()));