diff --git a/README.md b/README.md index 4443db281..6273da940 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm | `Ambient mode control` | Adds options to disable Ambient mode and to bypass Ambient mode restrictions. | 18.29.38 ~ 19.44.39 | | `Bypass URL redirects` | Adds an option to bypass URL redirects and open the original URL directly. | 18.29.38 ~ 19.44.39 | | `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 18.29.38 ~ 19.44.39 | -| `Change layout` | Adds an option to change the dp in order to use a tablet or phone layout. | 18.29.38 ~ 19.44.39 | +| `Change form factor` | Adds an option to change the UI appearance to a phone, tablet, or automotive device. | 18.29.38 ~ 19.44.39 | | `Change live ring click action` | Adds an option to open the channel instead of the live stream when clicking on the live ring. | 18.29.38 ~ 19.44.39 | | `Change player flyout menu toggles` | Adds an option to use text toggles instead of switch toggles within the additional settings menu. | 18.29.38 ~ 19.44.39 | | `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 18.29.38 ~ 19.44.39 | @@ -84,48 +84,49 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm | 💊 Patch | 📜 Description | 🏹 Target Version | |:--------:|:--------------:|:-----------------:| -| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 8.05.51 | -| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 8.05.51 | -| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 8.05.51 | -| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 6.20.51 ~ 8.05.51 | -| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 8.05.51 | -| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 8.05.51 | -| `Custom branding name for YouTube Music` | Changes the YouTube Music app name to the name specified in patch options. | 6.20.51 ~ 8.05.51 | -| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 8.05.51 | -| `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 6.20.51 ~ 8.05.51 | -| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.05.51 | -| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 8.05.51 | -| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 6.20.51 ~ 8.05.51 | -| `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 8.05.51 | -| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 8.05.51 | -| `Disable music video in album` | Adds option to redirect music videos from albums for non-premium users. | 6.20.51 ~ 8.05.51 | -| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 6.20.51 ~ 8.05.51 | -| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 8.05.51 | -| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 8.05.51 | -| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 8.05.51 | -| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 6.20.51 ~ 8.05.51 | -| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.05.51 | -| `Hide action bar components` | Adds options to hide action bar components and replace the offline download button with an external download button. | 6.20.51 ~ 8.05.51 | -| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.05.51 | -| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 8.05.51 | -| `Hide overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 8.05.51 | -| `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 8.05.51 | -| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 6.20.51 ~ 8.05.51 | -| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.05.51 | -| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 8.05.51 | -| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 6.20.51 ~ 8.05.51 | -| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 6.20.51 ~ 8.05.51 | -| `Return YouTube Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 8.05.51 | -| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 8.05.51 | -| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 6.20.51 ~ 8.05.51 | -| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 6.20.51 ~ 8.05.51 | -| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 8.05.51 | +| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 8.10.51 | +| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 8.10.51 | +| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 8.10.51 | +| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 6.20.51 ~ 8.10.51 | +| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 8.10.51 | +| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 8.10.51 | +| `Custom branding name for YouTube Music` | Changes the YouTube Music app name to the name specified in patch options. | 6.20.51 ~ 8.10.51 | +| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 8.10.51 | +| `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 6.20.51 ~ 8.10.51 | +| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.10.51 | +| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 8.10.51 | +| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 6.20.51 ~ 8.10.51 | +| `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 8.10.51 | +| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 8.10.51 | +| `Disable music video in album` | Adds option to redirect music videos from albums for non-premium users. | 6.20.51 ~ 8.10.51 | +| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 6.20.51 ~ 8.10.51 | +| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 8.10.51 | +| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 8.10.51 | +| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 8.10.51 | +| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 6.20.51 ~ 8.10.51 | +| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.10.51 | +| `Hide action bar components` | Adds options to hide action bar components and replace the offline download button with an external download button. | 6.20.51 ~ 8.10.51 | +| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.10.51 | +| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 8.10.51 | +| `Hide overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 8.10.51 | +| `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 8.10.51 | +| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 6.20.51 ~ 8.10.51 | +| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.10.51 | +| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 8.10.51 | +| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 6.20.51 ~ 8.10.51 | +| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 6.20.51 ~ 8.10.51 | +| `Return YouTube Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 8.10.51 | +| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 8.10.51 | +| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 6.20.51 ~ 8.10.51 | +| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 6.20.51 ~ 8.10.51 | +| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 8.10.51 | | `Spoof app version` | Adds options to spoof the YouTube Music client version. This can be used to restore old UI elements and features. | 6.51.53 ~ 7.16.53 | -| `Spoof client` | Adds options to spoof the client to allow playback. | 6.20.51 ~ 8.05.51 | -| `Translations for YouTube Music` | Add translations or remove string resources. | 6.20.51 ~ 8.05.51 | -| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 6.20.51 ~ 8.05.51 | -| `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 8.05.51 | -| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 6.20.51 ~ 8.05.51 | +| `Spoof client` | Adds options to spoof the client to allow playback. | 6.20.51 ~ 8.10.51 | +| `Spoof player parameter` | Adds options to spoof player parameter to allow playback. | 6.20.51 ~ 8.10.51 | +| `Translations for YouTube Music` | Add translations or remove string resources. | 6.20.51 ~ 8.10.51 | +| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 6.20.51 ~ 8.10.51 | +| `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 8.10.51 | +| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 6.20.51 ~ 8.10.51 | ### [📦 `com.reddit.frontpage`](https://play.google.com/store/apps/details?id=com.reddit.frontpage) @@ -187,7 +188,8 @@ Example: "6.51.53", "7.16.53", "7.25.53", - "8.05.51" + "8.05.51", + "8.10.51" ] }, "options": [] diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/ChangeStartPagePatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/ChangeStartPagePatch.java index d837c8827..d4775a3d9 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/ChangeStartPagePatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/ChangeStartPagePatch.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import org.apache.commons.lang3.StringUtils; import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.utils.ExtendedUtils; import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.ResourceUtils; @@ -40,27 +41,17 @@ public final class ChangeStartPagePatch { /** * Intent extra. */ - SEARCH("", 1, "Eh4IBRDTnQEYmgMiEwiZn+H0r5WLAxVV5OcDHcHRBmPqpd25AQA="); + SEARCH(""); @NonNull final String browseId; - final int shortcutType; - - /** - * Unique identifier for shortcut (Base64). - */ - @NonNull - final String shortcutId; - StartPage(@NonNull String browseId) { - this(browseId, 0, ""); + this.browseId = browseId; } - StartPage(@NonNull String browseId, int shortcutType, @NonNull String shortcutId) { - this.browseId = browseId; - this.shortcutType = shortcutType; - this.shortcutId = shortcutId; + public final String getBrowseId() { + return this.browseId; } } @@ -69,12 +60,6 @@ public final class ChangeStartPagePatch { */ private static final String ACTION_MAIN = "android.intent.action.MAIN"; - private static final String SHORTCUT_ACTION = "com.google.android.youtube.music.action.shortcut"; - - private static final String SHORTCUT_CLASS_DESCRIPTOR = "com.google.android.apps.youtube.music.activities.InternalMusicActivity"; - - private static final String SHORTCUT_TYPE = "com.google.android.youtube.music.action.shortcut_type"; - private static final StartPage START_PAGE = Settings.CHANGE_START_PAGE.get(); public static String overrideBrowseId(@NonNull String browseId) { @@ -96,20 +81,13 @@ public final class ChangeStartPagePatch { " as the current activity is not the entry point of the application"); return; } - final String overrideShortcutId = START_PAGE.shortcutId; - if (overrideShortcutId.isEmpty()) { + if (START_PAGE != StartPage.SEARCH) { return; } Activity mActivity = ResourceUtils.getActivity(); - if (mActivity == null) { - return; + if (mActivity != null) { + Logger.printDebug(() -> "Changing intent action to " + START_PAGE.name()); + ExtendedUtils.setSearchIntent(mActivity, intent); } - - Logger.printDebug(() -> "Changing intent action to " + START_PAGE.name()); - intent.setAction(SHORTCUT_ACTION); - intent.setClassName(mActivity, SHORTCUT_CLASS_DESCRIPTOR); - intent.setPackage(mActivity.getPackageName()); - intent.putExtra(SHORTCUT_TYPE, START_PAGE.shortcutType); - intent.putExtra(SHORTCUT_ACTION, overrideShortcutId); } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java index 6ffa16776..e275463d1 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/AlbumMusicVideoPatch.java @@ -159,7 +159,7 @@ public class AlbumMusicVideoPatch { VideoUtils.openInYouTubeMusic(songId); }, 1000); - VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 1500); + VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 2500); } catch (Exception ex) { Logger.printException(() -> "openMusic failure", ex); } diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/DrcAudioPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/DrcAudioPatch.java index 94e1e5335..699208a7b 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/DrcAudioPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/DrcAudioPatch.java @@ -4,11 +4,12 @@ import app.revanced.extension.music.settings.Settings; @SuppressWarnings("unused") public class DrcAudioPatch { + private static final boolean DISABLE_DRC_AUDIO = Settings.DISABLE_DRC_AUDIO.get(); public static float disableDrcAudio(float original) { - if (!Settings.DISABLE_DRC_AUDIO.get()) { - return original; + if (DISABLE_DRC_AUDIO) { + return 0f; } - return 0f; + return original; } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java new file mode 100644 index 000000000..d5e5b014b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java @@ -0,0 +1,174 @@ +package app.revanced.extension.music.patches.misc; + +import static app.revanced.extension.music.shared.VideoInformation.parameterIsAgeRestricted; +import static app.revanced.extension.music.shared.VideoInformation.parameterIsSample; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.BooleanUtils; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.shared.VideoInformation; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public class SpoofPlayerParameterPatch { + /** + * Used in YouTube Music. + */ + private static final boolean SPOOF_PLAYER_PARAMETER = Settings.SPOOF_PLAYER_PARAMETER.get(); + + /** + * Parameter to fix playback issues. + * Used in YouTube Music Samples. + */ + private static final String PLAYER_PARAMETER_SAMPLES = + "8AEB2AUBogYVAUY4C8W9wrM-FdhjSW4MnCgH44uhkAcI"; + + /** + * Parameter to fix playback issues. + * Used in YouTube Shorts. + */ + private static final String PLAYER_PARAMETER_SHORTS = + "8AEByAMkuAQ0ogYVAePzwRN3uesV1sPI2x4-GkDYlvqUkAcC"; + + /** + * On app first start, the first video played usually contains a single non-default window setting value + * and all other subtitle settings for the video are (incorrect) default Samples window settings. + * For this situation, the Samples settings must be replaced. + *

+ * But some videos use multiple text positions on screen (such as youtu.be/3hW1rMNC89o), + * and by chance many of the subtitles uses window positions that match a default Samples position. + * To handle these videos, selectively allowing the Samples specific window settings to 'pass thru' unchanged, + * but only if the video contains multiple non-default subtitle window positions. + *

+ * Do not enable 'pass thru mode' until this many non default subtitle settings are observed for a single video. + */ + private static final int NUMBER_OF_NON_DEFAULT_SUBTITLES_BEFORE_ENABLING_PASSTHRU = 2; + + /** + * The number of non default subtitle settings encountered for the current video. + */ + private static int numberOfNonDefaultSettingsObserved; + + @GuardedBy("itself") + private static final Map lastVideoIds = new LinkedHashMap<>() { + private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK; + } + }; + + /** + * Injection point. + */ + public static String spoofParameter(@NonNull String videoId, @Nullable String parameter) { + if (SPOOF_PLAYER_PARAMETER) { + synchronized (lastVideoIds) { + Boolean isSamples = parameterIsSample(parameter); + if (lastVideoIds.put(videoId, isSamples) == null) { + Logger.printDebug(() -> "New video loaded (videoId: " + videoId + ", isSamples: " + isSamples + ")"); + } + } + return parameterIsAgeRestricted(parameter) + ? PLAYER_PARAMETER_SHORTS + : PLAYER_PARAMETER_SAMPLES; + } + return parameter; + } + + /** + * Injection point. Overrides values passed into SubtitleWindowSettings constructor. + * + * @param ap anchor position. A bitmask with 6 bit fields, that appears to indicate the layout position on screen + * @param ah anchor horizontal. A percentage [0, 100], that appears to be a horizontal text anchor point + * @param av anchor vertical. A percentage [0, 100], that appears to be a vertical text anchor point + * @param vs appears to indicate if subtitles exist, and the value is always true. + * @param sd function is not entirely clear + */ + public static int[] fixSubtitleWindowPosition(int ap, int ah, int av, boolean vs, boolean sd) { + // Videos with custom captions that specify screen positions appear to always have correct screen positions (even with spoofing). + // But for auto generated and most other captions, the spoof incorrectly gives various default Samples caption settings. + // Check for these known default Samples captions parameters, and replace with the known correct values. + // + // If a regular video uses a custom subtitle setting that match a default Samples setting, + // then this will incorrectly replace the setting. + // But, if the video uses multiple subtitles in different screen locations, then detect the non-default values + // and do not replace any window settings for the video (regardless if they match a Samples default). + if (SPOOF_PLAYER_PARAMETER && + numberOfNonDefaultSettingsObserved < NUMBER_OF_NON_DEFAULT_SUBTITLES_BEFORE_ENABLING_PASSTHRU) { + synchronized (lastVideoIds) { + String videoId = VideoInformation.getVideoId(); + Boolean isSample = lastVideoIds.get(videoId); + if (BooleanUtils.isFalse(isSample)) { + for (SubtitleWindowReplacementSettings setting : SubtitleWindowReplacementSettings.values()) { + if (setting.match(ap, ah, av, vs, sd)) { + return setting.replacementSetting(); + } + } + + numberOfNonDefaultSettingsObserved++; + } + } + } + + return new int[]{ap, ah, av}; + } + + /** + * Injection point. + *

+ * Return false to force disable age restricted playback feature flag. + */ + public static boolean forceDisableAgeRestrictedPlaybackFeatureFlag(boolean original) { + if (SPOOF_PLAYER_PARAMETER) { + return false; + } + return original; + } + + /** + * Known incorrect default Samples subtitle parameters, and the corresponding correct (non-Samples) values. + */ + private enum SubtitleWindowReplacementSettings { + DEFAULT_SAMPLES_PARAMETERS_1(10, 50, 0, true, false, + 34, 50, 95), + DEFAULT_SAMPLES_PARAMETERS_2(9, 20, 0, true, false, + 34, 50, 90), + DEFAULT_SAMPLES_PARAMETERS_3(9, 20, 0, true, true, + 33, 20, 100); + + // original values + final int ap, ah, av; + final boolean vs, sd; + + // replacement int values + final int[] replacement; + + SubtitleWindowReplacementSettings(int ap, int ah, int av, boolean vs, boolean sd, + int replacementAp, int replacementAh, int replacementAv) { + this.ap = ap; + this.ah = ah; + this.av = av; + this.vs = vs; + this.sd = sd; + this.replacement = new int[]{replacementAp, replacementAh, replacementAv}; + } + + boolean match(int ap, int ah, int av, boolean vs, boolean sd) { + return this.ap == ap && this.ah == ah && this.av == av && this.vs == vs && this.sd == sd; + } + + int[] replacementSetting() { + return replacement; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/requests/PlaylistRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/requests/PlaylistRequest.kt index aa8c6ccec..90fb6f963 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/requests/PlaylistRequest.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/requests/PlaylistRequest.kt @@ -5,6 +5,7 @@ import androidx.annotation.GuardedBy import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.requests.Requester +import app.revanced.extension.shared.settings.AppLanguage import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Utils import org.json.JSONException @@ -139,11 +140,24 @@ class PlaylistRequest private constructor( PlayerRoutes.GET_PLAYLIST_PAGE, clientType ) + /** + * For some reason, the tracks in Top Songs have the playlistId of the album: + * [ReVanced_Extended#2835](https://github.com/inotia00/ReVanced_Extended/issues/2835) + * + * We can work around this issue by checking the playlist title in the response. + * Tracks played from an album have a playlist title that starts with 'Album', + * Tracks played from Top Songs have a playlist title that starts with 'Song'. + * + * By default, the playlist title follows the app language, + * So we can work around this by setting the language to English when sending the request. + */ val requestBody = PlayerRoutes.createApplicationRequestBody( clientType = clientType, videoId = videoId, - playlistId = playlistId + playlistId = playlistId, + setLocale = true, + language = AppLanguage.EN.language, ) connection.setFixedLengthStreamingMode(requestBody.size) @@ -183,18 +197,23 @@ class PlaylistRequest private constructor( .getJSONObject("playlist") .getJSONObject("playlist") - val currentStreamJsonObject = playlistJsonObject - ?.getJSONArray("contents") - ?.get(playlistIndex) + val playlistTitle = playlistJsonObject + ?.getString("title") + "" - if (currentStreamJsonObject is JSONObject) { - val watchEndpointJsonObject: JSONObject? = - currentStreamJsonObject - .getJSONObject("playlistPanelVideoRenderer") - .getJSONObject("navigationEndpoint") - .getJSONObject("watchEndpoint") + if (playlistTitle.startsWith("Album")) { + val currentStreamJsonObject = playlistJsonObject + ?.getJSONArray("contents") + ?.get(playlistIndex) - return watchEndpointJsonObject?.getString("videoId") + "" + if (currentStreamJsonObject is JSONObject) { + val watchEndpointJsonObject: JSONObject? = + currentStreamJsonObject + .getJSONObject("playlistPanelVideoRenderer") + .getJSONObject("navigationEndpoint") + .getJSONObject("watchEndpoint") + + return watchEndpointJsonObject?.getString("videoId") + "" + } } } } catch (e: JSONException) { diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/navigation/NavigationPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/navigation/NavigationPatch.java index a36ba8fe4..53e1fe5cb 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/patches/navigation/NavigationPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/navigation/NavigationPatch.java @@ -4,13 +4,19 @@ import static app.revanced.extension.shared.utils.StringRef.str; import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition; import android.graphics.Color; +import android.text.Spanned; import android.view.View; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; import app.revanced.extension.music.patches.utils.PatchStatus; import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.utils.ExtendedUtils; +import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.ResourceUtils; import app.revanced.extension.shared.utils.Utils; @@ -20,7 +26,14 @@ public class NavigationPatch { ? ResourceUtils.getColor("ytm_color_grey_12") : ResourceUtils.getColor("revanced_color_grey_12"); - public static Enum lastPivotTab; + @Nullable + private static String lastYTNavigationEnumName; + + public static void setLastAppNavigationEnum(@Nullable Enum ytNavigationEnumName) { + if (ytNavigationEnumName != null) { + lastYTNavigationEnumName = ytNavigationEnumName.name(); + } + } public static int enableCustomNavigationBarColor() { try { @@ -46,24 +59,138 @@ public class NavigationPatch { return; } - for (NavigationButton button : NavigationButton.values()) - if (lastPivotTab.name().equals(button.name)) - hideViewUnderCondition(button.enabled, view); + for (NavigationButton button : NavigationButton.values()) { + if (button.ytEnumNames.equals(lastYTNavigationEnumName)) { + if (button.replace) { + Runnable onClickAction = button.onClickAction; + if (onClickAction != null) { + view.setOnClickListener(v -> onClickAction.run()); + Utils.runOnMainThreadDelayed(() -> view.setOnClickListener(v -> onClickAction.run()), 500); + } + } + hideViewUnderCondition(button.hidden, view); + } + } + } + + public static String replaceBrowseId(Object component, String browseId, String fieldName) { + for (NavigationButton button : NavigationButton.values()) { + if (button.replace && button.browseId.equals(browseId)) { + String replaceBrowseId = Settings.CHANGE_START_PAGE.get().getBrowseId(); + if (replaceBrowseId.isEmpty()) { + replaceBrowseId = NavigationButton.HOME.browseId; + } + try { + Field browseIdField = component.getClass().getField(fieldName); + browseIdField.setAccessible(true); + browseIdField.set(component, replaceBrowseId); + return replaceBrowseId; + } catch (Exception ignored) { + Logger.printException(() -> "replaceBrowseId failed"); + } + } + } + + return browseId; + } + + public static int replaceNavigationIcon(int drawableId) { + for (NavigationButton button : NavigationButton.values()) { + if (button.replace && + (drawableId == button.unSelectedDrawableId || drawableId == button.selectedDrawableId)) { + int replaceDrawableId = button.replaceDrawableId; + if (replaceDrawableId != 0) { + return replaceDrawableId; + } + } + } + + return drawableId; + } + + public static Spanned replaceNavigationLabel(@NonNull Spanned sourceStyle) { + for (NavigationButton button : NavigationButton.values()) { + if (button.ytEnumNames.equals(lastYTNavigationEnumName) && button.replace) { + String label = button.label; + if (!label.isEmpty()) { + return Utils.newSpanUsingStylingOfAnotherSpan(sourceStyle, label); + } + } + } + + return sourceStyle; } private enum NavigationButton { - HOME("TAB_HOME", Settings.HIDE_NAVIGATION_HOME_BUTTON.get()), - SAMPLES("TAB_SAMPLES", Settings.HIDE_NAVIGATION_SAMPLES_BUTTON.get()), - EXPLORE("TAB_EXPLORE", Settings.HIDE_NAVIGATION_EXPLORE_BUTTON.get()), - LIBRARY("LIBRARY_MUSIC", Settings.HIDE_NAVIGATION_LIBRARY_BUTTON.get()), - UPGRADE("TAB_MUSIC_PREMIUM", Settings.HIDE_NAVIGATION_UPGRADE_BUTTON.get()); + HOME( + "TAB_HOME", + Settings.HIDE_NAVIGATION_HOME_BUTTON.get(), + "FEmusic_home" + ), + SAMPLES( + "TAB_SAMPLES", + Settings.HIDE_NAVIGATION_SAMPLES_BUTTON.get(), + Settings.REPLACE_NAVIGATION_SAMPLES_BUTTON.get(), + "FEmusic_immersive", + "search", + "yt_fill_samples_vd_theme_24", + "yt_outline_samples_vd_theme_24", + "yt_outline_search_vd_theme_24", + ExtendedUtils::openSearch + ), + EXPLORE( + "TAB_EXPLORE", + Settings.HIDE_NAVIGATION_EXPLORE_BUTTON.get(), + "FEmusic_explore" + ), + LIBRARY( + "LIBRARY_MUSIC", + Settings.HIDE_NAVIGATION_LIBRARY_BUTTON.get(), + "FEmusic_library_landing" + ), + UPGRADE( + "TAB_MUSIC_PREMIUM", + Settings.HIDE_NAVIGATION_UPGRADE_BUTTON.get(), + Settings.REPLACE_NAVIGATION_UPGRADE_BUTTON.get(), + "SPunlimited", + "settings", + "yt_fill_youtube_music_vd_theme_24", + "yt_outline_youtube_music_vd_theme_24", + "yt_outline_gear_vd_theme_24", + ExtendedUtils::openSetting + ); - private final boolean enabled; - private final String name; + private final String ytEnumNames; + private final boolean hidden; + private final boolean replace; + @NonNull + private final String browseId; + @NonNull + private final String label; + private final int selectedDrawableId; + private final int unSelectedDrawableId; + private final int replaceDrawableId; + @Nullable + private final Runnable onClickAction; - NavigationButton(String name, boolean enabled) { - this.enabled = enabled; - this.name = name; + NavigationButton(@NonNull String ytEnumNames, boolean hidden, + @NonNull String browseId) { + this(ytEnumNames, hidden, false, browseId, null, null, null, null, null); + } + + NavigationButton(@NonNull String ytEnumNames, boolean hidden, boolean replace, + @NonNull String browseId, @Nullable String label, + @Nullable String selectedIcon, @Nullable String unSelectedIcon, + @Nullable String replaceIcon, @Nullable Runnable onClickAction) { + this.ytEnumNames = ytEnumNames; + this.hidden = hidden; + this.replace = replace; + this.browseId = browseId; + this.label = label != null ? ResourceUtils.getString(label) : ""; + this.selectedDrawableId = selectedIcon != null ? ResourceUtils.getDrawableIdentifier(selectedIcon) : 0; + this.unSelectedDrawableId = unSelectedIcon != null ? ResourceUtils.getDrawableIdentifier(unSelectedIcon) : 0; + this.replaceDrawableId = replaceIcon != null ? ResourceUtils.getDrawableIdentifier(replaceIcon) : 0; + this.onClickAction = onClickAction; } } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java index 7958e4019..e4b192734 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java @@ -3,6 +3,7 @@ package app.revanced.extension.music.returnyoutubedislike; import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote; import static app.revanced.extension.shared.utils.StringRef.str; import static app.revanced.extension.shared.utils.Utils.isSDKAbove; +import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan; import android.graphics.Canvas; import android.graphics.Paint; @@ -283,15 +284,6 @@ public class ReturnYouTubeDislike { : formatDislikeCount(voteData.getDislikeCount())); } - private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) { - SpannableString destination = new SpannableString(newSpanText); - Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class); - for (Object span : spans) { - destination.setSpan(span, 0, destination.length(), sourceStyle.getSpanFlags(span)); - } - return destination; - } - private static String formatDislikeCount(long dislikeCount) { if (isSDKAbove(24)) { if (dislikeCountFormatter == null) { diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java index b0b531d8b..3f74973f2 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java @@ -130,6 +130,9 @@ public class Settings extends BaseSettings { public static final BooleanSetting HIDE_NAVIGATION_UPGRADE_BUTTON = new BooleanSetting("revanced_hide_navigation_upgrade_button", TRUE, true); public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true); public static final BooleanSetting HIDE_NAVIGATION_LABEL = new BooleanSetting("revanced_hide_navigation_label", FALSE, true); + public static final BooleanSetting REPLACE_NAVIGATION_SAMPLES_BUTTON = new BooleanSetting("revanced_replace_navigation_samples_button", FALSE, true); + public static final BooleanSetting REPLACE_NAVIGATION_UPGRADE_BUTTON = new BooleanSetting("revanced_replace_navigation_upgrade_button", FALSE, true); + public static final BooleanSetting REPLACE_NAVIGATION_BUTTON_ABOUT = new BooleanSetting("revanced_replace_navigation_button_about", FALSE, false); // PreferenceScreen: Player @@ -190,6 +193,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting DISABLE_DRC_AUDIO = new BooleanSetting("revanced_disable_drc_audio", FALSE, true); public static final BooleanSetting DISABLE_MUSIC_VIDEO_IN_ALBUM = new BooleanSetting("revanced_disable_music_video_in_album", FALSE, true); public static final EnumSetting DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE = new EnumSetting<>("revanced_disable_music_video_in_album_redirect_type", RedirectType.REDIRECT, true); + public static final BooleanSetting SPOOF_PLAYER_PARAMETER = new BooleanSetting("revanced_spoof_player_parameter", TRUE, true); public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false); // PreferenceScreen: Return YouTube Dislike @@ -276,6 +280,7 @@ public class Settings extends BaseSettings { RETURN_YOUTUBE_USERNAME_ABOUT.key, RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT.key, RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY.key, + REPLACE_NAVIGATION_BUTTON_ABOUT.key, SB_API_URL.key, SETTINGS_IMPORT_EXPORT.key, SPOOF_APP_VERSION_TARGET.key, diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java index d06d664cd..c8a16910e 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java @@ -12,6 +12,7 @@ import static app.revanced.extension.music.settings.Settings.EXTERNAL_DOWNLOADER import static app.revanced.extension.music.settings.Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS; import static app.revanced.extension.music.settings.Settings.OPEN_DEFAULT_APP_SETTINGS; import static app.revanced.extension.music.settings.Settings.OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX; +import static app.revanced.extension.music.settings.Settings.REPLACE_NAVIGATION_BUTTON_ABOUT; import static app.revanced.extension.music.settings.Settings.RETURN_YOUTUBE_USERNAME_ABOUT; import static app.revanced.extension.music.settings.Settings.SB_API_URL; import static app.revanced.extension.music.settings.Settings.SETTINGS_IMPORT_EXPORT; @@ -161,6 +162,8 @@ public class ReVancedPreferenceFragment extends PreferenceFragment { importExportListDialogBuilder(); } else if (settings.equals(RETURN_YOUTUBE_USERNAME_ABOUT)) { YouTubeDataAPIDialogBuilder.showDialog(mActivity); + } else if (settings.equals(REPLACE_NAVIGATION_BUTTON_ABOUT)) { + ResettableListPreference.showDialog(mActivity, CHANGE_START_PAGE, 0); } else { Logger.printDebug(() -> "Failed to find the right value: " + dataString); } diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java b/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java index 12ce65258..451d873bf 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java @@ -22,12 +22,25 @@ public final class VideoInformation { private static final float DEFAULT_YOUTUBE_MUSIC_PLAYBACK_SPEED = 1.0f; private static final int DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY = -2; private static final String DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY_STRING = getString("quality_auto"); + /** + * Prefix present in all Age-restricted music player parameters signature. + */ + private static final String AGE_RESTRICTED_PLAYER_PARAMETER = "ygYQ"; + /** + * Prefix present in all Sample player parameters signature. + */ + private static final String SAMPLES_PLAYER_PARAMETERS = "8AEB"; + @NonNull private static String videoId = ""; private static long videoLength = 0; private static long videoTime = -1; + @NonNull + private static volatile String playerResponseVideoId = ""; + private static volatile boolean playerResponseVideoIdIsSample; + /** * The current playback speed */ @@ -85,6 +98,65 @@ public final class VideoInformation { videoId = newlyLoadedVideoId; } + /** + * Differs from {@link #videoId} as this is the video id for the + * last player response received, which may not be the last video opened. + *

+ * If Shorts are loading the background, this commonly will be + * different from the Short that is currently on screen. + *

+ * For most use cases, you should instead use {@link #getVideoId()}. + * + * @return The id of the last video loaded, or an empty string if no videos have been loaded yet. + */ + @NonNull + public static String getPlayerResponseVideoId() { + return playerResponseVideoId; + } + + /** + * @return If the last player response video id was a Sample. + */ + public static boolean lastPlayerResponseIsSample() { + return playerResponseVideoIdIsSample; + } + + /** + * Injection point. Called off the main thread. + * + * @param videoId The id of the last video loaded. + */ + public static void setPlayerResponseVideoId(@NonNull String videoId) { + if (!playerResponseVideoId.equals(videoId)) { + playerResponseVideoId = videoId; + } + } + + /** + * @return If the player parameter is for a Age-restricted video. + */ + public static boolean parameterIsAgeRestricted(@Nullable String parameter) { + return parameter != null && parameter.startsWith(AGE_RESTRICTED_PLAYER_PARAMETER); + } + + /** + * @return If the player parameter is for a Sample. + */ + public static boolean parameterIsSample(@Nullable String parameter) { + return parameter != null && parameter.startsWith(SAMPLES_PLAYER_PARAMETERS); + } + + /** + * Injection point. + */ + @Nullable + public static String newPlayerResponseParameter(@NonNull String videoId, @Nullable String playerParameter) { + playerResponseVideoIdIsSample = parameterIsSample(playerParameter); + Logger.printDebug(() -> "videoId: " + videoId + ", playerParameter: " + playerParameter); + + return playerParameter; // Return the original value since we are observing and not modifying. + } + /** * Seek on the current video. * Does not function for playback of Shorts. diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/utils/ExtendedUtils.java b/extensions/shared/src/main/java/app/revanced/extension/music/utils/ExtendedUtils.java index ed5bebf89..ed7bef03d 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/utils/ExtendedUtils.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/utils/ExtendedUtils.java @@ -1,7 +1,9 @@ package app.revanced.extension.music.utils; +import android.app.Activity; import android.app.AlertDialog; import android.content.Context; +import android.content.Intent; import android.view.ViewGroup; import android.widget.FrameLayout; @@ -9,8 +11,20 @@ import androidx.annotation.NonNull; import app.revanced.extension.music.settings.Settings; import app.revanced.extension.shared.utils.PackageUtils; +import app.revanced.extension.shared.utils.ResourceUtils; public class ExtendedUtils extends PackageUtils { + private static final String SETTINGS_CLASS_DESCRIPTOR = "com.google.android.apps.youtube.music.settings.SettingsCompatActivity"; + private static final String SETTINGS_ATTRIBUTION_FRAGMENT_KEY = ":android:show_fragment"; + private static final String SETTINGS_ATTRIBUTION_FRAGMENT_VALUE = "com.google.android.apps.youtube.music.settings.fragment.SettingsHeadersFragment"; + private static final String SETTINGS_ATTRIBUTION_HEADER_KEY = ":android:no_headers"; + private static final int SETTINGS_ATTRIBUTION_HEADER_VALUE = 1; + + private static final String SHORTCUT_ACTION = "com.google.android.youtube.music.action.shortcut"; + private static final String SHORTCUT_CLASS_DESCRIPTOR = "com.google.android.apps.youtube.music.activities.InternalMusicActivity"; + private static final String SHORTCUT_TYPE = "com.google.android.youtube.music.action.shortcut_type"; + private static final String SHORTCUT_ID_SEARCH = "Eh4IBRDTnQEYmgMiEwiZn+H0r5WLAxVV5OcDHcHRBmPqpd25AQA="; + private static final int SHORTCUT_TYPE_SEARCH = 1; @SuppressWarnings("unused") public static boolean isSpoofingToLessThan(@NonNull String versionName) { @@ -39,4 +53,35 @@ public class ExtendedUtils extends PackageUtils { return params; } + + public static void openSearch() { + Activity mActivity = ResourceUtils.getActivity(); + if (mActivity == null) { + return; + } + Intent intent = new Intent(); + setSearchIntent(mActivity, intent); + mActivity.startActivity(intent); + } + + public static void openSetting() { + Activity mActivity = ResourceUtils.getActivity(); + if (mActivity == null) { + return; + } + Intent intent = new Intent(); + intent.setPackage(mActivity.getPackageName()); + intent.setClassName(mActivity, SETTINGS_CLASS_DESCRIPTOR); + intent.putExtra(SETTINGS_ATTRIBUTION_FRAGMENT_KEY, SETTINGS_ATTRIBUTION_FRAGMENT_VALUE); + intent.putExtra(SETTINGS_ATTRIBUTION_HEADER_KEY, SETTINGS_ATTRIBUTION_HEADER_VALUE); + mActivity.startActivity(intent); + } + + public static void setSearchIntent(Activity mActivity, Intent intent) { + intent.setAction(SHORTCUT_ACTION); + intent.setClassName(mActivity, SHORTCUT_CLASS_DESCRIPTOR); + intent.setPackage(mActivity.getPackageName()); + intent.putExtra(SHORTCUT_TYPE, SHORTCUT_TYPE_SEARCH); + intent.putExtra(SHORTCUT_ACTION, SHORTCUT_ID_SEARCH); + } } \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/ReturnYouTubeUsernamePatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/ReturnYouTubeUsernamePatch.java index a0b009040..20c987db3 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/ReturnYouTubeUsernamePatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/ReturnYouTubeUsernamePatch.java @@ -3,8 +3,7 @@ package app.revanced.extension.shared.patches; import static java.lang.Boolean.FALSE; import static java.lang.Boolean.TRUE; -import android.text.SpannableString; -import android.text.Spanned; +import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan; import androidx.annotation.NonNull; @@ -73,26 +72,13 @@ public class ReturnYouTubeUsernamePatch { Logger.printDebug(() -> "ChannelRequest Stream is null, handle:" + handle); return original; } - return copySpannableString(original, userName); + return newSpanUsingStylingOfAnotherSpan(original, userName); } catch (Exception ex) { Logger.printException(() -> "onLithoTextLoaded failure", ex); } return original; } - private static CharSequence copySpannableString(CharSequence original, String userName) { - if (original instanceof Spanned spanned) { - SpannableString newString = new SpannableString(userName); - Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); - for (Object span : spans) { - int flags = spanned.getSpanFlags(span); - newString.setSpan(span, 0, newString.length(), flags); - } - return newString; - } - return original; - } - public enum DisplayFormat { USERNAME_ONLY(null), USERNAME_HANDLE(TRUE), diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt index c4b22519b..8723b8297 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/YouTubeAppClient.kt @@ -27,28 +27,28 @@ object YouTubeAppClient { private val CLIENT_VERSION_IOS = if (forceAVC()) "17.40.5" else - "19.29.1" + "20.10.4" private const val DEVICE_MAKE_IOS = "Apple" private const val OS_NAME_IOS = "iOS" /** - * The device machine id for the iPhone 15 Pro Max (iPhone16,2), + * The device machine id for the iPhone 16 Pro Max (iPhone17,2), * used to get HDR with AV1 hardware decoding. * See [this GitHub Gist](https://gist.github.com/adamawolf/3048717) for more information. */ private val DEVICE_MODEL_IOS = if (forceAVC()) "iPhone12,5" // 11 Pro Max. (last device with iOS 13) else - "iPhone16,2" // 15 Pro Max. + "iPhone17,2" // 16 Pro Max. private val OS_VERSION_IOS = if (forceAVC()) "13.7.17H35" // Last release of iOS 13. else - "17.7.2.21H221" + "18.3.2.22D82" private val USER_AGENT_VERSION_IOS = if (forceAVC()) "13_7" else - "17_7_2" + "18_3_2" private val USER_AGENT_IOS = iOSUserAgent(PACKAGE_NAME_IOS, CLIENT_VERSION_IOS) @@ -69,7 +69,7 @@ object YouTubeAppClient { private val CLIENT_VERSION_IOS_UNPLUGGED = if (forceAVC()) "6.45" else - "8.33" + "9.10" private val USER_AGENT_IOS_UNPLUGGED = iOSUserAgent(PACKAGE_NAME_IOS_UNPLUGGED, CLIENT_VERSION_IOS_UNPLUGGED) @@ -92,35 +92,31 @@ object YouTubeAppClient { * Package name for YouTube VR (Meta Quests): com.google.android.apps.youtube.vr.oculus * Package name for YouTube VR (ByteDance Pico): com.google.android.apps.youtube.vr.pico */ - private const val PACKAGE_NAME_ANDROID_VR = "com.google.android.apps.youtube.vr.pico" + private const val PACKAGE_NAME_ANDROID_VR = "com.google.android.apps.youtube.vr.oculus" /** * The hardcoded client version of the Android VR app used for InnerTube requests with this client. * * It can be extracted by getting the latest release version of the app on - * [the App Store page of the YouTube VR app](https://store-global.picoxr.com/en/detail/1/7270207384512020485/), - * in the `Information` section. + * [the App Store page of the YouTube app](https://www.meta.com/en-us/experiences/2002317119880945/), + * in the `Additional details` section. */ private const val CLIENT_VERSION_ANDROID_VR = "1.62.27" /** - * The device machine id for the Pico 4 Ultra. - * - * For Pico 4 Ultra, there is no public firmware archive yet. - * The device machine id is taken from [this repository](https://github.com/Genymobile/scrcpy/issues/5659). - * The OS version and build ID are taken from [the signature key of OTA firmware](https://pico.crx.moe/docs/picoos-research/version-table#pico-4-ultra-series). + * The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client. + * See [this GitLab](https://dumps.tadiphone.dev/dumps/oculus/eureka) for more information. */ - private const val DEVICE_MODEL_ANDROID_VR = "A9210" + private const val DEVICE_MODEL_ANDROID_VR = "Quest 3" + private const val DEVICE_MAKE_ANDROID_VR = "Oculus" + private const val OS_VERSION_ANDROID_VR = "12" + /** - * The manufacturer is 'ByteDance', but the build prop is marked as 'Pico'. + * The SDK version for Android 12 is 31, + * but for some reason the build.props for the `Quest 3` state that the SDK version is 32. */ - private const val DEVICE_MAKE_ANDROID_VR = "Pico" - private const val OS_VERSION_ANDROID_VR = "14" - private const val ANDROID_SDK_VERSION_ANDROID_VR = "34" - /** - * PICO OS 5.12.6 (Android 14) - */ - private const val BUILD_ID_ANDROID_VR = "UKQ1.240321.001" + private const val ANDROID_SDK_VERSION_ANDROID_VR = "32" + private const val BUILD_ID_ANDROID_VR = "SQ3A.220605.009.A1" private val USER_AGENT_ANDROID_VR = androidUserAgent( packageName = PACKAGE_NAME_ANDROID_VR, @@ -137,7 +133,7 @@ object YouTubeAppClient { * Note: Audio track is not available */ private const val PACKAGE_NAME_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged" - private const val CLIENT_VERSION_ANDROID_UNPLUGGED = "8.16.0" + private const val CLIENT_VERSION_ANDROID_UNPLUGGED = "9.09.1" /** * The device machine id for the Chromecast with Google TV 4K. @@ -147,8 +143,8 @@ object YouTubeAppClient { private const val DEVICE_MAKE_ANDROID_UNPLUGGED = "Google" private const val OS_VERSION_ANDROID_UNPLUGGED = "14" private const val ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = "34" - private const val BUILD_ID_ANDROID_UNPLUGGED = "UTT3.240625.001.K5" - private const val GMS_CORE_VERSION_CODE_ANDROID_UNPLUGGED = "244336107" + private const val BUILD_ID_ANDROID_UNPLUGGED = "UTTK.241210.003" + private const val GMS_CORE_VERSION_CODE_ANDROID_UNPLUGGED = "244738119" private val USER_AGENT_ANDROID_UNPLUGGED = androidUserAgent( packageName = PACKAGE_NAME_ANDROID_UNPLUGGED, @@ -165,7 +161,7 @@ object YouTubeAppClient { * Note: Audio track is not available */ private const val PACKAGE_NAME_ANDROID_CREATOR = "com.google.android.apps.youtube.creator" - private const val CLIENT_VERSION_ANDROID_CREATOR = "23.47.101" + private const val CLIENT_VERSION_ANDROID_CREATOR = "25.10.100" /** * The device machine id for the Google Pixel 9 Pro Fold. @@ -176,7 +172,7 @@ object YouTubeAppClient { private const val OS_VERSION_ANDROID_CREATOR = "15" private const val ANDROID_SDK_VERSION_ANDROID_CREATOR = "35" private const val BUILD_ID_ANDROID_CREATOR = "AP3A.241005.015.A2" - private const val GMS_CORE_VERSION_CODE_ANDROID_CREATOR = "244738035" + private const val GMS_CORE_VERSION_CODE_ANDROID_CREATOR = "250932035" private val USER_AGENT_ANDROID_CREATOR = androidUserAgent( packageName = PACKAGE_NAME_ANDROID_CREATOR, diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java index 299ff96d9..ef9d62ae7 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java @@ -23,6 +23,8 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch { BaseSettings.SPOOF_STREAMING_DATA_PO_TOKEN.get(); private static final String VISITOR_DATA = BaseSettings.SPOOF_STREAMING_DATA_VISITOR_DATA.get(); + private static final boolean SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION = + SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION.get(); /** * Any unreachable ip address. Used to intentionally fail requests. @@ -67,11 +69,11 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch { * Skip response encryption in OnesiePlayerRequest. */ public static boolean skipResponseEncryption(boolean original) { - if (!SPOOF_STREAMING_DATA) { - return original; + if (SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) { + return false; } - return false; + return original; } /** diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt index 39ece7fd9..bd99fc453 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.kt @@ -88,6 +88,7 @@ object PlayerRoutes { botGuardPoToken: String = "", visitorId: String = "", setLocale: Boolean = false, + language: String = BaseSettings.SPOOF_STREAMING_DATA_LANGUAGE.get().language, ): ByteArray { val innerTubeBody = JSONObject() @@ -108,7 +109,7 @@ object PlayerRoutes { client.put( "hl", if (setLocale) { - BaseSettings.SPOOF_STREAMING_DATA_LANGUAGE.get().language + language } else { LOCALE_LANGUAGE } diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java index c91080390..004766e99 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -30,7 +30,7 @@ public class BaseSettings { * These settings are used by YouTube Music. * Some patches are in a shared path, so they are declared here. */ - public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", TRUE, true); + public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true); public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", MusicAppClient.ClientType.IOS_MUSIC_6_21, true); /** @@ -41,6 +41,7 @@ public class BaseSettings { public static final EnumSetting SPOOF_STREAMING_DATA_LANGUAGE = new EnumSetting<>("revanced_spoof_streaming_data_language", AppLanguage.DEFAULT, new AudioStreamLanguageOverrideAvailability()); public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, true, "revanced_spoof_streaming_data_ios_force_avc_user_dialog_message"); + public static final BooleanSetting SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION = new BooleanSetting("revanced_spoof_streaming_data_skip_response_encryption", TRUE, true); public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE); // Client type must be last spoof setting due to cyclic references. public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_UNPLUGGED, true); diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java index e3a769f67..8d8c567d9 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -24,9 +24,6 @@ import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.StringRef; import app.revanced.extension.shared.utils.Utils; -/** - * @noinspection rawtypes - */ @SuppressWarnings("unused") public abstract class Setting { @@ -128,6 +125,7 @@ public abstract class Setting { /** * @return All settings that have been created, sorted by keys. + * @noinspection Java8ListSort */ @NonNull private static List> allLoadedSettingsSorted() { @@ -171,7 +169,6 @@ public abstract class Setting { /** * Confirmation message to display, if the user tries to change the setting from the default value. - * Currently this works only for Boolean setting types. */ @Nullable public final StringRef userDialogMessage; @@ -271,6 +268,7 @@ public abstract class Setting { *

* This method will be deleted in the future. */ + @SuppressWarnings("rawtypes") public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) { if (!oldPrefs.preferences.contains(settingKey)) { return; // Nothing to do. @@ -452,6 +450,7 @@ public abstract class Setting { boolean rebootSettingChanged = false; int numberOfSettingsImported = 0; + //noinspection rawtypes for (Setting setting : SETTINGS) { String key = setting.getImportExportKey(); if (json.has(key)) { diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java index b2bac3d67..168ef7375 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -12,6 +12,7 @@ import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.preference.SwitchPreference; @@ -21,6 +22,9 @@ import android.widget.ListView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import java.util.Objects; + +import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.utils.Logger; @@ -48,10 +52,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { try { - if (str == null) { - return; - } - Setting setting = Setting.getSettingFromPath(str); + Setting setting = Setting.getSettingFromPath(Objects.requireNonNull(str)); if (setting == null) { return; } @@ -59,24 +60,23 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { if (pref == null) { return; } + Logger.printDebug(() -> "Preference changed: " + setting.key); + + if (!settingImportInProgress && !showingUserDialogMessage) { + if (setting.userDialogMessage != null && !prefIsSetToDefault(pref, setting)) { + // Do not change the setting yet, to allow preserving whatever + // list/text value was previously set if it needs to be reverted. + showSettingUserDialogConfirmation(pref, setting); + return; + } else if (setting.rebootApp) { + showRestartDialog(getContext()); + } + } // Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'. updatePreference(pref, setting, true, settingImportInProgress); // Update any other preference availability that may now be different. updateUIAvailability(); - - if (settingImportInProgress) { - return; - } - - if (!showingUserDialogMessage) { - if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) { - showSettingUserDialogConfirmation((SwitchPreference) pref, (BooleanSetting) setting); - } else if (setting.rebootApp) { - showRestartDialog(getActivity()); - } - } - } catch (Exception ex) { Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex); } @@ -90,14 +90,16 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { * so all app specific {@link Setting} instances are loaded before this method returns. */ protected void initialize() { - final int id = getXmlIdentifier("revanced_prefs"); + final int identifier = getXmlIdentifier("revanced_prefs"); + if (identifier == 0) return; + addPreferencesFromResource(identifier); - if (id == 0) return; - addPreferencesFromResource(id); - Utils.sortPreferenceGroups(getPreferenceScreen()); + PreferenceScreen screen = getPreferenceScreen(); + Utils.sortPreferenceGroups(screen); + Utils.setPreferenceTitlesToMultiLineIfNeeded(screen); } - private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) { + private void showSettingUserDialogConfirmation(Preference pref, Setting setting) { Utils.verifyOnMainThread(); final var context = getActivity(); @@ -107,12 +109,19 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { .setTitle(android.R.string.dialog_alert_title) .setMessage(setting.userDialogMessage.toString()) .setPositiveButton(android.R.string.ok, (dialog, id) -> { + // User confirmed, save to the Setting. + updatePreference(pref, setting, true, false); + + // Update availability of other preferences that may be changed. + updateUIAvailability(); + if (setting.rebootApp) { showRestartDialog(context); } }) .setNegativeButton(android.R.string.cancel, (dialog, id) -> { - switchPref.setChecked(setting.defaultValue); // Recursive call that resets the Setting value. + // Restore whatever the setting was before the change. + updatePreference(pref, setting, true, true); }) .setOnDismissListener(dialog -> showingUserDialogMessage = false) .setCancelable(false) @@ -123,7 +132,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { * Updates all Preferences values and their availability using the current values in {@link Setting}. */ protected void updateUIToSettingValues() { - updatePreferenceScreen(getPreferenceScreen(), true, true); + updatePreferenceScreen(getPreferenceScreen(), true,true); } /** @@ -133,24 +142,48 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { updatePreferenceScreen(getPreferenceScreen(), false, false); } + /** + * @return If the preference is currently set to the default value of the Setting. + */ + protected boolean prefIsSetToDefault(Preference pref, Setting setting) { + if (pref instanceof SwitchPreference switchPref) { + return switchPref.isChecked() == (Boolean) setting.defaultValue; + } + if (pref instanceof EditTextPreference editPreference) { + return editPreference.getText().equals(setting.defaultValue.toString()); + } + if (pref instanceof ListPreference listPref) { + return listPref.getValue().equals(setting.defaultValue.toString()); + } + + throw new IllegalStateException("Must override method to handle " + + "preference type: " + pref.getClass()); + } + + /** * Syncs all UI Preferences to any {@link Setting} they represent. */ - private void updatePreferenceScreen(@NonNull PreferenceScreen screen, + private void updatePreferenceScreen(@NonNull PreferenceGroup group, boolean syncSettingValue, boolean applySettingToPreference) { // Alternatively this could iterate thru all Settings and check for any matching Preferences, // but there are many more Settings than UI preferences so it's more efficient to only check // the Preferences. - for (int i = 0, prefCount = screen.getPreferenceCount(); i < prefCount; i++) { - Preference pref = screen.getPreference(i); - if (pref instanceof PreferenceScreen preferenceScreen) { - updatePreferenceScreen(preferenceScreen, syncSettingValue, applySettingToPreference); + for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { + Preference pref = group.getPreference(i); + if (pref instanceof PreferenceGroup subGroup) { + updatePreferenceScreen(subGroup, syncSettingValue, applySettingToPreference); } else if (pref.hasKey()) { String key = pref.getKey(); Setting setting = Setting.getSettingFromPath(key); + if (setting != null) { updatePreference(pref, setting, syncSettingValue, applySettingToPreference); + } else if (BaseSettings.ENABLE_DEBUG_LOGGING.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); } } } @@ -166,26 +199,26 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { protected void syncSettingWithPreference(@NonNull Preference pref, @NonNull Setting setting, boolean applySettingToPreference) { - if (pref instanceof SwitchPreference switchPreference) { + if (pref instanceof SwitchPreference switchPref) { BooleanSetting boolSetting = (BooleanSetting) setting; if (applySettingToPreference) { - switchPreference.setChecked(boolSetting.get()); + switchPref.setChecked(boolSetting.get()); } else { - BooleanSetting.privateSetValue(boolSetting, switchPreference.isChecked()); + BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked()); } - } else if (pref instanceof EditTextPreference editTextPreference) { + } else if (pref instanceof EditTextPreference editPreference) { if (applySettingToPreference) { - editTextPreference.setText(setting.get().toString()); + editPreference.setText(setting.get().toString()); } else { - Setting.privateSetValueFromString(setting, editTextPreference.getText()); + Setting.privateSetValueFromString(setting, editPreference.getText()); } - } else if (pref instanceof ListPreference listPreference) { + } else if (pref instanceof ListPreference listPref) { if (applySettingToPreference) { - listPreference.setValue(setting.get().toString()); + listPref.setValue(setting.get().toString()); } else { - Setting.privateSetValueFromString(setting, listPreference.getValue()); + Setting.privateSetValueFromString(setting, listPref.getValue()); } - updateListPreferenceSummary(listPreference, setting); + updateListPreferenceSummary(listPref, setting); } else { Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref); } @@ -194,7 +227,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment { /** * Updates a UI Preference with the {@link Setting} that backs it. * - * @param syncSetting If the UI should be synced {@link Setting} <-> Preference + * @param syncSetting If the UI should be synced {@link Setting} <-> Preference * @param applySettingToPreference If true, then apply {@link Setting} -> Preference. * If false, then apply {@link Setting} <- Preference. */ diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ResourceUtils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ResourceUtils.java index 3aaba81ce..809c4ce75 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ResourceUtils.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ResourceUtils.java @@ -7,6 +7,7 @@ import android.view.animation.Animation; import android.view.animation.AnimationUtils; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; @SuppressWarnings({"unused", "deprecation", "DiscouragedApi"}) public class ResourceUtils extends Utils { @@ -100,13 +101,19 @@ public class ResourceUtils extends Utils { return getIdentifier(str, ResourceType.XML); } + @Nullable public static Animation getAnimation(@NonNull String str) { - int identifier = getAnimIdentifier(str); - if (identifier == 0) { + try { + int identifier = getAnimIdentifier(str); + if (identifier == 0) { + handleException(str, ResourceType.ANIM); + identifier = android.R.anim.fade_in; + } + return AnimationUtils.loadAnimation(getContext(), identifier); + } catch (Exception ex) { handleException(str, ResourceType.ANIM); - identifier = android.R.anim.fade_in; } - return AnimationUtils.loadAnimation(getContext(), identifier); + return null; } public static int getColor(@NonNull String str) { diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java index 6e50f65a6..268a9e0be 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java @@ -3,6 +3,8 @@ package app.revanced.extension.shared.utils; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; import android.app.Fragment; import android.content.ClipboardManager; import android.content.Context; @@ -13,11 +15,14 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.Build; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.preference.Preference; import android.preference.PreferenceGroup; import android.preference.PreferenceScreen; +import android.text.SpannableString; +import android.text.Spanned; import android.util.TypedValue; import android.view.View; import android.view.ViewGroup; @@ -43,17 +48,18 @@ import java.util.concurrent.Future; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import app.revanced.extension.shared.settings.AppLanguage; import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BooleanSetting; -import kotlin.text.Regex; @SuppressWarnings("deprecation") public class Utils { private static WeakReference activityRef = new WeakReference<>(null); - private static WeakReference contextRef = new WeakReference<>(null); + @SuppressLint("StaticFieldLeak") + private static volatile Context context; protected Utils() { } // utility class @@ -274,17 +280,15 @@ public class Utils { } public static Context getContext() { - Context mContext = contextRef.get(); - if (mContext == null) { + if (context == null) { Logger.initializationException(Utils.class, "Context is null, returning null!", null); } - return mContext; + return context; } public static Resources getResources() { - Context mContext = contextRef.get(); - if (mContext != null) { - return mContext.getResources(); + if (context != null) { + return context.getResources(); } Activity mActivity = activityRef.get(); if (mActivity != null) { @@ -347,7 +351,7 @@ public class Utils { } // Must initially set context to check the app language. - contextRef = new WeakReference<>(appContext); + context = appContext; Logger.initializationInfo(Utils.class, "Set context: " + appContext); AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get(); @@ -355,7 +359,7 @@ public class Utils { // Create a new context with the desired language. Configuration config = appContext.getResources().getConfiguration(); config.setLocale(language.getLocale()); - contextRef = new WeakReference<>(appContext.createConfigurationContext(config)); + context = appContext.createConfigurationContext(config); } } @@ -364,8 +368,7 @@ public class Utils { } public static void setClipboard(@NonNull String text, @Nullable String toastMessage) { - Context mContext = contextRef.get(); - if (mContext != null && mContext.getSystemService(Context.CLIPBOARD_SERVICE) instanceof ClipboardManager clipboardManager) { + if (context != null && context.getSystemService(Context.CLIPBOARD_SERVICE) instanceof ClipboardManager clipboardManager) { android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text); clipboardManager.setPrimaryClip(clip); @@ -508,6 +511,26 @@ public class Utils { return false; } + public static CharSequence newSpanUsingStylingOfAnotherSpan(@Nullable CharSequence sourceStyle, @NonNull CharSequence newSpanText) { + if (sourceStyle instanceof Spanned spanned) { + return newSpanUsingStylingOfAnotherSpan(spanned, newSpanText); + } + return sourceStyle; + } + + public static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) { + if (sourceStyle == newSpanText && sourceStyle instanceof SpannableString spannableString) { + return spannableString; // Nothing to do. + } + SpannableString destination = new SpannableString(newSpanText); + Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class); + for (Object span : spans) { + destination.setSpan(span, 0, destination.length(), sourceStyle.getSpanFlags(span)); + } + return destination; + } + + /** * @return whether the device's API level is higher than a specific SDK version. */ @@ -516,23 +539,96 @@ public class Utils { } public static int dpToPx(float dp) { - Context mContext = contextRef.get(); - if (mContext == null) { + if (context == null) { return (int) dp; } else { - return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mContext.getResources().getDisplayMetrics()); + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); } } public static int dpToPx(int dp) { - Context mContext = contextRef.get(); - if (mContext == null) { + if (context == null) { return dp; } else { - return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mContext.getResources().getDisplayMetrics()); + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); } } + + /** + * Ignore this class. It must be public to satisfy Android requirements. + */ + public static final class DialogFragmentWrapper extends DialogFragment { + + private Dialog dialog; + @Nullable + private DialogFragmentOnStartAction onStartAction; + + @Override + public void onSaveInstanceState(Bundle outState) { + // Do not call super method to prevent state saving. + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return dialog; + } + + @Override + public void onStart() { + try { + super.onStart(); + + if (onStartAction != null) { + onStartAction.onStart((AlertDialog) getDialog()); + } + } catch (Exception ex) { + Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex); + } + } + } + + /** + * Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}. + */ + @FunctionalInterface + public interface DialogFragmentOnStartAction { + void onStart(AlertDialog dialog); + } + + public static void showDialog(Activity activity, AlertDialog dialog) { + showDialog(activity, dialog, true, null); + } + + /** + * Utility method to allow showing an AlertDialog on top of other alert dialogs. + * Calling this will always display the dialog on top of all other dialogs + * previously called using this method. + *
+ * Be aware the on start action can be called multiple times for some situations, + * such as the user switching apps without dismissing the dialog then switching back to this app. + *
+ * This method is only useful during app startup and multiple patches may show their own dialog, + * and the most important dialog can be called last (using a delay) so it's always on top. + *
+ * For all other situations it's better to not use this method and + * call {@link AlertDialog#show()} on the dialog. + */ + public static void showDialog(Activity activity, + AlertDialog dialog, + boolean isCancelable, + @Nullable DialogFragmentOnStartAction onStartAction) { + verifyOnMainThread(); + + DialogFragmentWrapper fragment = new DialogFragmentWrapper(); + fragment.dialog = dialog; + fragment.onStartAction = onStartAction; + fragment.setCancelable(isCancelable); + + fragment.show(activity.getFragmentManager(), null); + } + /** * Safe to call from any thread */ @@ -550,20 +646,18 @@ public class Utils { private static void showToast(@NonNull String messageToToast, int toastDuration) { Objects.requireNonNull(messageToToast); runOnMainThreadNowOrLater(() -> { - Context mContext = contextRef.get(); - if (mContext == null) { + if (context == null) { Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null); } else { Logger.printDebug(() -> "Showing toast: " + messageToToast); - Toast.makeText(mContext, messageToToast, toastDuration).show(); + Toast.makeText(context, messageToToast, toastDuration).show(); } }); } public static boolean isLandscapeOrientation() { - Context mContext = contextRef.get(); - if (mContext == null) return false; - final int orientation = mContext.getResources().getConfiguration().orientation; + if (context == null) return false; + final int orientation = context.getResources().getConfiguration().orientation; return orientation == Configuration.ORIENTATION_LANDSCAPE; } @@ -654,8 +748,7 @@ public class Utils { @SuppressLint("MissingPermission") // permission already included in YouTube public static NetworkType getNetworkType() { - Context mContext = contextRef.get(); - if (mContext == null || !(mContext.getSystemService(Context.CONNECTIVITY_SERVICE) instanceof ConnectivityManager cm)) + if (context == null || !(context.getSystemService(Context.CONNECTIVITY_SERVICE) instanceof ConnectivityManager cm)) return NetworkType.NONE; final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); @@ -744,14 +837,14 @@ public class Utils { } } - private static final Regex punctuationRegex = new Regex("\\p{P}+"); + private static final Pattern punctuationPattern = Pattern.compile("\\p{P}+"); /** * Strips all punctuation and converts to lower case. A null parameter returns an empty string. */ public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) { if (original == null) return ""; - return punctuationRegex.replace(original, "").toLowerCase(); + return punctuationPattern.matcher(original).replaceAll("").toLowerCase(); } /** @@ -762,7 +855,6 @@ public class Utils { * If a preference has no key or no {@link Sort} suffix, * then the preferences are left unsorted. */ - @SuppressWarnings("deprecation") public static void sortPreferenceGroups(@NonNull PreferenceGroup group) { Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED); SortedMap preferences = new TreeMap<>(); @@ -771,8 +863,8 @@ public class Utils { Preference preference = group.getPreference(i); final Sort preferenceSort; - if (preference instanceof PreferenceGroup preferenceGroup) { - sortPreferenceGroups(preferenceGroup); + if (preference instanceof PreferenceGroup subGroup) { + sortPreferenceGroups(subGroup); preferenceSort = groupSort; // Sort value for groups is for it's content, not itself. } else { // Allow individual preferences to set a key sorting. @@ -782,13 +874,16 @@ public class Utils { final String sortValue; switch (preferenceSort) { - case BY_TITLE -> - sortValue = removePunctuationConvertToLowercase(preference.getTitle()); - case BY_KEY -> sortValue = preference.getKey(); - case UNSORTED -> { + case BY_TITLE: + sortValue = removePunctuationConvertToLowercase(preference.getTitle()); + break; + case BY_KEY: + sortValue = preference.getKey(); + break; + case UNSORTED: continue; // Keep original sorting. - } - default -> throw new IllegalStateException(); + default: + throw new IllegalStateException(); } preferences.put(sortValue, preference); @@ -798,7 +893,7 @@ public class Utils { for (Preference pref : preferences.values()) { int order = index++; - // If the preference is a PreferenceScreen or is an intent preference, move to the top. + // Move any screens, intents, and the one off About preference to the top. if (pref instanceof PreferenceScreen || pref.getIntent() != null) { // Arbitrary high number. order -= 1000; @@ -807,4 +902,32 @@ public class Utils { pref.setOrder(order); } } + + /** + * Set all preferences to multiline titles if the device is not using an English variant. + * The English strings are heavily scrutinized and all titles fit on screen + * except 2 or 3 preference strings and those do not affect readability. + *

+ * Allowing multiline for those 2 or 3 English preferences looks weird and out of place, + * and visually it looks better to clip the text and keep all titles 1 line. + */ + public static void setPreferenceTitlesToMultiLineIfNeeded(PreferenceGroup group) { + if (!isSDKAbove(26)) { + return; + } + + String revancedLocale = Utils.getContext().getResources().getConfiguration().locale.getLanguage(); + if (revancedLocale.equals(Locale.ENGLISH.getLanguage())) { + return; + } + + for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { + Preference pref = group.getPreference(i); + pref.setSingleLineTitle(false); + + if (pref instanceof PreferenceGroup subGroup) { + setPreferenceTitlesToMultiLineIfNeeded(subGroup); + } + } + } } diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/alternativethumbnails/AlternativeThumbnailsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/alternativethumbnails/AlternativeThumbnailsPatch.java index aa9750853..f060d2a56 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/alternativethumbnails/AlternativeThumbnailsPatch.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/alternativethumbnails/AlternativeThumbnailsPatch.java @@ -189,14 +189,13 @@ public final class AlternativeThumbnailsPatch { // Unknown tab, treat as the home tab; return homeOption; } - if (selectedNavButton == NavigationButton.HOME) { - return homeOption; - } - if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) { - return subscriptionsOption; - } - // A library tab variant is active. - return libraryOption; + + return switch (selectedNavButton) { + case SUBSCRIPTIONS, NOTIFICATIONS -> subscriptionsOption; + case LIBRARY -> libraryOption; + // Home or explore tab. + default -> homeOption; + }; } /** diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionsFilter.java index fb2224d18..abf9981e3 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionsFilter.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionsFilter.java @@ -7,12 +7,15 @@ import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList import app.revanced.extension.shared.patches.components.Filter; import app.revanced.extension.shared.patches.components.StringFilterGroup; import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.EngagementPanel; +import app.revanced.extension.youtube.shared.RootView; @SuppressWarnings("unused") public final class DescriptionsFilter extends Filter { private final ByteArrayFilterGroupList macroMarkerShelfGroupList = new ByteArrayFilterGroupList(); private final StringFilterGroup howThisWasMadeSection; + private final StringFilterGroup horizontalShelf; private final StringFilterGroup infoCardsSection; private final StringFilterGroup macroMarkerShelf; private final StringFilterGroup shoppingLinks; @@ -54,6 +57,13 @@ public final class DescriptionsFilter extends Filter { "how_this_was_made_section.eml" ); + // In the latest YouTube, the Attribute section has the same path as the Carousel shelf. + // To hide only the Attribute section, check if the Description panel is open. + horizontalShelf = new StringFilterGroup( + Settings.HIDE_ATTRIBUTES_SECTION, + "horizontal_shelf.eml" + ); + infoCardsSection = new StringFilterGroup( Settings.HIDE_INFO_CARDS_SECTION, "infocards_section.eml" @@ -72,6 +82,7 @@ public final class DescriptionsFilter extends Filter { addPathCallbacks( howThisWasMadeSection, + horizontalShelf, infoCardsSection, macroMarkerShelf, shoppingLinks @@ -104,6 +115,16 @@ public final class DescriptionsFilter extends Filter { if (!macroMarkerShelfGroupList.check(protobufBufferArray).isFiltered()) { return false; } + } else if (matchedGroup == horizontalShelf) { + if (contentIndex != 0) { + return false; + } + if (!RootView.isPlayerActive()) { + return false; + } + if (!EngagementPanel.isDescription()) { + return false; + } } return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java index bef4712de..9c162d025 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java @@ -556,14 +556,13 @@ public final class KeywordContentFilter extends Filter { if (selectedNavButton == null) { return hideHome; // Unknown tab, treat the same as home. } - if (selectedNavButton == NavigationButton.HOME) { - return hideHome; - } - if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) { - return hideSubscriptions; - } - // User is in the Library or Notifications tab. - return false; + + return switch (selectedNavButton) { + case HOME, EXPLORE -> hideHome; + case SUBSCRIPTIONS -> hideSubscriptions; + // User is in the Library or notifications. + default -> false; + }; } private void updateStats(boolean videoWasHidden, @Nullable String keyword) { diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeFormFactorPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeFormFactorPatch.java new file mode 100644 index 000000000..4fa3779fc --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeFormFactorPatch.java @@ -0,0 +1,151 @@ +package app.revanced.extension.youtube.patches.general; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; + +import android.view.View; + +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.BooleanUtils; + +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.PackageUtils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.shared.RootView; + +@SuppressWarnings("unused") +public class ChangeFormFactorPatch { + + public enum FormFactor { + /** + * Unmodified, and same as un-patched. + */ + DEFAULT(null, null, null), + /** + *

+         * Some changes include:
+         * - Explore tab is present.
+         * - watch history is missing.
+         * - feed thumbnails fade in.
+         */
+        UNKNOWN(0, null, null),
+        SMALL(1, null, TRUE),
+        SMALL_WIDTH_DP(1, 480, TRUE),
+        LARGE(2, null, FALSE),
+        LARGE_WIDTH_DP(2, 600, FALSE),
+        /**
+         * Cars with 'Google built-in'.
+         * Layout seems identical to {@link #UNKNOWN}
+         * even when using an Android Automotive device.
+         */
+        AUTOMOTIVE(3, null, null),
+        WEARABLE(4, null, null);
+
+        @Nullable
+        final Integer formFactorType;
+
+        @Nullable
+        final Integer widthDp;
+
+        @Nullable
+        final Boolean setMinimumDp;
+
+
+        FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) {
+            this.formFactorType = formFactorType;
+            this.widthDp = widthDp;
+            this.setMinimumDp = setMinimumDp;
+        }
+
+        private boolean setMinimumDp() {
+            return BooleanUtils.isTrue(setMinimumDp);
+        }
+    }
+
+    private static final FormFactor FORM_FACTOR = Settings.CHANGE_FORM_FACTOR.get();
+    @Nullable
+    private static final Integer FORM_FACTOR_TYPE = FORM_FACTOR.formFactorType;
+    private static final boolean USING_AUTOMOTIVE_TYPE = Objects.requireNonNull(
+            FormFactor.AUTOMOTIVE.formFactorType).equals(FORM_FACTOR_TYPE);
+
+    /**
+     * Injection point.
+     */
+    public static int getFormFactor(int original) {
+        if (FORM_FACTOR_TYPE == null) return original;
+
+        if (USING_AUTOMOTIVE_TYPE) {
+            // Do not change if the player is opening or is opened,
+            // otherwise the video description cannot be opened.
+            PlayerType current = PlayerType.getCurrent();
+            if (current.isMaximizedOrFullscreen() || current == PlayerType.WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED) {
+                Logger.printDebug(() -> "Using original form factor for player");
+                return original;
+            }
+            if (!RootView.isSearchBarActive()) {
+                // Automotive type shows error 400 when opening a channel page and using some explore tab.
+                // This is a bug in unpatched YouTube that occurs on actual Android Automotive devices.
+                // Work around the issue by using the original form factor if not in search and the
+                // navigation back button is present.
+                if (RootView.isBackButtonVisible()) {
+                    Logger.printDebug(() -> "Using original form factor, as back button is visible without search present");
+                    return original;
+                }
+
+                // Do not change library tab otherwise watch history is hidden.
+                // Do this check last since the current navigation button is required.
+                if (NavigationButton.getSelectedNavigationButton() == NavigationButton.LIBRARY) {
+                    return original;
+                }
+            }
+        }
+
+        return FORM_FACTOR_TYPE;
+    }
+
+    /**
+     * Injection point.
+     */
+    public static int getWidthDp(int original) {
+        if (FORM_FACTOR_TYPE == null) return original;
+        Integer widthDp = FORM_FACTOR.widthDp;
+        if (widthDp == null) {
+            return original;
+        }
+        final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp();
+        if (smallestScreenWidthDp == 0) {
+            return original;
+        }
+        return FORM_FACTOR.setMinimumDp()
+                ? Math.min(smallestScreenWidthDp, widthDp)
+                : Math.max(smallestScreenWidthDp, widthDp);
+    }
+
+    public static boolean phoneLayoutEnabled() {
+        return Objects.equals(FORM_FACTOR.formFactorType, 1);
+    }
+
+    public static boolean tabletLayoutEnabled() {
+        return Objects.equals(FORM_FACTOR.formFactorType, 2);
+    }
+
+    /**
+     * Injection point.
+     */
+    public static void navigationTabCreated(NavigationButton button, View tabView) {
+        // On first startup of the app the navigation buttons are fetched and updated.
+        // If the user immediately opens the 'You' or opens a video, then the call to
+        // update the navigtation buttons will use the non automotive form factor
+        // and the explore tab is missing.
+        // Fixing this is not so simple because of the concurrent calls for the player and You tab.
+        // For now, always hide the explore tab.
+        if (USING_AUTOMOTIVE_TYPE && button == NavigationButton.EXPLORE) {
+            tabView.setVisibility(View.GONE);
+        }
+    }
+}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java
index 531af491c..b5652471b 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java
@@ -478,6 +478,10 @@ public class GeneralPatch {
         return Settings.HIDE_SEARCH_TERM_THUMBNAIL.get();
     }
 
+    public static boolean hideSearchTermThumbnail(boolean original) {
+        return !hideSearchTermThumbnail() && original;
+    }
+
     private static final boolean hideImageSearchButton = Settings.HIDE_IMAGE_SEARCH_BUTTON.get();
     private static final boolean hideVoiceSearchButton = Settings.HIDE_VOICE_SEARCH_BUTTON.get();
 
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/LayoutSwitchPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/LayoutSwitchPatch.java
deleted file mode 100644
index 56d343080..000000000
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/LayoutSwitchPatch.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package app.revanced.extension.youtube.patches.general;
-
-import static java.lang.Boolean.FALSE;
-import static java.lang.Boolean.TRUE;
-
-import androidx.annotation.Nullable;
-
-import org.apache.commons.lang3.BooleanUtils;
-
-import java.util.Objects;
-
-import app.revanced.extension.shared.utils.PackageUtils;
-import app.revanced.extension.youtube.settings.Settings;
-
-@SuppressWarnings("unused")
-public final class LayoutSwitchPatch {
-
-    public enum FormFactor {
-        /**
-         * Unmodified type, and same as un-patched.
-         */
-        ORIGINAL(null, null, null),
-        SMALL_FORM_FACTOR(1, null, TRUE),
-        SMALL_FORM_FACTOR_WIDTH_DP(1, 480, TRUE),
-        LARGE_FORM_FACTOR(2, null, FALSE),
-        LARGE_FORM_FACTOR_WIDTH_DP(2, 600, FALSE);
-
-        @Nullable
-        final Integer formFactorType;
-
-        @Nullable
-        final Integer widthDp;
-
-        @Nullable
-        final Boolean setMinimumDp;
-
-        FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) {
-            this.formFactorType = formFactorType;
-            this.widthDp = widthDp;
-            this.setMinimumDp = setMinimumDp;
-        }
-
-        private boolean setMinimumDp() {
-            return BooleanUtils.isTrue(setMinimumDp);
-        }
-    }
-
-    private static final FormFactor FORM_FACTOR = Settings.CHANGE_LAYOUT.get();
-
-    public static int getFormFactor(int original) {
-        Integer formFactorType = FORM_FACTOR.formFactorType;
-        return formFactorType == null
-                ? original
-                : formFactorType;
-    }
-
-    public static int getWidthDp(int original) {
-        Integer widthDp = FORM_FACTOR.widthDp;
-        if (widthDp == null) {
-            return original;
-        }
-        final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp();
-        if (smallestScreenWidthDp == 0) {
-            return original;
-        }
-        return FORM_FACTOR.setMinimumDp()
-                ? Math.min(smallestScreenWidthDp, widthDp)
-                : Math.max(smallestScreenWidthDp, widthDp);
-    }
-
-    public static boolean phoneLayoutEnabled() {
-        return Objects.equals(FORM_FACTOR.formFactorType, 1);
-    }
-
-    public static boolean tabletLayoutEnabled() {
-        return Objects.equals(FORM_FACTOR.formFactorType, 2);
-    }
-
-}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/BottomControlButton.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/BottomControlButton.java
index da4744f5a..abb4958b6 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/BottomControlButton.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/BottomControlButton.java
@@ -25,8 +25,11 @@ import app.revanced.extension.shared.utils.Logger;
 import app.revanced.extension.shared.utils.Utils;
 
 public abstract class BottomControlButton {
+    @Nullable
     private static final Animation fadeIn;
+    @Nullable
     private static final Animation fadeOut;
+    @Nullable
     private static final Animation fadeOutImmediate;
 
     private final ColorFilter cf =
@@ -40,29 +43,35 @@ public abstract class BottomControlButton {
 
     static {
         fadeIn = getAnimation("fade_in");
-        // android.R.integer.config_shortAnimTime, 200
-        fadeIn.setDuration(getInteger("fade_duration_fast"));
+        if (fadeIn != null) {
+            // android.R.integer.config_shortAnimTime, 200
+            fadeIn.setDuration(getInteger("fade_duration_fast"));
+        }
 
         fadeOut = getAnimation("fade_out");
-        // android.R.integer.config_mediumAnimTime, 400
-        fadeOut.setDuration(getInteger("fade_overlay_fade_duration"));
+        if (fadeOut != null) {
+            // android.R.integer.config_mediumAnimTime, 400
+            fadeOut.setDuration(getInteger("fade_overlay_fade_duration"));
+        }
 
         fadeOutImmediate = getAnimation("abc_fade_out");
-        // android.R.integer.config_shortAnimTime, 200
-        fadeOutImmediate.setDuration(getInteger("fade_duration_fast"));
+        if (fadeOutImmediate != null) {
+            // android.R.integer.config_shortAnimTime, 200
+            fadeOutImmediate.setDuration(getInteger("fade_duration_fast"));
+        }
     }
 
-    @NonNull
+    @Nullable
     public static Animation getButtonFadeIn() {
         return fadeIn;
     }
 
-    @NonNull
+    @Nullable
     public static Animation getButtonFadeOut() {
         return fadeOut;
     }
 
-    @NonNull
+    @Nullable
     public static Animation getButtonFadeOutImmediate() {
         return fadeOutImmediate;
     }
@@ -153,11 +162,15 @@ public abstract class BottomControlButton {
         imageView.clearAnimation();
         if (visible && setting.get()) {
             imageView.setVisibility(View.VISIBLE);
-            if (animation) imageView.startAnimation(fadeIn);
+            if (animation && fadeIn != null) {
+                imageView.startAnimation(fadeIn);
+            }
             return;
         }
         if (imageView.getVisibility() == View.VISIBLE) {
-            if (animation) imageView.startAnimation(fadeOut);
+            if (animation && fadeOut != null) {
+                imageView.startAnimation(fadeOut);
+            }
             imageView.setVisibility(View.GONE);
         }
     }
@@ -168,7 +181,9 @@ public abstract class BottomControlButton {
         if (!setting.get()) return;
 
         imageView.clearAnimation();
-        imageView.startAnimation(fadeOutImmediate);
+        if (fadeOutImmediate != null) {
+            imageView.startAnimation(fadeOutImmediate);
+        }
         imageView.setVisibility(View.GONE);
     }
 }
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/requests/ActionButtonRequest.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/requests/ActionButtonRequest.kt
index a10bd26d4..af38ab459 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/requests/ActionButtonRequest.kt
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/requests/ActionButtonRequest.kt
@@ -147,7 +147,7 @@ class ActionButtonRequest private constructor(
             } catch (ex: IOException) {
                 handleConnectionError("Network error", ex)
             } catch (ex: Exception) {
-                Logger.printException({ "sendApplicationRequest failed" }, ex)
+                Logger.printException({ "sendRequest failed" }, ex)
             } finally {
                 Logger.printDebug { "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
             }
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PatchStatus.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PatchStatus.java
index 6c9ddcbfd..d5317e396 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PatchStatus.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PatchStatus.java
@@ -1,5 +1,7 @@
 package app.revanced.extension.youtube.patches.utils;
 
+import app.revanced.extension.shared.utils.Utils;
+
 public class PatchStatus {
 
     public static boolean ImageSearchButton() {
@@ -7,6 +9,11 @@ public class PatchStatus {
         return false;
     }
 
+    public static boolean OldSplashAnimation() {
+        // Replace this with true if the Restore old splash animation (Custom branding icon) succeeds
+        return false;
+    }
+
     public static boolean PlayerButtons() {
         // Replace this with true if the Hide player buttons patch succeeds
         return false;
@@ -22,6 +29,12 @@ public class PatchStatus {
         return false;
     }
 
+    public static boolean SplashAnimation() {
+        // If 'Restore old splash animation' is included and device is running Android 12+,
+        // YouTube TV splash animations will be disabled by default.
+        return OldSplashAnimation() && Utils.isSDKAbove(31);
+    }
+
     public static boolean SponsorBlock() {
         // Replace this with true if the SponsorBlock patch succeeds
         return false;
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
index 16a3ef066..ca5ff0aaa 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java
@@ -3,6 +3,7 @@ package app.revanced.extension.youtube.returnyoutubedislike;
 import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote;
 import static app.revanced.extension.shared.utils.StringRef.str;
 import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
+import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan;
 import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
 
 import android.content.res.Resources;
@@ -336,20 +337,6 @@ public class ReturnYouTubeDislike {
                         : formatDislikeCount(voteData.getDislikeCount()));
     }
 
-    private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) {
-        if (sourceStyle == newSpanText && sourceStyle instanceof SpannableString spannableString) {
-            return spannableString; // Nothing to do.
-        }
-
-        SpannableString destination = new SpannableString(newSpanText);
-        Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class);
-        for (Object span : spans) {
-            destination.setSpan(span, 0, destination.length(), sourceStyle.getSpanFlags(span));
-        }
-
-        return destination;
-    }
-
     private static String formatDislikeCount(long dislikeCount) {
         if (isSDKAbove(24)) {
             synchronized (ReturnYouTubeDislike.class) { // Number formatter is not thread safe.
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
index 2551ef43d..6ea0ffe24 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java
@@ -32,9 +32,9 @@ import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeT
 import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability;
 import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption;
 import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailStillTime;
+import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch.FormFactor;
 import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch;
 import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch.StartPage;
-import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFactor;
 import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch;
 import app.revanced.extension.youtube.patches.player.ExitFullscreenPatch.FullscreenMode;
 import app.revanced.extension.youtube.patches.player.MiniplayerPatch;
@@ -147,14 +147,14 @@ public class Settings extends BaseSettings {
     public static final BooleanSetting CHANGE_START_PAGE_TYPE = new BooleanSetting("revanced_change_start_page_type", FALSE, true,
             new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
     public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE);
-    public static final BooleanSetting DISABLE_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_splash_animation", FALSE, true);
+    public static final BooleanSetting DISABLE_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_splash_animation", PatchStatus.SplashAnimation(), true);
     public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", TRUE, true);
     public static final BooleanSetting ENABLE_GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_enable_gradient_loading_screen", FALSE, true);
     public static final BooleanSetting HIDE_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true);
     public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE);
     public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE);
 
-    public static final EnumSetting CHANGE_LAYOUT = new EnumSetting<>("revanced_change_layout", FormFactor.ORIGINAL, true);
+    public static final EnumSetting CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
     public static final BooleanSetting CHANGE_LIVE_RING_CLICK_ACTION = new BooleanSetting("revanced_change_live_ring_click_action", FALSE, 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", PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION));
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
index d5abb90e7..5eab38ba7 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java
@@ -75,7 +75,7 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
     private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
         try {
             if (str == null) return;
-            Setting setting = Setting.getSettingFromPath(str);
+            Setting setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
 
             if (setting == null) return;
 
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
index 43ee95719..c3c22520b 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java
@@ -8,7 +8,7 @@ import android.preference.Preference;
 import android.preference.SwitchPreference;
 
 import app.revanced.extension.shared.settings.Setting;
-import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch;
+import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch;
 import app.revanced.extension.youtube.patches.utils.PatchStatus;
 import app.revanced.extension.youtube.patches.utils.ReturnYouTubeDislikePatch;
 import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
@@ -82,7 +82,7 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
      */
     private static void TabletLayoutLinks() {
         final boolean isTablet = ExtendedUtils.isTablet() &&
-                !LayoutSwitchPatch.phoneLayoutEnabled();
+                !ChangeFormFactorPatch.phoneLayoutEnabled();
 
         enableDisablePreferences(
                 isTablet,
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/EngagementPanel.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/EngagementPanel.java
index 11c4b3c8b..9b6fe69fc 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/EngagementPanel.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/EngagementPanel.java
@@ -6,7 +6,7 @@ import java.util.concurrent.atomic.AtomicReference;
 
 import app.revanced.extension.shared.utils.Logger;
 
-@SuppressWarnings("unused")
+@SuppressWarnings({"unused", "BooleanMethodIsAlwaysInverted"})
 public final class EngagementPanel {
     private static final AtomicReference engagementPanelId = new AtomicReference<>("");
 
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java
index 3c65e1d0c..469e8aa9b 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java
@@ -223,6 +223,10 @@ public final class NavigationBar {
          * This tab will never be in a selected state, even if the create video UI is on screen.
          */
         CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"),
+        /**
+         * Only shown to automotive layout.
+         */
+        EXPLORE("TAB_EXPLORE"),
         SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"),
         /**
          * Notifications tab.  Only present when
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java
index 1a6d97edd..3d18b32b2 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java
@@ -2,9 +2,48 @@ package app.revanced.extension.youtube.shared;
 
 import static app.revanced.extension.youtube.patches.components.RelatedVideoFilter.isActionBarVisible;
 
+import android.graphics.drawable.Drawable;
+import android.widget.FrameLayout;
+
+import java.lang.ref.WeakReference;
+
+import app.revanced.extension.shared.utils.Logger;
+import app.revanced.extension.shared.utils.Utils;
+
 @SuppressWarnings("unused")
 public final class RootView {
 
+    /**
+     * Interface to call obfuscated methods in AppCompat Toolbar class.
+     */
+    public interface AppCompatToolbarPatchInterface {
+        Drawable patch_getToolbarIcon();
+    }
+
+    private static volatile WeakReference toolbarResultsRef
+            = new WeakReference<>(null);
+
+    /**
+     * Injection point.
+     */
+    public static void setToolbar(FrameLayout layout) {
+        AppCompatToolbarPatchInterface toolbar = Utils.getChildView(layout, false, (view) ->
+                view instanceof AppCompatToolbarPatchInterface
+        );
+
+        if (toolbar == null) {
+            Logger.printException(() -> "Could not find toolbar");
+            return;
+        }
+
+        toolbarResultsRef = new WeakReference<>(toolbar);
+    }
+
+    public static boolean isBackButtonVisible() {
+        AppCompatToolbarPatchInterface toolbar = toolbarResultsRef.get();
+        return toolbar != null && toolbar.patch_getToolbarIcon() != null;
+    }
+
     /**
      * @return If the search bar is on screen.  This includes if the player
      * is on screen and the search results are behind the player (and not visible).
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButtonController.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButtonController.java
index 44478658a..d6f956483 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButtonController.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButtonController.java
@@ -3,6 +3,7 @@ package app.revanced.extension.youtube.sponsorblock.ui;
 import static app.revanced.extension.shared.utils.Utils.getChildView;
 
 import android.view.View;
+import android.view.animation.Animation;
 import android.widget.ImageView;
 
 import java.lang.ref.WeakReference;
@@ -45,7 +46,10 @@ public class CreateSegmentButtonController {
                 return;
             }
             if (animation) {
-                imageView.startAnimation(BottomControlButton.getButtonFadeIn());
+                Animation fadeIn = BottomControlButton.getButtonFadeIn();
+                if (fadeIn != null) {
+                    imageView.startAnimation(fadeIn);
+                }
             }
             imageView.setVisibility(View.VISIBLE);
             return;
@@ -53,7 +57,10 @@ public class CreateSegmentButtonController {
         if (imageView.getVisibility() == View.VISIBLE) {
             imageView.clearAnimation();
             if (animation) {
-                imageView.startAnimation(BottomControlButton.getButtonFadeOut());
+                Animation fadeOut = BottomControlButton.getButtonFadeOut();
+                if (fadeOut != null) {
+                    imageView.startAnimation(fadeOut);
+                }
             }
             imageView.setVisibility(View.GONE);
         }
@@ -65,7 +72,10 @@ public class CreateSegmentButtonController {
         if (!shouldBeShown()) return;
 
         imageView.clearAnimation();
-        imageView.startAnimation(BottomControlButton.getButtonFadeOutImmediate());
+        Animation fadeOutImmediate = BottomControlButton.getButtonFadeOutImmediate();
+        if (fadeOutImmediate != null) {
+            imageView.startAnimation(fadeOutImmediate);
+        }
         imageView.setVisibility(View.GONE);
     }
 
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButtonController.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButtonController.java
index 4b8ab8e2e..53e9f24ae 100644
--- a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButtonController.java
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButtonController.java
@@ -4,6 +4,7 @@ import static app.revanced.extension.shared.utils.Utils.getChildView;
 import static app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController.videoHasSegments;
 
 import android.view.View;
+import android.view.animation.Animation;
 import android.widget.ImageView;
 
 import java.lang.ref.WeakReference;
@@ -47,7 +48,10 @@ public class VotingButtonController {
                 return;
             }
             if (animation) {
-                imageView.startAnimation(BottomControlButton.getButtonFadeIn());
+                Animation fadeIn = BottomControlButton.getButtonFadeIn();
+                if (fadeIn != null) {
+                    imageView.startAnimation(fadeIn);
+                }
             }
             imageView.setVisibility(View.VISIBLE);
             return;
@@ -55,7 +59,10 @@ public class VotingButtonController {
         if (imageView.getVisibility() == View.VISIBLE) {
             imageView.clearAnimation();
             if (animation) {
-                imageView.startAnimation(BottomControlButton.getButtonFadeOut());
+                Animation fadeOut = BottomControlButton.getButtonFadeOut();
+                if (fadeOut != null) {
+                    imageView.startAnimation(fadeOut);
+                }
             }
             imageView.setVisibility(View.GONE);
         }
@@ -68,7 +75,10 @@ public class VotingButtonController {
 
 
         imageView.clearAnimation();
-        imageView.startAnimation(BottomControlButton.getButtonFadeOutImmediate());
+        Animation fadeOutImmediate = BottomControlButton.getButtonFadeOutImmediate();
+        if (fadeOutImmediate != null) {
+            imageView.startAnimation(fadeOutImmediate);
+        }
         imageView.setVisibility(View.GONE);
     }
 
diff --git a/gradle.properties b/gradle.properties
index 28a0e3ffa..8b8969587 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -4,5 +4,5 @@ org.gradle.parallel = true
 android.useAndroidX = true
 kotlin.code.style = official
 kotlin.jvm.target.validation.mode = IGNORE
-version = 5.4.2
+version = 5.5.1
 
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 169ef7199..4376956d5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,7 +3,7 @@ revanced-patcher = "21.0.0"
 # Tracking https://github.com/google/smali/issues/64.
 #noinspection GradleDependency
 smali = "3.0.5"
-gson = "2.11.0"
+gson = "2.12.1"
 agp = "8.2.2"
 annotation = "1.9.1"
 lang3 = "3.17.0"
diff --git a/patches.json b/patches.json
index 92ecdbb98..93b46884c 100644
--- a/patches.json
+++ b/patches.json
@@ -58,7 +58,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -99,7 +100,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -138,17 +140,20 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
   },
   {
-    "name": "Change layout",
-    "description": "Adds an option to change the dp in order to use a tablet or phone layout.",
+    "name": "Change form factor",
+    "description": "Adds an option to change the UI appearance to a phone, tablet, or automotive device.",
     "use": true,
     "dependencies": [
-      "Settings for YouTube"
+      "Settings for YouTube",
+      "BytecodePatch",
+      "BytecodePatch"
     ],
     "compatiblePackages": {
       "com.google.android.youtube": [
@@ -250,7 +255,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -282,6 +288,7 @@
     "description": "Adds an option to set which page the app opens in instead of the homepage.",
     "use": true,
     "dependencies": [
+      "ResourcePatch",
       "Settings for YouTube Music"
     ],
     "compatiblePackages": {
@@ -292,7 +299,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -448,7 +456,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": [
@@ -564,7 +573,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": [
@@ -676,7 +686,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": [
@@ -708,7 +719,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": [
@@ -781,7 +793,8 @@
         "7.06.54",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -802,7 +815,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -823,7 +837,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -865,7 +880,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -906,7 +922,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -969,7 +986,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -1020,7 +1038,8 @@
     "description": "Adds an option to disable the popup that appears when taking a screenshot.",
     "use": true,
     "dependencies": [
-      "Settings for Reddit"
+      "Settings for Reddit",
+      "ResourcePatch"
     ],
     "compatiblePackages": {
       "com.reddit.frontpage": [
@@ -1067,7 +1086,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -1108,7 +1128,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -1169,7 +1190,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -1196,7 +1218,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -1264,7 +1287,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": [
@@ -1451,7 +1475,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -1476,7 +1501,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -1526,7 +1552,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -1661,7 +1688,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -1722,7 +1750,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -1788,7 +1817,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -1956,9 +1986,11 @@
     "description": "Adds options to hide or change components related to the navigation bar.",
     "use": true,
     "dependencies": [
+      "Change start page",
       "ResourcePatch",
       "ResourcePatch",
-      "Settings for YouTube Music"
+      "Settings for YouTube Music",
+      "ResourcePatch"
     ],
     "compatiblePackages": {
       "com.google.android.apps.youtube.music": [
@@ -1968,7 +2000,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -2140,7 +2173,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -2203,7 +2237,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -2261,7 +2296,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -2302,7 +2338,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -2323,7 +2360,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -2369,7 +2407,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -2411,7 +2450,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -2591,7 +2631,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": [
@@ -2750,7 +2791,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -2840,7 +2882,7 @@
   {
     "name": "Spoof client",
     "description": "Adds options to spoof the client to allow playback.",
-    "use": true,
+    "use": false,
     "dependencies": [
       "Settings for YouTube Music",
       "ResourcePatch",
@@ -2856,7 +2898,31 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
+      ]
+    },
+    "options": []
+  },
+  {
+    "name": "Spoof player parameter",
+    "description": "Adds options to spoof player parameter to allow playback.",
+    "use": true,
+    "dependencies": [
+      "Settings for YouTube Music",
+      "BytecodePatch",
+      "BytecodePatch"
+    ],
+    "compatiblePackages": {
+      "com.google.android.apps.youtube.music": [
+        "6.20.51",
+        "6.29.59",
+        "6.42.55",
+        "6.51.53",
+        "7.16.53",
+        "7.25.53",
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -3056,7 +3122,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": [
@@ -3106,7 +3173,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
@@ -3201,7 +3269,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": [
@@ -3238,7 +3307,8 @@
         "6.51.53",
         "7.16.53",
         "7.25.53",
-        "8.05.51"
+        "8.05.51",
+        "8.10.51"
       ]
     },
     "options": []
diff --git a/patches/api/patches.api b/patches/api/patches.api
index 5d1c2bf9e..30b027aeb 100644
--- a/patches/api/patches.api
+++ b/patches/api/patches.api
@@ -198,6 +198,10 @@ public final class app/revanced/patches/music/utils/fix/fileprovider/FileProvide
 	public static final fun fileProviderPatch (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch;
 }
 
+public final class app/revanced/patches/music/utils/fix/parameter/SpoofPlayerParameterPatchKt {
+	public static final fun getSpoofPlayerParameterPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+}
+
 public final class app/revanced/patches/music/utils/flyoutmenu/FlyoutMenuHookPatchKt {
 	public static final fun getFlyoutMenuHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
 }
@@ -288,6 +292,10 @@ public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdP
 	public static final fun getTouchOutside ()J
 	public static final fun getTrimSilenceSwitch ()J
 	public static final fun getVarispeedUnavailableTitle ()J
+	public static final fun getYtFillSamples ()J
+	public static final fun getYtFillYouTubeMusic ()J
+	public static final fun getYtOutlineSamples ()J
+	public static final fun getYtOutlineYouTubeMusic ()J
 	public static final fun getYtmLogo ()J
 	public static final fun getYtmLogoRingo2 ()J
 	public static final fun isTablet ()J
@@ -331,10 +339,30 @@ public final class app/revanced/patches/music/video/playback/VideoPlaybackPatchK
 	public static final fun getVideoPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
 }
 
+public abstract class app/revanced/patches/music/video/playerresponse/Hook {
+	public synthetic fun  (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public fun toString ()Ljava/lang/String;
+}
+
+public final class app/revanced/patches/music/video/playerresponse/Hook$PlayerParameter : app/revanced/patches/music/video/playerresponse/Hook {
+	public fun  (Ljava/lang/String;)V
+}
+
+public final class app/revanced/patches/music/video/playerresponse/Hook$PlayerParameterBeforeVideoId : app/revanced/patches/music/video/playerresponse/Hook {
+	public fun  (Ljava/lang/String;)V
+}
+
+public final class app/revanced/patches/music/video/playerresponse/Hook$VideoId : app/revanced/patches/music/video/playerresponse/Hook {
+	public fun  (Ljava/lang/String;)V
+}
+
+public final class app/revanced/patches/music/video/playerresponse/Hook$VideoIdAndPlaylistId : app/revanced/patches/music/video/playerresponse/Hook {
+	public fun  (Ljava/lang/String;)V
+}
+
 public final class app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatchKt {
+	public static final fun addPlayerResponseMethodHook (Lapp/revanced/patches/music/video/playerresponse/Hook;)V
 	public static final fun getPlayerResponseMethodHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
-	public static final fun hookPlayerResponse (Ljava/lang/String;Z)V
-	public static synthetic fun hookPlayerResponse$default (Ljava/lang/String;ZILjava/lang/Object;)V
 }
 
 public final class app/revanced/patches/reddit/ad/AdsPatchKt {
@@ -424,6 +452,10 @@ public final class app/revanced/patches/reddit/utils/extension/SharedExtensionPa
 	public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
 }
 
+public final class app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatchKt {
+	public static final fun getScreenShotShareBanner ()J
+}
+
 public final class app/revanced/patches/reddit/utils/settings/SettingsPatchKt {
 	public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
 	public static final fun is_2024_26_or_greater ()Z
@@ -643,8 +675,8 @@ public final class app/revanced/patches/youtube/general/downloads/DownloadAction
 	public static final fun getDownloadActionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
 }
 
-public final class app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatchKt {
-	public static final fun getLayoutSwitchPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
+public final class app/revanced/patches/youtube/general/formfactor/ChangeFormFactorPatchKt {
+	public static final fun getChangeFormFactorPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
 }
 
 public final class app/revanced/patches/youtube/general/livering/OpenChannelOfLiveAvatarPatchKt {
@@ -992,6 +1024,7 @@ public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPa
 	public static final fun is_19_04_or_greater ()Z
 	public static final fun is_19_09_or_greater ()Z
 	public static final fun is_19_15_or_greater ()Z
+	public static final fun is_19_16_or_greater ()Z
 	public static final fun is_19_17_or_greater ()Z
 	public static final fun is_19_23_or_greater ()Z
 	public static final fun is_19_25_or_greater ()Z
@@ -1010,6 +1043,7 @@ public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPa
 	public static final fun is_20_02_or_greater ()Z
 	public static final fun is_20_03_or_greater ()Z
 	public static final fun is_20_05_or_greater ()Z
+	public static final fun is_20_10_or_greater ()Z
 }
 
 public final class app/revanced/patches/youtube/utils/recyclerview/RecyclerViewTreeObserverPatchKt {
@@ -1122,6 +1156,7 @@ public final class app/revanced/patches/youtube/utils/resourceid/SharedResourceI
 	public static final fun getTapBloomView ()J
 	public static final fun getTitleAnchor ()J
 	public static final fun getToolTipContentView ()J
+	public static final fun getToolbarContainerId ()J
 	public static final fun getTotalTime ()J
 	public static final fun getTouchArea ()J
 	public static final fun getVarispeedUnavailableTitle ()J
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt
index 92aa964c8..8a95aa8b6 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt
@@ -4,13 +4,17 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
 import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
 import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
 import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.patch.resourcePatch
 import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
 import app.revanced.patches.music.utils.extension.Constants.GENERAL_PATH
 import app.revanced.patches.music.utils.patch.PatchList.CHANGE_START_PAGE
+import app.revanced.patches.music.utils.playservice.is_6_27_or_greater
+import app.revanced.patches.music.utils.playservice.versionCheckPatch
 import app.revanced.patches.music.utils.settings.CategoryType
 import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
 import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
 import app.revanced.patches.music.utils.settings.settingsPatch
+import app.revanced.util.addEntryValues
 import app.revanced.util.fingerprint.methodOrThrow
 import app.revanced.util.getReference
 import app.revanced.util.indexOfFirstInstructionReversedOrThrow
@@ -22,6 +26,33 @@ import com.android.tools.smali.dexlib2.iface.reference.FieldReference
 private const val EXTENSION_CLASS_DESCRIPTOR =
     "$GENERAL_PATH/ChangeStartPagePatch;"
 
+private val changeStartPageResourcePatch = resourcePatch(
+    description = "changeStartPageResourcePatch"
+) {
+    dependsOn(
+        settingsPatch,
+        versionCheckPatch,
+    )
+
+    execute {
+        fun appendStartPage(startPage: String) {
+            addEntryValues(
+                "revanced_change_start_page_entries",
+                "@string/revanced_change_start_page_entry_$startPage",
+            )
+            addEntryValues(
+                "revanced_change_start_page_entry_values",
+                startPage.uppercase(),
+            )
+        }
+
+        if (is_6_27_or_greater) {
+            appendStartPage("search")
+        }
+        appendStartPage("subscriptions")
+    }
+}
+
 @Suppress("unused")
 val changeStartPagePatch = bytecodePatch(
     CHANGE_START_PAGE.title,
@@ -29,7 +60,10 @@ val changeStartPagePatch = bytecodePatch(
 ) {
     compatibleWith(COMPATIBLE_PACKAGE)
 
-    dependsOn(settingsPatch)
+    dependsOn(
+        changeStartPageResourcePatch,
+        settingsPatch,
+    )
 
     execute {
 
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt
index f72a914ee..5944b88cd 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt
@@ -14,7 +14,8 @@ import app.revanced.patches.music.utils.settings.addSwitchPreference
 import app.revanced.patches.music.utils.settings.settingsPatch
 import app.revanced.patches.music.video.information.videoIdHook
 import app.revanced.patches.music.video.information.videoInformationPatch
-import app.revanced.patches.music.video.playerresponse.hookPlayerResponse
+import app.revanced.patches.music.video.playerresponse.Hook
+import app.revanced.patches.music.video.playerresponse.addPlayerResponseMethodHook
 import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch
 import app.revanced.util.findMethodOrThrow
 import app.revanced.util.fingerprint.methodOrThrow
@@ -46,7 +47,11 @@ val albumMusicVideoPatch = bytecodePatch(
 
         // region hook player response
 
-        hookPlayerResponse("$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponse(Ljava/lang/String;Ljava/lang/String;I)V")
+        addPlayerResponseMethodHook(
+            Hook.VideoIdAndPlaylistId(
+                "$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponse(Ljava/lang/String;Ljava/lang/String;I)V"
+            ),
+        )
 
         // endregion
 
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt
index da41fe3c4..d918c8163 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt
@@ -41,6 +41,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
             "7.16.53",
             "7.25.53",
             "8.05.51",
+            "8.10.51",
         ),
     )
 
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/Fingerprints.kt
index 239029a93..c8902bfce 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/Fingerprints.kt
@@ -3,9 +3,13 @@ package app.revanced.patches.music.navigation.components
 import app.revanced.patches.music.utils.resourceid.colorGrey
 import app.revanced.patches.music.utils.resourceid.text1
 import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstruction
 import app.revanced.util.or
 import com.android.tools.smali.dexlib2.AccessFlags
 import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.Method
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
 
 internal val tabLayoutFingerprint = legacyFingerprint(
     name = "tabLayoutFingerprint",
@@ -30,5 +34,22 @@ internal val tabLayoutTextFingerprint = legacyFingerprint(
         Opcode.INVOKE_INTERFACE,
         Opcode.MOVE_RESULT
     ),
-    literals = listOf(text1)
+    strings = listOf("FEmusic_search"),
+    literals = listOf(text1),
+    customFingerprint = { method, _ ->
+        indexOfGetVisibilityInstruction(method) >= 0 &&
+                indexOfSetTextInstruction(method) >= 0
+    }
 )
+
+internal fun indexOfGetVisibilityInstruction(method: Method) =
+    method.indexOfFirstInstruction {
+        opcode == Opcode.INVOKE_VIRTUAL &&
+                getReference()?.name == "getVisibility"
+    }
+
+internal fun indexOfSetTextInstruction(method: Method) =
+    method.indexOfFirstInstruction {
+        opcode == Opcode.INVOKE_VIRTUAL &&
+                getReference()?.name == "setText"
+    }
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt
index 265ee72d4..e4b459e25 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt
@@ -6,26 +6,38 @@ import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
 import app.revanced.patcher.patch.PatchException
 import app.revanced.patcher.patch.bytecodePatch
 import app.revanced.patcher.patch.resourcePatch
+import app.revanced.patches.music.general.startpage.changeStartPagePatch
 import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
 import app.revanced.patches.music.utils.extension.Constants.NAVIGATION_CLASS_DESCRIPTOR
 import app.revanced.patches.music.utils.patch.PatchList.NAVIGATION_BAR_COMPONENTS
+import app.revanced.patches.music.utils.playservice.is_6_27_or_greater
+import app.revanced.patches.music.utils.playservice.versionCheckPatch
 import app.revanced.patches.music.utils.resourceid.colorGrey
 import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
 import app.revanced.patches.music.utils.resourceid.text1
+import app.revanced.patches.music.utils.resourceid.ytFillSamples
+import app.revanced.patches.music.utils.resourceid.ytFillYouTubeMusic
+import app.revanced.patches.music.utils.resourceid.ytOutlineSamples
+import app.revanced.patches.music.utils.resourceid.ytOutlineYouTubeMusic
 import app.revanced.patches.music.utils.settings.CategoryType
 import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
 import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
 import app.revanced.patches.music.utils.settings.addSwitchPreference
 import app.revanced.patches.music.utils.settings.settingsPatch
+import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT
 import app.revanced.util.fingerprint.matchOrThrow
 import app.revanced.util.fingerprint.methodOrThrow
 import app.revanced.util.getReference
 import app.revanced.util.indexOfFirstInstructionOrThrow
+import app.revanced.util.indexOfFirstInstructionReversedOrThrow
 import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
+import app.revanced.util.replaceLiteralInstructionCall
 import com.android.tools.smali.dexlib2.Opcode
 import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
 import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
 import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
+import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.FieldReference
 import com.android.tools.smali.dexlib2.iface.reference.MethodReference
 
 private const val FLAG = "android:layout_weight"
@@ -56,9 +68,11 @@ val navigationBarComponentsPatch = bytecodePatch(
     compatibleWith(COMPATIBLE_PACKAGE)
 
     dependsOn(
+        changeStartPagePatch,
         navigationBarComponentsResourcePatch,
         sharedResourceIdPatch,
         settingsPatch,
+        versionCheckPatch,
     )
 
     execute {
@@ -105,29 +119,73 @@ val navigationBarComponentsPatch = bytecodePatch(
          */
         tabLayoutTextFingerprint.matchOrThrow().let {
             it.method.apply {
+                val stringIndex = it.stringMatches!!.first().index
+                val browseIdIndex = indexOfFirstInstructionReversedOrThrow(stringIndex) {
+                    opcode == Opcode.IGET_OBJECT &&
+                            getReference()?.type == "Ljava/lang/String;"
+                }
+                val browseIdReference = getInstruction(browseIdIndex).reference as FieldReference
+                val fieldName = browseIdReference.name
+                val componentIndex = indexOfFirstInstructionOrThrow(stringIndex) {
+                    opcode == Opcode.IGET_OBJECT &&
+                            getReference()?.toString() == browseIdReference.toString()
+                }
+                val browseIdRegister = getInstruction(componentIndex).registerA
+                val componentRegister = getInstruction(componentIndex).registerB
+
                 val enumIndex = it.patternMatch!!.startIndex + 3
                 val enumRegister = getInstruction(enumIndex).registerA
                 val insertEnumIndex = indexOfFirstInstructionOrThrow(Opcode.AND_INT_LIT8) - 2
 
-                val pivotTabIndex = indexOfFirstInstructionOrThrow {
-                    opcode == Opcode.INVOKE_VIRTUAL &&
-                            getReference()?.name == "getVisibility"
-                }
+                val pivotTabIndex = indexOfGetVisibilityInstruction(this)
                 val pivotTabRegister =
                     getInstruction(pivotTabIndex).registerC
 
+                val spannedIndex = indexOfSetTextInstruction(this)
+                val spannedRegister =
+                    getInstruction(spannedIndex).registerD
+
                 addInstruction(
                     pivotTabIndex,
                     "invoke-static {v$pivotTabRegister}, $NAVIGATION_CLASS_DESCRIPTOR->hideNavigationButton(Landroid/view/View;)V"
                 )
 
+                addInstructions(
+                    componentIndex + 1, """
+                        const-string v$enumRegister, "$fieldName"
+                        invoke-static {v$componentRegister, v$browseIdRegister, v$enumRegister}, $NAVIGATION_CLASS_DESCRIPTOR->replaceBrowseId(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
+                        move-result-object v$browseIdRegister
+                        """
+                )
+
+                addInstructions(
+                    spannedIndex, """
+                        invoke-static {v$spannedRegister}, $NAVIGATION_CLASS_DESCRIPTOR->replaceNavigationLabel(Landroid/text/Spanned;)Landroid/text/Spanned;
+                        move-result-object v$spannedRegister
+                        """
+                )
+
                 addInstruction(
                     insertEnumIndex,
-                    "sput-object v$enumRegister, $NAVIGATION_CLASS_DESCRIPTOR->lastPivotTab:Ljava/lang/Enum;"
+                    "invoke-static {v$enumRegister}, $NAVIGATION_CLASS_DESCRIPTOR->setLastAppNavigationEnum(Ljava/lang/Enum;)V"
                 )
             }
         }
 
+        val smaliInstruction = """
+            invoke-static {v$REGISTER_TEMPLATE_REPLACEMENT}, $NAVIGATION_CLASS_DESCRIPTOR->replaceNavigationIcon(I)I
+            move-result v$REGISTER_TEMPLATE_REPLACEMENT
+            """
+
+        arrayOf(
+            ytFillSamples,
+            ytFillYouTubeMusic,
+            ytOutlineSamples,
+            ytOutlineYouTubeMusic,
+        ).forEach { literal ->
+            replaceLiteralInstructionCall(literal, smaliInstruction)
+        }
+
         addSwitchPreference(
             CategoryType.NAVIGATION,
             "revanced_enable_custom_navigation_bar_color",
@@ -173,6 +231,22 @@ val navigationBarComponentsPatch = bytecodePatch(
             "revanced_hide_navigation_label",
             "false"
         )
+        if (is_6_27_or_greater) {
+            addSwitchPreference(
+                CategoryType.NAVIGATION,
+                "revanced_replace_navigation_samples_button",
+                "false"
+            )
+        }
+        addSwitchPreference(
+            CategoryType.NAVIGATION,
+            "revanced_replace_navigation_upgrade_button",
+            "false"
+        )
+        addPreferenceWithIntent(
+            CategoryType.NAVIGATION,
+            "revanced_replace_navigation_button_about"
+        )
 
         updatePatchStatus(NAVIGATION_BAR_COMPONENTS)
 
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt
index 0182bc141..20921e3c4 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt
@@ -20,8 +20,8 @@ import app.revanced.util.or
 import com.android.tools.smali.dexlib2.AccessFlags
 import com.android.tools.smali.dexlib2.Opcode
 import com.android.tools.smali.dexlib2.iface.Method
-import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
 import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
 
 const val AUDIO_VIDEO_SWITCH_TOGGLE_VISIBILITY =
     "/AudioVideoSwitcherToggleView;->setVisibility(I)V"
@@ -44,7 +44,6 @@ internal val audioVideoSwitchToggleFingerprint = legacyFingerprint(
 internal val engagementPanelHeightFingerprint = legacyFingerprint(
     name = "engagementPanelHeightFingerprint",
     returnType = "L",
-    accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
     // In YouTube Music 7.21.50+, there are two methods with similar structure, so this Opcode pattern must be used.
     opcodes = listOf(
         Opcode.IGET_OBJECT,
@@ -54,6 +53,7 @@ internal val engagementPanelHeightFingerprint = legacyFingerprint(
     ),
     parameters = emptyList(),
     customFingerprint = { method, _ ->
+        AccessFlags.FINAL.isSet(method.accessFlags) &&
         method.containsLiteralInstruction(1) &&
                 method.indexOfFirstInstruction {
                     opcode == Opcode.INVOKE_VIRTUAL &&
@@ -65,7 +65,6 @@ internal val engagementPanelHeightFingerprint = legacyFingerprint(
 internal val engagementPanelHeightParentFingerprint = legacyFingerprint(
     name = "engagementPanelHeightParentFingerprint",
     returnType = "L",
-    accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
     opcodes = listOf(Opcode.NEW_ARRAY),
     parameters = emptyList(),
     customFingerprint = custom@{ method, _ ->
@@ -75,9 +74,12 @@ internal val engagementPanelHeightParentFingerprint = legacyFingerprint(
         if (method.returnType == "Ljava/lang/Object;") {
             return@custom false
         }
+        if (!AccessFlags.FINAL.isSet(method.accessFlags)) {
+            return@custom false
+        }
         method.indexOfFirstInstruction {
             opcode == Opcode.CHECK_CAST &&
-                    (this as? ReferenceInstruction)?.reference?.toString() == "Lcom/google/android/libraries/youtube/engagementpanel/size/EngagementPanelSizeBehavior;"
+                    getReference()?.type == "Lcom/google/android/libraries/youtube/engagementpanel/size/EngagementPanelSizeBehavior;"
         } >= 0
     }
 )
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt
index 435f19c2e..3169e345f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt
@@ -15,7 +15,8 @@ internal object Constants {
             "6.51.53", // This is the latest version of YouTube Music 6.xx.xx
             "7.16.53", // This is the latest version that supports the 'Spoof app version' patch.
             "7.25.53", // This is the last supported version for 2024.
-            "8.05.51", // This is the latest version supported by the RVX patch.
+            "8.05.51", // This was the latest version supported by the previous RVX patch.
+            "8.10.51", // This is the latest version supported by the RVX patch.
         )
     )
 }
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt
index 386955825..840bf6be2 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt
@@ -64,7 +64,8 @@ private const val CLIENT_INFO_CLASS_DESCRIPTOR =
 @Suppress("unused")
 val spoofClientPatch = bytecodePatch(
     SPOOF_CLIENT.title,
-    SPOOF_CLIENT.summary
+    SPOOF_CLIENT.summary,
+    false,
 ) {
     compatibleWith(COMPATIBLE_PACKAGE)
 
@@ -359,7 +360,7 @@ val spoofClientPatch = bytecodePatch(
         addSwitchPreference(
             CategoryType.MISC,
             "revanced_spoof_client",
-            "true"
+            "false"
         )
         addPreferenceWithIntent(
             CategoryType.MISC,
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/Fingerprints.kt
new file mode 100644
index 000000000..727df6df9
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/Fingerprints.kt
@@ -0,0 +1,26 @@
+package app.revanced.patches.music.utils.fix.parameter
+
+import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.or
+import com.android.tools.smali.dexlib2.AccessFlags
+
+internal val subtitleWindowFingerprint = legacyFingerprint(
+    name = "subtitleWindowFingerprint",
+    returnType = "V",
+    accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR,
+    parameters = listOf("I", "I", "I", "Z", "Z"),
+    strings = listOf("invalid anchorHorizontalPos: %s"),
+)
+
+/**
+ * If this flag is activated, a playback issue occurs in age-restricted videos.
+ */
+internal const val AGE_RESTRICTED_PLAYBACK_FEATURE_FLAG = 45651506L
+
+internal val ageRestrictedPlaybackFeatureFlagFingerprint = legacyFingerprint(
+    name = "ageRestrictedPlaybackFeatureFlagFingerprint",
+    returnType = "Z",
+    accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+    parameters = emptyList(),
+    literals = listOf(AGE_RESTRICTED_PLAYBACK_FEATURE_FLAG),
+)
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/SpoofPlayerParameterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/SpoofPlayerParameterPatch.kt
new file mode 100644
index 000000000..5e71c60cd
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/SpoofPlayerParameterPatch.kt
@@ -0,0 +1,82 @@
+package app.revanced.patches.music.utils.fix.parameter
+
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
+import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
+import app.revanced.patches.music.utils.extension.Constants.MISC_PATH
+import app.revanced.patches.music.utils.patch.PatchList.SPOOF_PLAYER_PARAMETER
+import app.revanced.patches.music.utils.settings.CategoryType
+import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
+import app.revanced.patches.music.utils.settings.addSwitchPreference
+import app.revanced.patches.music.utils.settings.settingsPatch
+import app.revanced.patches.music.video.information.videoInformationPatch
+import app.revanced.patches.music.video.playerresponse.Hook
+import app.revanced.patches.music.video.playerresponse.addPlayerResponseMethodHook
+import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch
+import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
+import app.revanced.util.fingerprint.methodOrThrow
+import app.revanced.util.fingerprint.resolvable
+
+private const val EXTENSION_CLASS_DESCRIPTOR =
+    "$MISC_PATH/SpoofPlayerParameterPatch;"
+
+@Suppress("unused")
+val spoofPlayerParameterPatch = bytecodePatch(
+    SPOOF_PLAYER_PARAMETER.title,
+    SPOOF_PLAYER_PARAMETER.summary
+) {
+    compatibleWith(COMPATIBLE_PACKAGE)
+
+    dependsOn(
+        settingsPatch,
+        videoInformationPatch,
+        playerResponseMethodHookPatch,
+    )
+
+    execute {
+
+        addPlayerResponseMethodHook(
+            Hook.PlayerParameter(
+                "$EXTENSION_CLASS_DESCRIPTOR->spoofParameter(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"
+            ),
+        )
+
+        // region fix for subtitles position
+
+        subtitleWindowFingerprint.methodOrThrow().addInstructions(
+            0,
+            """
+                invoke-static {p1, p2, p3, p4, p5}, $EXTENSION_CLASS_DESCRIPTOR->fixSubtitleWindowPosition(IIIZZ)[I
+                move-result-object v0
+                const/4 v1, 0x0
+                aget p1, v0, v1     # ap, anchor position
+                const/4 v1, 0x1
+                aget p2, v0, v1     # ah, horizontal anchor
+                const/4 v1, 0x2
+                aget p3, v0, v1     # av, vertical anchor
+            """
+        )
+
+        // endregion
+
+        // region fix for feature flags
+
+        if (ageRestrictedPlaybackFeatureFlagFingerprint.resolvable()) {
+            ageRestrictedPlaybackFeatureFlagFingerprint.injectLiteralInstructionBooleanCall(
+                AGE_RESTRICTED_PLAYBACK_FEATURE_FLAG,
+                "$EXTENSION_CLASS_DESCRIPTOR->forceDisableAgeRestrictedPlaybackFeatureFlag(Z)Z"
+            )
+        }
+
+        // endregion
+
+        addSwitchPreference(
+            CategoryType.MISC,
+            "revanced_spoof_player_parameter",
+            "true"
+        )
+
+        updatePatchStatus(SPOOF_PLAYER_PARAMETER)
+
+    }
+}
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
index 1b9c330e0..7fb107952 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt
@@ -157,6 +157,10 @@ internal enum class PatchList(
         "Spoof client",
         "Adds options to spoof the client to allow playback."
     ),
+    SPOOF_PLAYER_PARAMETER(
+        "Spoof player parameter",
+        "Adds options to spoof player parameter to allow playback."
+    ),
     TRANSLATIONS_FOR_YOUTUBE_MUSIC(
         "Translations for YouTube Music",
         "Add translations or remove string resources."
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt
index f8ffbd8be..e1b1bce52 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt
@@ -105,6 +105,14 @@ var trimSilenceSwitch = -1L
     private set
 var varispeedUnavailableTitle = -1L
     private set
+var ytFillSamples = -1L
+    private set
+var ytFillYouTubeMusic = -1L
+    private set
+var ytOutlineSamples = -1L
+    private set
+var ytOutlineYouTubeMusic = -1L
+    private set
 var ytmLogo = -1L
     private set
 var ytmLogoRingo2 = -1L
@@ -300,6 +308,22 @@ internal val sharedResourceIdPatch = resourcePatch(
             STRING,
             "varispeed_unavailable_title"
         ]
+        ytFillSamples = resourceMappings[
+            DRAWABLE,
+            "yt_fill_samples_vd_theme_24",
+        ]
+        ytFillYouTubeMusic = resourceMappings[
+            DRAWABLE,
+            "yt_fill_youtube_music_vd_theme_24",
+        ]
+        ytOutlineSamples = resourceMappings[
+            DRAWABLE,
+            "yt_outline_samples_vd_theme_24",
+        ]
+        ytOutlineYouTubeMusic = resourceMappings[
+            DRAWABLE,
+            "yt_outline_youtube_music_vd_theme_24",
+        ]
         ytmLogo = resourceMappings[
             DRAWABLE,
             "ytm_logo",
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt
index 275e96b4c..bc4b331aa 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt
@@ -13,6 +13,9 @@ import app.revanced.patches.music.utils.extension.Constants.SHARED_PATH
 import app.revanced.patches.music.utils.playbackSpeedFingerprint
 import app.revanced.patches.music.utils.playbackSpeedParentFingerprint
 import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
+import app.revanced.patches.music.video.playerresponse.Hook
+import app.revanced.patches.music.video.playerresponse.addPlayerResponseMethodHook
+import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch
 import app.revanced.patches.shared.mdxPlayerDirectorSetVideoStageFingerprint
 import app.revanced.patches.shared.videoLengthFingerprint
 import app.revanced.util.addStaticFieldToExtension
@@ -71,7 +74,10 @@ private var videoTimeConstructorInsertIndex = 2
 val videoInformationPatch = bytecodePatch(
     description = "videoInformationPatch",
 ) {
-    dependsOn(sharedResourceIdPatch)
+    dependsOn(
+        playerResponseMethodHookPatch,
+        sharedResourceIdPatch
+    )
 
     execute {
         fun addSeekInterfaceMethods(
@@ -241,7 +247,18 @@ val videoInformationPatch = bytecodePatch(
          * Set current video id
          */
         videoIdHook("$EXTENSION_CLASS_DESCRIPTOR->setVideoId(Ljava/lang/String;)V")
-
+        addPlayerResponseMethodHook(
+            Hook.VideoId(
+                "$EXTENSION_CLASS_DESCRIPTOR->setPlayerResponseVideoId(Ljava/lang/String;)V"
+            ),
+        )
+        // Call before any other video id hooks,
+        // so they can use VideoInformation and check if the video id is for a Short.
+        addPlayerResponseMethodHook(
+            Hook.PlayerParameterBeforeVideoId(
+                "$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponseParameter(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;"
+            )
+        )
         /**
          * Hook current playback speed
          */
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt
index 450ce37ea..1208c5e2b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt
@@ -11,8 +11,7 @@ private val PLAYER_PARAMETER_STARTS_WITH_PARAMETER_LIST = listOf(
     "[B",
     "Ljava/lang/String;", // Player parameters proto buffer.
     "Ljava/lang/String;", // PlaylistId.
-    "I",                  // PlaylistIndex.
-    "I"
+    "I"                  // PlaylistIndex.
 )
 
 /**
@@ -30,7 +29,7 @@ internal val playerParameterBuilderFingerprint = legacyFingerprint(
             return@custom false
         }
 
-        val startsWithMethodParameterList = parameterTypes.slice(0..5)
+        val startsWithMethodParameterList = parameterTypes.slice(0..4)
 
         parametersEqual(
             PLAYER_PARAMETER_STARTS_WITH_PARAMETER_LIST,
diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt
index 8570097d0..85e51a62b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt
@@ -1,22 +1,35 @@
 package app.revanced.patches.music.video.playerresponse
 
 import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
+import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
 import app.revanced.patcher.patch.bytecodePatch
 import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
+import app.revanced.patches.music.utils.extension.sharedExtensionPatch
 import app.revanced.patches.music.utils.playservice.is_7_03_or_greater
 import app.revanced.patches.music.utils.playservice.versionCheckPatch
 import app.revanced.util.fingerprint.methodOrThrow
 
+private val hooks = mutableSetOf()
+
+fun addPlayerResponseMethodHook(hook: Hook) {
+    hooks += hook
+}
+
 private const val REGISTER_VIDEO_ID = "p1"
+private const val REGISTER_PLAYER_PARAMETER = "p3"
 private const val REGISTER_PLAYLIST_ID = "p4"
 private const val REGISTER_PLAYLIST_INDEX = "p5"
 
 private lateinit var playerResponseMethod: MutableMethod
+private var numberOfInstructionsAdded = 0
 
 val playerResponseMethodHookPatch = bytecodePatch(
     description = "playerResponseMethodHookPatch"
 ) {
-    dependsOn(versionCheckPatch)
+    dependsOn(
+        sharedExtensionPatch,
+        versionCheckPatch,
+    )
 
     execute {
         playerResponseMethod = if (is_7_03_or_greater) {
@@ -25,16 +38,70 @@ val playerResponseMethodHookPatch = bytecodePatch(
             playerParameterBuilderLegacyFingerprint
         }.methodOrThrow()
     }
+
+    finalize {
+        fun hookVideoId(hook: Hook) {
+            playerResponseMethod.addInstruction(
+                0,
+                "invoke-static {$REGISTER_VIDEO_ID}, $hook",
+            )
+            numberOfInstructionsAdded++
+        }
+
+        fun hookVideoIdAndPlaylistId(hook: Hook) {
+            playerResponseMethod.addInstruction(
+                0,
+                "invoke-static {$REGISTER_VIDEO_ID, $REGISTER_PLAYLIST_ID, $REGISTER_PLAYLIST_INDEX}, $hook",
+            )
+            numberOfInstructionsAdded++
+        }
+
+        fun hookPlayerParameter(hook: Hook) {
+            playerResponseMethod.addInstructions(
+                0,
+                """
+                    invoke-static {$REGISTER_VIDEO_ID, v0}, $hook
+                    move-result-object v0
+                    """,
+            )
+            numberOfInstructionsAdded += 2
+        }
+
+        // Reverse the order in order to preserve insertion order of the hooks.
+        val beforeVideoIdHooks =
+            hooks.filterIsInstance().asReversed()
+        val videoIdHooks = hooks.filterIsInstance().asReversed()
+        val videoIdAndPlaylistIdHooks = hooks.filterIsInstance().asReversed()
+        val afterVideoIdHooks = hooks.filterIsInstance().asReversed()
+
+        // Add the hooks in this specific order as they insert instructions at the beginning of the method.
+        afterVideoIdHooks.forEach(::hookPlayerParameter)
+        videoIdAndPlaylistIdHooks.forEach(::hookVideoIdAndPlaylistId)
+        videoIdHooks.forEach(::hookVideoId)
+        beforeVideoIdHooks.forEach(::hookPlayerParameter)
+
+        playerResponseMethod.apply {
+            addInstruction(
+                0,
+                "move-object/from16 v0, $REGISTER_PLAYER_PARAMETER"
+            )
+            numberOfInstructionsAdded++
+
+            // Move the modified register back.
+            addInstruction(
+                numberOfInstructionsAdded,
+                "move-object/from16 $REGISTER_PLAYER_PARAMETER, v0"
+            )
+        }
+    }
 }
 
-fun hookPlayerResponse(
-    descriptor: String,
-    onlyVideoId: Boolean = false
-) {
-    val smaliInstruction = if (onlyVideoId)
-        "invoke-static {$REGISTER_VIDEO_ID}, $descriptor"
-    else
-        "invoke-static {$REGISTER_VIDEO_ID, $REGISTER_PLAYLIST_ID, $REGISTER_PLAYLIST_INDEX}, $descriptor"
+sealed class Hook(private val methodDescriptor: String) {
+    class VideoId(methodDescriptor: String) : Hook(methodDescriptor)
+    class VideoIdAndPlaylistId(methodDescriptor: String) : Hook(methodDescriptor)
 
-    playerResponseMethod.addInstruction(0, smaliInstruction)
+    class PlayerParameter(methodDescriptor: String) : Hook(methodDescriptor)
+    class PlayerParameterBeforeVideoId(methodDescriptor: String) : Hook(methodDescriptor)
+
+    override fun toString() = methodDescriptor
 }
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt
index 8c4c9cdbf..51c8c7e54 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt
@@ -5,9 +5,11 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith
 import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
 import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
 import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
 import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE
 import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH
 import app.revanced.patches.reddit.utils.patch.PatchList.HIDE_ADS
+import app.revanced.patches.reddit.utils.settings.is_2025_06_or_greater
 import app.revanced.patches.reddit.utils.settings.settingsPatch
 import app.revanced.patches.reddit.utils.settings.updatePatchStatus
 import app.revanced.util.findMutableMethodOf
@@ -78,22 +80,33 @@ val adsPatch = bytecodePatch(
         }
 
         // region Filter comment ads
-        classes.forEach { classDef ->
-            classDef.methods.forEach { method ->
-                if (method.isCommentAdsMethod()) {
-                    proxy(classDef)
-                        .mutableClass
-                        .findMutableMethodOf(method)
-                        .addInstructionsWithLabels(
-                            0, """
-                                invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideCommentAds()Z
-                                move-result v0
-                                if-eqz v0, :show
-                                return-void
-                                :show
-                                nop
-                                """
-                        )
+        fun MutableMethod.hook() =
+            addInstructionsWithLabels(
+                0, """
+                    invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideCommentAds()Z
+                    move-result v0
+                    if-eqz v0, :show
+                    return-void
+                    :show
+                    nop
+                    """
+            )
+        if (is_2025_06_or_greater) {
+            listOf(
+                commentAdCommentScreenAdViewFingerprint,
+                commentAdDetailListHeaderViewFingerprint
+            ).forEach { fingerprint ->
+                fingerprint.methodOrThrow().hook()
+            }
+        } else {
+            classes.forEach { classDef ->
+                classDef.methods.forEach { method ->
+                    if (method.isCommentAdsMethod()) {
+                        proxy(classDef)
+                            .mutableClass
+                            .findMutableMethodOf(method)
+                            .hook()
+                    }
                 }
             }
         }
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt
index fb5f5d21b..70df18225 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt
@@ -27,6 +27,28 @@ internal val adPostFingerprint = legacyFingerprint(
     },
 )
 
+internal val commentAdCommentScreenAdViewFingerprint = legacyFingerprint(
+    name = "commentAdCommentScreenAdViewFingerprint",
+    returnType = "V",
+    accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+    parameters = listOf("L"),
+    strings = listOf("ad"),
+    customFingerprint = { _, classDef ->
+        classDef.type.endsWith("/CommentScreenAdView;")
+    },
+)
+
+internal val commentAdDetailListHeaderViewFingerprint = legacyFingerprint(
+    name = "commentAdDetailListHeaderViewFingerprint",
+    returnType = "V",
+    accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+    parameters = listOf("L"),
+    strings = listOf("ad"),
+    customFingerprint = { _, classDef ->
+        classDef.type.endsWith("/DetailListHeaderView;")
+    },
+)
+
 internal val newAdPostFingerprint = legacyFingerprint(
     name = "newAdPostFingerprint",
     returnType = "L",
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/Fingerprints.kt
index e8ac32dc3..6f040ac24 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/Fingerprints.kt
@@ -1,5 +1,6 @@
 package app.revanced.patches.reddit.layout.navigation
 
+import app.revanced.util.containsLiteralInstruction
 import app.revanced.util.fingerprint.legacyFingerprint
 import app.revanced.util.getReference
 import app.revanced.util.indexOfFirstInstruction
@@ -8,6 +9,7 @@ import com.android.tools.smali.dexlib2.AccessFlags
 import com.android.tools.smali.dexlib2.Opcode
 import com.android.tools.smali.dexlib2.iface.Method
 import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
 
 internal val bottomNavScreenFingerprint = legacyFingerprint(
     name = "bottomNavScreenFingerprint",
@@ -62,13 +64,32 @@ internal val bottomNavScreenOnGlobalLayoutFingerprint = legacyFingerprint(
     }
 )
 
+private const val CHAT_BUTTON_MAGIC_NUMBER = 1906671695L
+
 internal val bottomNavScreenSetupBottomNavigationFingerprint = legacyFingerprint(
     name = "bottomNavScreenSetupBottomNavigationFingerprint",
-    returnType = "V",
     accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
     opcodes = listOf(Opcode.FILLED_NEW_ARRAY),
     customFingerprint = { method, classDef ->
-        classDef.type.startsWith("Lcom/reddit/launch/bottomnav/BottomNavScreen${'$'}setupBottomNavigation${'$'}") &&
-                method.name == "invoke"
+        method.containsLiteralInstruction(CHAT_BUTTON_MAGIC_NUMBER) &&
+                method.name == "invoke" &&
+                indexOfButtonsArrayInstruction(method) >= 0
     }
 )
+
+internal val composeBottomNavScreenFingerprint = legacyFingerprint(
+    name = "composeBottomNavScreenFingerprint",
+    accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+    parameters = listOf("Landroid/content/res/Resources;"),
+    opcodes = listOf(Opcode.FILLED_NEW_ARRAY),
+    customFingerprint = { method, classDef ->
+        classDef.type == "Lcom/reddit/launch/bottomnav/ComposeBottomNavScreen;" &&
+                indexOfButtonsArrayInstruction(method) >= 0
+    }
+)
+
+internal fun indexOfButtonsArrayInstruction(method: Method) =
+    method.indexOfFirstInstruction {
+        opcode == Opcode.FILLED_NEW_ARRAY &&
+                getReference()?.type?.startsWith("[Lcom/reddit/widget/bottomnav/") == true
+    }
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt
index dc86d0e56..473e2498d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt
@@ -8,12 +8,12 @@ import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACK
 import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH
 import app.revanced.patches.reddit.utils.patch.PatchList.HIDE_NAVIGATION_BUTTONS
 import app.revanced.patches.reddit.utils.settings.is_2024_26_or_greater
+import app.revanced.patches.reddit.utils.settings.is_2025_06_or_greater
 import app.revanced.patches.reddit.utils.settings.settingsPatch
 import app.revanced.patches.reddit.utils.settings.updatePatchStatus
 import app.revanced.util.fingerprint.methodOrThrow
 import app.revanced.util.fingerprint.resolvable
 import app.revanced.util.indexOfFirstInstructionOrThrow
-import app.revanced.util.indexOfFirstInstructionReversedOrThrow
 import com.android.tools.smali.dexlib2.Opcode
 import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
 import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@@ -34,17 +34,23 @@ val navigationButtonsPatch = bytecodePatch(
     execute {
 
         if (is_2024_26_or_greater) {
-            bottomNavScreenSetupBottomNavigationFingerprint.methodOrThrow().apply {
-                val arrayIndex = indexOfFirstInstructionReversedOrThrow(Opcode.FILLED_NEW_ARRAY)
-                val arrayRegister =
-                    getInstruction(arrayIndex + 1).registerA
+            val fingerprints = mutableListOf(bottomNavScreenSetupBottomNavigationFingerprint)
 
-                addInstructions(
-                    arrayIndex + 2, """
-                        invoke-static {v$arrayRegister}, $EXTENSION_CLASS_DESCRIPTOR->hideNavigationButtons([Ljava/lang/Object;)[Ljava/lang/Object;
-                        move-result-object v$arrayRegister
-                        """
-                )
+            if (is_2025_06_or_greater) fingerprints += composeBottomNavScreenFingerprint
+
+            fingerprints.forEach { fingerprint ->
+                fingerprint.methodOrThrow().apply {
+                    val arrayIndex = indexOfButtonsArrayInstruction(this)
+                    val arrayRegister =
+                        getInstruction(arrayIndex + 1).registerA
+
+                    addInstructions(
+                        arrayIndex + 2, """
+                            invoke-static {v$arrayRegister}, $EXTENSION_CLASS_DESCRIPTOR->hideNavigationButtons([Ljava/lang/Object;)[Ljava/lang/Object;
+                            move-result-object v$arrayRegister
+                            """
+                    )
+                }
             }
         } else {
             if (bottomNavScreenFingerprint.resolvable()) {
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt
index 13767b47b..13cc83e84 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt
@@ -1,9 +1,32 @@
 package app.revanced.patches.reddit.layout.screenshotpopup
 
+import app.revanced.patches.reddit.utils.resourceid.screenShotShareBanner
+import app.revanced.util.containsLiteralInstruction
 import app.revanced.util.fingerprint.legacyFingerprint
+import com.android.tools.smali.dexlib2.Opcode
 
+/**
+ * Reddit 2025.06.0 ~
+ */
 internal val screenshotTakenBannerFingerprint = legacyFingerprint(
     name = "screenshotTakenBannerFingerprint",
+    returnType = "L",
+    opcodes = listOf(
+        Opcode.CONST_4,
+        Opcode.IF_NE,
+    ),
+    customFingerprint = { method, classDef ->
+        method.containsLiteralInstruction(screenShotShareBanner) &&
+        classDef.type.startsWith("Lcom/reddit/sharing/screenshot/composables/") &&
+                method.name == "invoke"
+    }
+)
+
+/**
+ * ~ Reddit 2025.05.1
+ */
+internal val screenshotTakenBannerLegacyFingerprint = legacyFingerprint(
+    name = "screenshotTakenBannerLegacyFingerprint",
     returnType = "V",
     parameters = listOf("Landroidx/compose/runtime/", "I"),
     customFingerprint = { method, classDef ->
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt
index 48f009c59..a8ceffdb9 100644
--- a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt
@@ -7,9 +7,17 @@ import app.revanced.patcher.util.smali.ExternalLabel
 import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE
 import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH
 import app.revanced.patches.reddit.utils.patch.PatchList.DISABLE_SCREENSHOT_POPUP
+import app.revanced.patches.reddit.utils.resourceid.screenShotShareBanner
+import app.revanced.patches.reddit.utils.resourceid.sharedResourceIdPatch
+import app.revanced.patches.reddit.utils.settings.is_2025_06_or_greater
 import app.revanced.patches.reddit.utils.settings.settingsPatch
 import app.revanced.patches.reddit.utils.settings.updatePatchStatus
 import app.revanced.util.fingerprint.methodOrThrow
+import app.revanced.util.indexOfFirstInstructionOrThrow
+import app.revanced.util.indexOfFirstInstructionReversedOrThrow
+import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
+import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
 
 private const val EXTENSION_METHOD_DESCRIPTOR =
     "$PATCHES_PATH/ScreenshotPopupPatch;->disableScreenshotPopup()Z"
@@ -21,18 +29,39 @@ val screenshotPopupPatch = bytecodePatch(
 ) {
     compatibleWith(COMPATIBLE_PACKAGE)
 
-    dependsOn(settingsPatch)
+    dependsOn(
+        settingsPatch,
+        sharedResourceIdPatch,
+    )
 
     execute {
-        screenshotTakenBannerFingerprint.methodOrThrow().apply {
-            addInstructionsWithLabels(
-                0, """
-                    invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR
-                    move-result v0
-                    if-eqz v0, :dismiss
-                    return-void
-                    """, ExternalLabel("dismiss", getInstruction(0))
-            )
+
+        if (is_2025_06_or_greater) {
+            screenshotTakenBannerFingerprint.methodOrThrow().apply {
+                val literalIndex = indexOfFirstLiteralInstructionOrThrow(screenShotShareBanner)
+                val insertIndex = indexOfFirstInstructionReversedOrThrow(literalIndex, Opcode.CONST_4)
+                val insertRegister = getInstruction(insertIndex).registerA
+                val jumpIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.SGET_OBJECT)
+
+                addInstructionsWithLabels(
+                    insertIndex, """
+                        invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR
+                        move-result v$insertRegister
+                        if-nez v$insertRegister, :hidden
+                        """, ExternalLabel("hidden", getInstruction(jumpIndex))
+                )
+            }
+        } else {
+            screenshotTakenBannerLegacyFingerprint.methodOrThrow().apply {
+                addInstructionsWithLabels(
+                    0, """
+                        invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR
+                        move-result v0
+                        if-eqz v0, :dismiss
+                        return-void
+                        """, ExternalLabel("dismiss", getInstruction(0))
+                )
+            }
         }
 
         updatePatchStatus(
diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatch.kt
new file mode 100644
index 000000000..8fcffa95d
--- /dev/null
+++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatch.kt
@@ -0,0 +1,23 @@
+package app.revanced.patches.reddit.utils.resourceid
+
+import app.revanced.patcher.patch.resourcePatch
+import app.revanced.patches.shared.mapping.ResourceType.STRING
+import app.revanced.patches.shared.mapping.get
+import app.revanced.patches.shared.mapping.resourceMappingPatch
+import app.revanced.patches.shared.mapping.resourceMappings
+
+var screenShotShareBanner = -1L
+    private set
+
+internal val sharedResourceIdPatch = resourcePatch(
+    description = "sharedResourceIdPatch"
+) {
+    dependsOn(resourceMappingPatch)
+
+    execute {
+        screenShotShareBanner = resourceMappings[
+            STRING,
+            "screenshot_share_banner_title",
+        ]
+    }
+}
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt
index ff76bc9f7..655606d54 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt
@@ -259,13 +259,7 @@ val feedComponentsPatch = bytecodePatch(
         elementParserFingerprint.matchOrThrow(elementParserParentFingerprint).let {
             it.method.apply {
                 val freeRegister = implementation!!.registerCount - parameters.size - 2
-                val insertIndex = indexOfFirstInstructionOrThrow {
-                    val reference = getReference()
-
-                    reference?.parameterTypes?.size == 1 &&
-                            reference.parameterTypes.first() == "[B" &&
-                            reference.returnType.startsWith("L")
-                }
+                val insertIndex = indexOfBufferParserInstruction(this)
 
                 if (is_19_46_or_greater) {
                     val objectIndex = indexOfFirstInstructionReversedOrThrow(insertIndex,  Opcode.IGET_OBJECT)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/Fingerprints.kt
index 9ee3d8d9d..1e7609914 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/Fingerprints.kt
@@ -11,9 +11,13 @@ import app.revanced.patches.youtube.utils.resourceid.filterBarHeight
 import app.revanced.patches.youtube.utils.resourceid.horizontalCardList
 import app.revanced.patches.youtube.utils.resourceid.relatedChipCloudMargin
 import app.revanced.util.fingerprint.legacyFingerprint
+import app.revanced.util.getReference
+import app.revanced.util.indexOfFirstInstruction
 import app.revanced.util.or
 import com.android.tools.smali.dexlib2.AccessFlags
 import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.iface.Method
+import com.android.tools.smali.dexlib2.iface.reference.MethodReference
 
 internal val breakingNewsFingerprint = legacyFingerprint(
     name = "breakingNewsFingerprint",
@@ -90,9 +94,19 @@ internal val elementParserFingerprint = legacyFingerprint(
         Opcode.MOVE_RESULT_OBJECT,
         Opcode.IGET_OBJECT,
         Opcode.RETURN_OBJECT
-    )
+    ),
+    customFingerprint = { method, _ ->
+        indexOfBufferParserInstruction(method) >= 0
+    }
 )
 
+internal fun indexOfBufferParserInstruction(method: Method) =
+    method.indexOfFirstInstruction {
+        val reference = getReference()
+        reference?.parameterTypes?.firstOrNull() == "[B" &&
+                reference.returnType.startsWith("L")
+    }
+
 internal val elementParserParentFingerprint = legacyFingerprint(
     name = "elementParserParentFingerprint",
     returnType = "L",
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/ChangeFormFactorPatch.kt
similarity index 65%
rename from patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatch.kt
rename to patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/ChangeFormFactorPatch.kt
index 26ec6af6d..4b89c7782 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/ChangeFormFactorPatch.kt
@@ -1,4 +1,4 @@
-package app.revanced.patches.youtube.general.layoutswitch
+package app.revanced.patches.youtube.general.formfactor
 
 import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
 import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
@@ -6,30 +6,37 @@ import app.revanced.patcher.patch.bytecodePatch
 import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint
 import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
 import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH
-import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_LAYOUT
+import app.revanced.patches.youtube.utils.navigation.hookNavigationButtonCreated
+import app.revanced.patches.youtube.utils.navigation.navigationBarHookPatch
+import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_FORM_FACTOR
+import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch
 import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference
 import app.revanced.patches.youtube.utils.settings.settingsPatch
 import app.revanced.util.fingerprint.definingClassOrThrow
+import app.revanced.util.fingerprint.matchOrThrow
 import app.revanced.util.fingerprint.methodOrThrow
 import app.revanced.util.getReference
 import app.revanced.util.indexOfFirstInstructionOrThrow
-import app.revanced.util.indexOfFirstInstructionReversedOrThrow
 import com.android.tools.smali.dexlib2.Opcode
 import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
 import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
 import com.android.tools.smali.dexlib2.iface.reference.FieldReference
 
 private const val EXTENSION_CLASS_DESCRIPTOR =
-    "$GENERAL_PATH/LayoutSwitchPatch;"
+    "$GENERAL_PATH/ChangeFormFactorPatch;"
 
 @Suppress("unused")
-val layoutSwitchPatch = bytecodePatch(
-    CHANGE_LAYOUT.title,
-    CHANGE_LAYOUT.summary,
+val changeFormFactorPatch = bytecodePatch(
+    CHANGE_FORM_FACTOR.title,
+    CHANGE_FORM_FACTOR.summary,
 ) {
     compatibleWith(COMPATIBLE_PACKAGE)
 
-    dependsOn(settingsPatch)
+    dependsOn(
+        settingsPatch,
+        playerTypeHookPatch,
+        navigationBarHookPatch,
+    )
 
     execute {
 
@@ -53,27 +60,31 @@ val layoutSwitchPatch = bytecodePatch(
             )
         }
 
-        layoutSwitchFingerprint.methodOrThrow().apply {
-            val index = indexOfFirstInstructionReversedOrThrow(Opcode.IF_NEZ)
-            val register = getInstruction(index).registerA
+        widthDpUIFingerprint.matchOrThrow().let {
+            it.method.apply {
+                val index = it.patternMatch!!.startIndex
+                val register = getInstruction(index).registerA
 
-            addInstructions(
-                index, """
-                    invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->getWidthDp(I)I
-                    move-result v$register
-                    """
-            )
+                addInstructions(
+                    index, """
+                        invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->getWidthDp(I)I
+                        move-result v$register
+                        """
+                )
+            }
         }
 
+        hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR)
+
         // region add settings
 
         addPreference(
             arrayOf(
                 "PREFERENCE_SCREEN: GENERAL",
                 "PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS",
-                "SETTINGS: CHANGE_LAYOUT"
+                "SETTINGS: CHANGE_FORM_FACTOR"
             ),
-            CHANGE_LAYOUT
+            CHANGE_FORM_FACTOR
         )
 
         // endregion
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/Fingerprints.kt
similarity index 71%
rename from patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt
rename to patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/Fingerprints.kt
index f5d8e5b4b..569d6aed2 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/formfactor/Fingerprints.kt
@@ -1,4 +1,4 @@
-package app.revanced.patches.youtube.general.layoutswitch
+package app.revanced.patches.youtube.general.formfactor
 
 import app.revanced.util.fingerprint.legacyFingerprint
 import app.revanced.util.or
@@ -11,20 +11,16 @@ internal val formFactorEnumConstructorFingerprint = legacyFingerprint(
     strings = listOf(
         "UNKNOWN_FORM_FACTOR",
         "SMALL_FORM_FACTOR",
-        "LARGE_FORM_FACTOR"
+        "LARGE_FORM_FACTOR",
+        "AUTOMOTIVE_FORM_FACTOR",
     )
 )
 
-internal val layoutSwitchFingerprint = legacyFingerprint(
-    name = "layoutSwitchFingerprint",
+internal val widthDpUIFingerprint = legacyFingerprint(
+    name = "widthDpUIFingerprint",
     returnType = "I",
     accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC,
-    parameters = listOf("L"),
     opcodes = listOf(
-        Opcode.INVOKE_VIRTUAL,
-        Opcode.MOVE_RESULT_OBJECT,
-        Opcode.INVOKE_STATIC,
-        Opcode.MOVE_RESULT,
         Opcode.IF_NEZ,
         Opcode.CONST_4,
         Opcode.RETURN,
@@ -41,6 +37,11 @@ internal val layoutSwitchFingerprint = legacyFingerprint(
         Opcode.CONST_4,
         Opcode.RETURN,
         Opcode.CONST_4,
-        Opcode.RETURN
+        Opcode.RETURN,
+    ),
+    literals = listOf(
+        480L,
+        600L,
+        720L
     )
 )
\ No newline at end of file
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/Fingerprints.kt
index 6440b9e6e..163bc173a 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/Fingerprints.kt
@@ -91,7 +91,25 @@ internal val createSearchSuggestionsFingerprint = legacyFingerprint(
     returnType = "Landroid/view/View;",
     accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
     parameters = listOf("I", "Landroid/view/View;", "Landroid/view/ViewGroup;"),
-    strings = listOf("ss_rds")
+    strings = listOf("ss_rds"),
+    customFingerprint = { method, _ ->
+        indexOfIteratorInstruction(method) >= 0
+    }
+)
+
+internal fun indexOfIteratorInstruction(method: Method) =
+    method.indexOfFirstInstructionReversed {
+        opcode == Opcode.INVOKE_INTERFACE &&
+                getReference()?.toString() == "Ljava/util/Iterator;->next()Ljava/lang/Object;"
+    }
+
+// Flag is present in YouTube 19.16, but was not used until YouTube 19.43.
+// Related issue: https://github.com/inotia00/ReVanced_Extended/issues/2784
+internal const val SEARCH_FRAGMENT_FEATURE_FLAG = 45353159L
+
+internal val searchFragmentFeatureFlagFingerprint = legacyFingerprint(
+    name = "searchFragmentFeatureFlagFingerprint",
+    literals = listOf(SEARCH_FRAGMENT_FEATURE_FLAG),
 )
 
 internal val drawerContentViewConstructorFingerprint = legacyFingerprint(
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt
index 2d50bbb25..f928fd94d 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt
@@ -15,7 +15,7 @@ import app.revanced.patches.youtube.utils.castbutton.hookToolBarCastButton
 import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
 import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR
 import app.revanced.patches.youtube.utils.patch.PatchList.TOOLBAR_COMPONENTS
-import app.revanced.patches.youtube.utils.playservice.is_19_46_or_greater
+import app.revanced.patches.youtube.utils.playservice.is_19_16_or_greater
 import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
 import app.revanced.patches.youtube.utils.resourceid.actionBarRingoBackground
 import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch
@@ -31,6 +31,7 @@ import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT
 import app.revanced.util.doRecursively
 import app.revanced.util.findInstructionIndicesReversedOrThrow
 import app.revanced.util.findMethodOrThrow
+import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
 import app.revanced.util.fingerprint.matchOrThrow
 import app.revanced.util.fingerprint.methodCall
 import app.revanced.util.fingerprint.methodOrThrow
@@ -38,7 +39,6 @@ import app.revanced.util.fingerprint.mutableClassOrThrow
 import app.revanced.util.getReference
 import app.revanced.util.getWalkerMethod
 import app.revanced.util.indexOfFirstInstructionOrThrow
-import app.revanced.util.indexOfFirstInstructionReversedOrThrow
 import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
 import app.revanced.util.replaceLiteralInstructionCall
 import com.android.tools.smali.dexlib2.Opcode
@@ -46,6 +46,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
 import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
 import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
 import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
+import com.android.tools.smali.dexlib2.iface.reference.FieldReference
 import com.android.tools.smali.dexlib2.iface.reference.MethodReference
 import com.android.tools.smali.dexlib2.util.MethodUtil
 import org.w3c.dom.Element
@@ -266,36 +267,39 @@ val toolBarComponentsPatch = bytecodePatch(
         // region patch for hide search term thumbnail
 
         createSearchSuggestionsFingerprint.methodOrThrow().apply {
-            val literal = if (is_19_46_or_greater)
-                32L
-            else
-                40L
-            val relativeIndex = indexOfFirstLiteralInstructionOrThrow(literal)
-            val replaceIndex = indexOfFirstInstructionReversedOrThrow(relativeIndex) {
-                opcode == Opcode.INVOKE_VIRTUAL &&
-                        getReference()?.toString() == "Landroid/widget/ImageView;->setVisibility(I)V"
-            } - 1
-
-            val jumpIndex = indexOfFirstInstructionOrThrow(relativeIndex) {
+            val iteratorIndex = indexOfIteratorInstruction(this)
+            val replaceIndex = indexOfFirstInstructionOrThrow(iteratorIndex) {
+                opcode == Opcode.IGET_OBJECT &&
+                        getReference()?.type == "Landroid/widget/ImageView;"
+            }
+            val jumpIndex = indexOfFirstInstructionOrThrow(replaceIndex) {
                 opcode == Opcode.INVOKE_STATIC &&
                         getReference()?.toString() == "Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;"
             } + 4
-
             val replaceIndexInstruction = getInstruction(replaceIndex)
+            val freeRegister = replaceIndexInstruction.registerA
+            val classRegister = replaceIndexInstruction.registerB
             val replaceIndexReference =
                 getInstruction(replaceIndex).reference
 
             addInstructionsWithLabels(
                 replaceIndex + 1, """
                     invoke-static { }, $GENERAL_CLASS_DESCRIPTOR->hideSearchTermThumbnail()Z
-                    move-result v${replaceIndexInstruction.registerA}
-                    if-nez v${replaceIndexInstruction.registerA}, :hidden
-                    iget-object v${replaceIndexInstruction.registerA}, v${replaceIndexInstruction.registerB}, $replaceIndexReference
+                    move-result v$freeRegister
+                    if-nez v$freeRegister, :hidden
+                    iget-object v$freeRegister, v$classRegister, $replaceIndexReference
                     """, ExternalLabel("hidden", getInstruction(jumpIndex))
             )
             removeInstruction(replaceIndex)
         }
 
+        if (is_19_16_or_greater) {
+            searchFragmentFeatureFlagFingerprint.injectLiteralInstructionBooleanCall(
+                SEARCH_FRAGMENT_FEATURE_FLAG,
+                "$GENERAL_CLASS_DESCRIPTOR->hideSearchTermThumbnail(Z)Z"
+            )
+        }
+
         // endregion
 
         /*
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt
index 797477de2..a4cedb451 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt
@@ -4,12 +4,14 @@ import app.revanced.patcher.patch.booleanOption
 import app.revanced.patcher.patch.resourcePatch
 import app.revanced.patcher.patch.stringOption
 import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE
+import app.revanced.patches.youtube.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR
 import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_BRANDING_ICON_FOR_YOUTUBE
 import app.revanced.patches.youtube.utils.playservice.is_19_17_or_greater
 import app.revanced.patches.youtube.utils.playservice.is_19_32_or_greater
 import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater
 import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
 import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusIcon
+import app.revanced.patches.youtube.utils.settings.getBytecodeContext
 import app.revanced.patches.youtube.utils.settings.settingsPatch
 import app.revanced.util.ResourceGroup
 import app.revanced.util.Utils.printWarn
@@ -19,6 +21,7 @@ import app.revanced.util.copyFile
 import app.revanced.util.copyResources
 import app.revanced.util.getResourceGroup
 import app.revanced.util.underBarOrThrow
+import app.revanced.util.updatePatchStatus
 import app.revanced.util.valueOrThrow
 import org.w3c.dom.Element
 
@@ -247,11 +250,17 @@ val customBrandingIconPatch = resourcePatch(
                         resourcesNode.appendChild(style)
                     }
                 }
+
+                getBytecodeContext().apply {
+                    updatePatchStatus(PATCH_STATUS_CLASS_DESCRIPTOR, "OldSplashAnimation")
+                }
             }
 
             updatePatchStatusIcon(appIcon)
         }
 
+        CUSTOM_BRANDING_ICON_FOR_YOUTUBE.included = true
+
         // region fix app icon
 
         if (!is_19_34_or_greater) {
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt
index 23bdab4dd..441ec5f4c 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt
@@ -123,7 +123,7 @@ val seekbarComponentsPatch = bytecodePatch(
     execute {
 
         val restoreOldSplashAnimationIncluded = CUSTOM_BRANDING_ICON_FOR_YOUTUBE.included == true &&
-                customBrandingIconPatch.getBooleanOptionValue("restoreOldSplashAnimationOption").value == true
+                customBrandingIconPatch.getBooleanOptionValue("restoreOldSplashAnimation").value == true
 
         var settingArray = arrayOf(
             "PREFERENCE_SCREEN: PLAYER",
@@ -388,7 +388,7 @@ val seekbarComponentsPatch = bytecodePatch(
                 scaleNode.replaceChild(replacementNode, shapeNode)
             }
 
-        if (is_19_25_or_greater && !restoreOldSplashAnimationIncluded) {
+        if (is_19_25_or_greater) {
             // Add attribute and styles for splash screen custom color.
             // Using a style is the only way to selectively change just the seekbar fill color.
             //
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/Fingerprints.kt
index 3a579a948..7488b3d1e 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/Fingerprints.kt
@@ -234,7 +234,8 @@ internal val youtubeControlsOverlayFingerprint = legacyFingerprint(
     accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL,
     parameters = emptyList(),
     literals = listOf(
-        fadeDurationFast,
+        // Removed in YouTube 20.09.40+
+        // fadeDurationFast,
         insetOverlayViewLayout,
         scrimOverlay,
         // Removed in YouTube 20.02.38+
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/splash/DarkModeSplashScreenPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/splash/DarkModeSplashScreenPatch.kt
index 515dd45af..198a22138 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/splash/DarkModeSplashScreenPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/splash/DarkModeSplashScreenPatch.kt
@@ -1,8 +1,11 @@
 package app.revanced.patches.youtube.utils.fix.splash
 
 import app.revanced.patcher.patch.resourcePatch
+import app.revanced.patches.youtube.layout.branding.icon.customBrandingIconPatch
+import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_BRANDING_ICON_FOR_YOUTUBE
 import app.revanced.patches.youtube.utils.playservice.is_19_32_or_greater
 import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
+import app.revanced.util.getBooleanOptionValue
 import org.w3c.dom.Element
 
 val darkModeSplashScreenPatch = resourcePatch(
@@ -10,10 +13,10 @@ val darkModeSplashScreenPatch = resourcePatch(
 ) {
     dependsOn(versionCheckPatch)
 
-    execute {
-        if (!is_19_32_or_greater) {
-            return@execute
-        }
+    finalize {
+        val restoreOldSplashAnimationIncluded = is_19_32_or_greater &&
+                CUSTOM_BRANDING_ICON_FOR_YOUTUBE.included == true &&
+                customBrandingIconPatch.getBooleanOptionValue("restoreOldSplashAnimation").value == true
 
         /**
          * Fix the splash screen dark mode background color.
@@ -24,29 +27,57 @@ val darkModeSplashScreenPatch = resourcePatch(
          * This is a bug in unpatched YouTube.
          * Should always be applied even if the `Theme` patch is excluded.
          */
-        document("res/values-night/styles.xml").use { document ->
-            val resourcesNode = document.getElementsByTagName("resources").item(0) as Element
-            val childNodes = resourcesNode.childNodes
+        if (restoreOldSplashAnimationIncluded) {
+            document("res/values-night/styles.xml").use { document ->
+                val resourcesNode = document.getElementsByTagName("resources").item(0) as Element
+                val childNodes = resourcesNode.childNodes
 
-            for (i in 0 until childNodes.length) {
-                val node = childNodes.item(i) as? Element ?: continue
-                val nodeAttributeName = node.getAttribute("name")
-                if (nodeAttributeName.startsWith("Theme.YouTube.Launcher")) {
-                    val nodeAttributeParent = node.getAttribute("parent")
+                for (i in 0 until childNodes.length) {
+                    val node = childNodes.item(i) as? Element ?: continue
+                    val nodeAttributeName = node.getAttribute("name")
+                    if (nodeAttributeName.startsWith("Theme.YouTube.Launcher")) {
+                        val nodeAttributeParent = node.getAttribute("parent")
 
-                    val style = document.createElement("style")
-                    style.setAttribute("name", "Theme.YouTube.Home")
-                    style.setAttribute("parent", nodeAttributeParent)
+                        val style = document.createElement("style")
+                        style.setAttribute("name", "Theme.YouTube.Home")
+                        style.setAttribute("parent", nodeAttributeParent)
 
-                    val windowItem = document.createElement("item")
-                    windowItem.setAttribute("name", "android:windowBackground")
-                    windowItem.textContent = "@color/yt_black1"
-                    style.appendChild(windowItem)
+                        val windowItem = document.createElement("item")
+                        windowItem.setAttribute("name", "android:windowBackground")
+                        windowItem.textContent = "@color/yt_black1"
+                        style.appendChild(windowItem)
 
-                    resourcesNode.removeChild(node)
-                    resourcesNode.appendChild(style)
+                        resourcesNode.removeChild(node)
+                        resourcesNode.appendChild(style)
+                    }
                 }
             }
+        } else {
+            document("res/values-night-v27/styles.xml").use { document ->
+                // Create a night mode specific override for the splash screen background.
+                val style = document.createElement("style")
+                style.setAttribute("name", "Theme.YouTube.Home")
+                style.setAttribute("parent", "@style/Base.V27.Theme.YouTube.Home")
+
+                // Fix status and navigation bar showing white on some Android devices,
+                // such as SDK 28 Android 10 medium tablet.
+                val colorSplashBackgroundColor = "@color/yt_black1"
+                arrayOf(
+                    "android:navigationBarColor" to colorSplashBackgroundColor,
+                    "android:windowBackground" to colorSplashBackgroundColor,
+                    "android:colorBackground" to colorSplashBackgroundColor,
+                    "colorPrimaryDark" to colorSplashBackgroundColor,
+                    "android:windowLightStatusBar" to "false",
+                ).forEach { (name, value) ->
+                    val styleItem = document.createElement("item")
+                    styleItem.setAttribute("name", name)
+                    styleItem.textContent = value
+                    style.appendChild(styleItem)
+                }
+
+                val resourcesNode = document.getElementsByTagName("resources").item(0) as Element
+                resourcesNode.appendChild(style)
+            }
         }
     }
 }
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt
index 978104994..fa1662872 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt
@@ -146,4 +146,10 @@ internal val onesieEncryptionFeatureFlagFingerprint = legacyFingerprint(
     literals = listOf(ONESIE_ENCRYPTION_FEATURE_FLAG),
 )
 
+// YouTube 20.10 ~
+internal const val ONESIE_ENCRYPTION_ALTERNATIVE_FEATURE_FLAG = 45683169L
 
+internal val onesieEncryptionAlternativeFeatureFlagFingerprint = legacyFingerprint(
+    name = "onesieEncryptionAlternativeFeatureFlagFingerprint",
+    literals = listOf(ONESIE_ENCRYPTION_ALTERNATIVE_FEATURE_FLAG),
+)
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
index 4c5579b1f..0befb7493 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt
@@ -19,6 +19,7 @@ import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PAC
 import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAGE_NAME
 import app.revanced.patches.youtube.utils.patch.PatchList.SPOOF_STREAMING_DATA
 import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater
+import app.revanced.patches.youtube.utils.playservice.is_20_10_or_greater
 import app.revanced.patches.youtube.utils.playservice.versionCheckPatch
 import app.revanced.patches.youtube.utils.request.buildRequestPatch
 import app.revanced.patches.youtube.utils.request.hookBuildRequest
@@ -61,6 +62,10 @@ val spoofStreamingDataPatch = bytecodePatch(
 
     execute {
 
+        var settingArray = arrayOf(
+            "SETTINGS: SPOOF_STREAMING_DATA"
+        )
+
         // region Get replacement streams at player requests.
 
         hookBuildRequest("$EXTENSION_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;Ljava/util/Map;)V")
@@ -327,6 +332,15 @@ val spoofStreamingDataPatch = bytecodePatch(
                 ONESIE_ENCRYPTION_FEATURE_FLAG,
                 "$EXTENSION_CLASS_DESCRIPTOR->skipResponseEncryption(Z)Z"
             )
+
+            if (is_20_10_or_greater) {
+                onesieEncryptionAlternativeFeatureFlagFingerprint.injectLiteralInstructionBooleanCall(
+                    ONESIE_ENCRYPTION_ALTERNATIVE_FEATURE_FLAG,
+                    "$EXTENSION_CLASS_DESCRIPTOR->skipResponseEncryption(Z)Z"
+                )
+            }
+
+            settingArray += "SETTINGS: SKIP_RESPONSE_ENCRYPTION"
         }
 
         // endregion
@@ -339,9 +353,7 @@ val spoofStreamingDataPatch = bytecodePatch(
         )
 
         addPreference(
-            arrayOf(
-                "SETTINGS: SPOOF_STREAMING_DATA"
-            ),
+            settingArray,
             SPOOF_STREAMING_DATA
         )
     }
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch.kt
index 27a267ce9..5d351f608 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch.kt
@@ -109,8 +109,7 @@ val navigationBarHookPatch = bytecodePatch(
         hookNavigationButtonCreated = { extensionClassDescriptor ->
             navigationBarHookCallbackFingerprint.methodOrThrow().addInstruction(
                 0,
-                "invoke-static { p0, p1 }, " +
-                        "$extensionClassDescriptor->navigationTabCreated" +
+                "invoke-static { p0, p1 }, $extensionClassDescriptor->navigationTabCreated" +
                         "(${EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR}Landroid/view/View;)V",
             )
         }
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
index d56f8fb1d..c8dca6a6f 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt
@@ -21,9 +21,9 @@ internal enum class PatchList(
         "Bypass URL redirects",
         "Adds an option to bypass URL redirects and open the original URL directly."
     ),
-    CHANGE_LAYOUT(
-        "Change layout",
-        "Adds an option to change the dp in order to use a tablet or phone layout."
+    CHANGE_FORM_FACTOR(
+        "Change form factor",
+        "Adds an option to change the UI appearance to a phone, tablet, or automotive device."
     ),
     CHANGE_LIVE_RING_CLICK_ACTION(
         "Change live ring click action",
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt
index f7208bf7b..3a1f528b5 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt
@@ -1,6 +1,7 @@
 package app.revanced.patches.youtube.utils.playertype
 
 import app.revanced.patches.youtube.utils.resourceid.reelWatchPlayer
+import app.revanced.patches.youtube.utils.resourceid.toolbarContainerId
 import app.revanced.util.fingerprint.legacyFingerprint
 import app.revanced.util.getReference
 import app.revanced.util.indexOfFirstInstruction
@@ -9,6 +10,7 @@ import com.android.tools.smali.dexlib2.AccessFlags
 import com.android.tools.smali.dexlib2.Opcode
 import com.android.tools.smali.dexlib2.iface.Method
 import com.android.tools.smali.dexlib2.iface.reference.MethodReference
+import com.android.tools.smali.dexlib2.iface.reference.TypeReference
 
 internal val browseIdClassFingerprint = legacyFingerprint(
     name = "browseIdClassFingerprint",
@@ -61,6 +63,34 @@ internal val searchQueryClassFingerprint = legacyFingerprint(
     }
 )
 
+internal val toolbarLayoutFingerprint = legacyFingerprint(
+    name = "toolbarLayoutFingerprint",
+    literals = listOf(toolbarContainerId),
+    customFingerprint = { method, _ ->
+        method.name == "" &&
+                indexOfMainCollapsingToolbarLayoutInstruction(method) >= 0
+    }
+)
+
+internal fun indexOfMainCollapsingToolbarLayoutInstruction(method: Method) =
+    method.indexOfFirstInstruction {
+        opcode == Opcode.CHECK_CAST &&
+                getReference()?.type == "Lcom/google/android/apps/youtube/app/ui/actionbar/MainCollapsingToolbarLayout;"
+    }
+
+/**
+ * Matches to https://android.googlesource.com/platform/frameworks/support/+/9eee6ba/v7/appcompat/src/android/support/v7/widget/Toolbar.java#963
+ */
+internal val appCompatToolbarBackButtonFingerprint = legacyFingerprint(
+    name = "appCompatToolbarBackButtonFingerprint",
+    accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
+    returnType = "Landroid/graphics/drawable/Drawable;",
+    parameters = emptyList(),
+    customFingerprint =  { _, classDef ->
+        classDef.type == "Landroid/support/v7/widget/Toolbar;"
+    },
+)
+
 internal val videoStateFingerprint = legacyFingerprint(
     name = "videoStateFingerprint",
     returnType = "V",
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt
index 3bcb4e63f..6fe33c7d2 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt
@@ -5,6 +5,7 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
 import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
 import app.revanced.patcher.patch.PatchException
 import app.revanced.patcher.patch.bytecodePatch
+import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
 import app.revanced.patches.shared.litho.addLithoFilter
 import app.revanced.patches.shared.litho.lithoFilterPatch
 import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH
@@ -21,10 +22,13 @@ import app.revanced.util.getReference
 import app.revanced.util.indexOfFirstInstructionOrThrow
 import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
 import app.revanced.util.indexOfFirstStringInstructionOrThrow
+import com.android.tools.smali.dexlib2.AccessFlags
 import com.android.tools.smali.dexlib2.Opcode
+import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
 import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
 import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
 import com.android.tools.smali.dexlib2.iface.reference.FieldReference
+import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
 
 private const val EXTENSION_PLAYER_TYPE_HOOK_CLASS_DESCRIPTOR =
     "$UTILS_PATH/PlayerTypeHookPatch;"
@@ -32,6 +36,9 @@ private const val EXTENSION_PLAYER_TYPE_HOOK_CLASS_DESCRIPTOR =
 private const val EXTENSION_ROOT_VIEW_HOOK_CLASS_DESCRIPTOR =
     "$SHARED_PATH/RootView;"
 
+private const val EXTENSION_ROOT_VIEW_TOOLBAR_INTERFACE =
+    "$SHARED_PATH/RootView${'$'}AppCompatToolbarPatchInterface;"
+
 private const val FILTER_CLASS_DESCRIPTOR =
     "$COMPONENTS_PATH/RelatedVideoFilter;"
 
@@ -165,6 +172,53 @@ val playerTypeHookPatch = bytecodePatch(
 
         // endregion
 
+        // region patch for hook back button visibility
+
+        toolbarLayoutFingerprint.methodOrThrow().apply {
+            val index = indexOfMainCollapsingToolbarLayoutInstruction(this)
+            val register = getInstruction(index).registerA
+
+            addInstruction(
+                index + 1,
+                "invoke-static { v$register }, $EXTENSION_ROOT_VIEW_HOOK_CLASS_DESCRIPTOR->setToolbar(Landroid/widget/FrameLayout;)V"
+            )
+        }
+
+        // Add interface for extensions code to call obfuscated methods.
+        appCompatToolbarBackButtonFingerprint.matchOrThrow().let {
+            it.classDef.apply {
+                interfaces.add(EXTENSION_ROOT_VIEW_TOOLBAR_INTERFACE)
+
+                val definingClass = type
+                val obfuscatedMethodName = it.originalMethod.name
+                val returnType = "Landroid/graphics/drawable/Drawable;"
+
+                methods.add(
+                    ImmutableMethod(
+                        definingClass,
+                        "patch_getToolbarIcon",
+                        listOf(),
+                        returnType,
+                        AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
+                        null,
+                        null,
+                        MutableMethodImplementation(2),
+                    ).toMutable().apply {
+                        addInstructions(
+                            0,
+                            """
+                                 invoke-virtual { p0 }, $definingClass->$obfuscatedMethodName()$returnType
+                                 move-result-object v0
+                                 return-object v0
+                             """
+                        )
+                    }
+                )
+            }
+        }
+
+        // endregion
+
         addLithoFilter(FILTER_CLASS_DESCRIPTOR)
     }
 }
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playservice/VersionCheckPatch.kt
index 0015e98d5..ef0ca18fa 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playservice/VersionCheckPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playservice/VersionCheckPatch.kt
@@ -23,6 +23,8 @@ var is_19_09_or_greater = false
     private set
 var is_19_15_or_greater = false
     private set
+var is_19_16_or_greater = false
+    private set
 var is_19_17_or_greater = false
     private set
 var is_19_23_or_greater = false
@@ -59,6 +61,8 @@ var is_20_03_or_greater = false
     private set
 var is_20_05_or_greater = false
     private set
+var is_20_10_or_greater = false
+    private set
 
 val versionCheckPatch = resourcePatch(
     description = "versionCheckPatch",
@@ -83,6 +87,7 @@ val versionCheckPatch = resourcePatch(
         is_19_04_or_greater = 240502000 <= playStoreServicesVersion
         is_19_09_or_greater = 241002000 <= playStoreServicesVersion
         is_19_15_or_greater = 241602000 <= playStoreServicesVersion
+        is_19_16_or_greater = 241702000 <= playStoreServicesVersion
         is_19_17_or_greater = 241802000 <= playStoreServicesVersion
         is_19_23_or_greater = 242402000 <= playStoreServicesVersion
         is_19_25_or_greater = 242599000 <= playStoreServicesVersion
@@ -101,5 +106,6 @@ val versionCheckPatch = resourcePatch(
         is_20_02_or_greater = 250299000 <= playStoreServicesVersion
         is_20_03_or_greater = 250405000 <= playStoreServicesVersion
         is_20_05_or_greater = 250605000 <= playStoreServicesVersion
+        is_20_10_or_greater = 251105000 <= playStoreServicesVersion
     }
 }
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt
index 654130e91..6bf586431 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt
@@ -213,6 +213,8 @@ var tapBloomView = -1L
     private set
 var titleAnchor = -1L
     private set
+var toolbarContainerId = -1L
+    private set
 var toolTipContentView = -1L
     private set
 var totalTime = -1L
@@ -656,6 +658,10 @@ internal val sharedResourceIdPatch = resourcePatch(
             ID,
             "title_anchor"
         ]
+        toolbarContainerId = resourceMappings[
+            ID,
+            "toolbar_container"
+        ]
         toolTipContentView = resourceMappings[
             LAYOUT,
             "tooltip_content_view"
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt
index 032dbc5b6..d3fe3da10 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt
@@ -1,6 +1,7 @@
 package app.revanced.patches.youtube.utils.settings
 
 import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
+import app.revanced.patcher.patch.BytecodePatchContext
 import app.revanced.patcher.patch.bytecodePatch
 import app.revanced.patcher.patch.resourcePatch
 import app.revanced.patcher.patch.stringOption
@@ -39,6 +40,10 @@ private const val EXTENSION_INITIALIZATION_CLASS_DESCRIPTOR =
 private const val EXTENSION_THEME_METHOD_DESCRIPTOR =
     "$EXTENSION_UTILS_PATH/BaseThemeUtils;->setTheme(Ljava/lang/Enum;)V"
 
+private lateinit var bytecodeContext: BytecodePatchContext
+
+internal fun getBytecodeContext() = bytecodeContext
+
 private val settingsBytecodePatch = bytecodePatch(
     description = "settingsBytecodePatch"
 ) {
@@ -50,6 +55,7 @@ private val settingsBytecodePatch = bytecodePatch(
     )
 
     execute {
+        bytecodeContext = this
 
         // apply the current theme of the settings page
         themeSetterSystemFingerprint.methodOrThrow().apply {
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt
index 822f8e6aa..61b78022b 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt
@@ -10,7 +10,6 @@ private val PLAYER_PARAMETER_STARTS_WITH_PARAMETER_LIST = listOf(
     "[B",
     "Ljava/lang/String;", // Player parameters proto buffer.
     "Ljava/lang/String;", // PlaylistId.
-    "I",
     "I"
 )
 
@@ -19,7 +18,7 @@ internal val playerParameterBuilderFingerprint = legacyFingerprint(
     accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
     returnType = "L",
     strings = listOf("psps"),
-    // 19.22 and earlier parameters are:
+    // parameters in 18.29 ~ 19.22 :
     // "Ljava/lang/String;", // VideoId.
     // "[B",
     // "Ljava/lang/String;", // Player parameters proto buffer.
@@ -34,20 +33,39 @@ internal val playerParameterBuilderFingerprint = legacyFingerprint(
     // "Z",
     // "Z"
 
-    // 19.23+ parameters are:
+    // parameters in 19.23 ~ 20.09 :
     // "Ljava/lang/String;", // VideoId.
     // "[B",
     // "Ljava/lang/String;", // Player parameters proto buffer.
     // "Ljava/lang/String;", // PlaylistId.
     // "I",
     // "I",
-    // "L",
+    // "L", // New parameters added in 19.25.
     // "Ljava/util/Set;",
     // "Ljava/lang/String;",
     // "Ljava/lang/String;",
     // "L",
     // "Z", // Appears to indicate if the video id is being opened or is currently playing.
     // "Z",
+    // "Z",
+    // "Z"
+
+    // parameters in 20.10 ~ :
+    // "Ljava/lang/String;", // VideoId.
+    // "[B",
+    // "Ljava/lang/String;", // Player parameters proto buffer.
+    // "Ljava/lang/String;", // PlaylistId.
+    // "I",
+    // "Z", // New parameters added in 20.10.
+    // "I",
+    // "L", // New parameters added in 19.25.
+    // "Ljava/util/Set;",
+    // "Ljava/lang/String;",
+    // "Ljava/lang/String;",
+    // "L",
+    // "Z", // Appears to indicate if the video id is being opened or is currently playing.
+    // "Z",
+    // "Z",
     // "Z"
     customFingerprint = custom@{ method, _ ->
         val parameterTypes = method.parameterTypes
@@ -56,7 +74,7 @@ internal val playerParameterBuilderFingerprint = legacyFingerprint(
             return@custom false
         }
 
-        val startsWithMethodParameterList = parameterTypes.slice(0..5)
+        val startsWithMethodParameterList = parameterTypes.slice(0..4)
 
         parametersEqual(
             PLAYER_PARAMETER_STARTS_WITH_PARAMETER_LIST,
diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt
index cf1eb8951..9e8d23aeb 100644
--- a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt
+++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt
@@ -37,11 +37,18 @@ val playerResponseMethodHookPatch = bytecodePatch(
             ?: playerParameterBuilderLegacyFingerprint.methodOrThrow()
 
         playerResponseMethod.apply {
-            parameterIsShortAndOpeningOrPlaying = parameterTypes.indexOfFirst { it == "Z" } + 1
+            val setIndex = parameterTypes.indexOfFirst { it == "Ljava/util/Set;" }
+            val parameterSize = parameterTypes.size
+            val relativeIndex = parameterTypes.subList(setIndex, parameterSize - 1).indexOfFirst { it == "Z" }
+
+            // YouTube 18.29 ~ 19.22 : p11
+            // YouTube 19.23 ~ 20.09 : p12
+            // YouTube 20.10 ~ : p13
+            parameterIsShortAndOpeningOrPlaying = setIndex + relativeIndex + 1
             // On some app targets the method has too many registers pushing the parameters past v15.
             // If needed, move the parameters to 4-bit registers so they can be passed to extension.
             playerResponseMethodCopyRegisters = implementation!!.registerCount -
-                    parameterTypes.size + parameterIsShortAndOpeningOrPlaying > 15
+                    parameterSize + parameterIsShortAndOpeningOrPlaying > 15
         }
 
         if (playerResponseMethodCopyRegisters) {
diff --git a/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt b/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt
index 36dc8bdb2..87d432cf0 100644
--- a/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt
+++ b/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt
@@ -100,24 +100,24 @@ fun Pair.injectLiteralInstructionBooleanCall(
 ) {
     methodOrThrow().apply {
         val literalIndex = indexOfFirstLiteralInstruction(literal)
-        val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT)
-        val targetRegister = getInstruction(targetIndex).registerA
+        val index = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT)
+        val register = getInstruction(index).registerA
 
         val smaliInstruction =
             if (descriptor.startsWith("0x")) """
-                const/16 v$targetRegister, $descriptor
+                const/16 v$register, $descriptor
                 """
             else if (descriptor.endsWith("(Z)Z")) """
-                invoke-static {v$targetRegister}, $descriptor
-                move-result v$targetRegister
+                invoke-static/range { v$register .. v$register }, $descriptor
+                move-result v$register
                 """
             else """
                 invoke-static {}, $descriptor
-                move-result v$targetRegister
+                move-result v$register
                 """
 
         addInstructions(
-            targetIndex + 1,
+            index + 1,
             smaliInstruction
         )
     }
diff --git a/patches/src/main/resources/music/settings/host/values/arrays.xml b/patches/src/main/resources/music/settings/host/values/arrays.xml
index e56a20190..d019ea5b1 100644
--- a/patches/src/main/resources/music/settings/host/values/arrays.xml
+++ b/patches/src/main/resources/music/settings/host/values/arrays.xml
@@ -10,8 +10,6 @@
         @string/revanced_change_start_page_entry_liked_music
         @string/revanced_change_start_page_entry_podcasts
         @string/revanced_change_start_page_entry_samples
-        @string/revanced_change_start_page_entry_search
-        @string/revanced_change_start_page_entry_subscriptions
     
     
         ORIGINAL
@@ -23,8 +21,6 @@
         LIKED_MUSIC
         PODCASTS
         SAMPLES
-        SEARCH
-        SUBSCRIPTIONS
     
     
         @string/revanced_disable_music_video_in_album_redirect_type_entry_redirect
diff --git a/patches/src/main/resources/music/settings/host/values/strings.xml b/patches/src/main/resources/music/settings/host/values/strings.xml
index c8a34ae88..e7f6491dd 100644
--- a/patches/src/main/resources/music/settings/host/values/strings.xml
+++ b/patches/src/main/resources/music/settings/host/values/strings.xml
@@ -231,6 +231,18 @@ This does not bypass the age restriction. It just accepts it automatically."Hides the navigation bar.
     Hide navigation labels
     Hides the label below each navigation button.
+    Replace Samples button
+    Replaces the Samples button with the Search button.
+    Replace Upgrade button
+    Replaces the Upgrade button with the Settings button.
+    About replace button
+    "This feature is experimental.
+There are structural limitations of the patch, as Activities such as Search and Settings in YouTube Music are not public.
+
+Known issues:
+• When a replaced Activity such as Search and Settings is closed, the start page opens.
+
+Click to open the 'Change start page' settings."
 
 
     
@@ -501,6 +513,11 @@ Info:
     Android Music 5.29.53
     iOS Music 6.21
     iOS Music 7.04
+    Spoof player parameter
+    "Spoof the player parameter to prevent playback issues.
+
+Side effect:
+• Sometimes the subtitles are located at the top of the player instead of the bottom."
     Watch history type
     "• Original: Follows the watch history settings of Google account, but watch history may not work due to DNS or VPN.
 • Replace domain: Follows the watch history settings of Google account.
diff --git a/patches/src/main/resources/music/translations/el-rGR/strings.xml b/patches/src/main/resources/music/translations/el-rGR/strings.xml
index 9cbb285a0..44d4e10f7 100644
--- a/patches/src/main/resources/music/translations/el-rGR/strings.xml
+++ b/patches/src/main/resources/music/translations/el-rGR/strings.xml
@@ -451,6 +451,7 @@
     Android Music 5.29.53
     iOS Music 6.21
     iOS Music 7.04
+    Παραποίηση παραμέτρου προγράμματος αναπαραγωγής
     Τύπος ιστορικού παρακολούθησης
     "• Αρχικός: Ακολουθεί τις ρυθμίσεις ιστορικού παρακολούθησης του λογαριασμού Google σας, αλλά το ιστορικό παρακολούθησης μπορεί να μη λειτουργεί λόγω χρήσης VPN ή εναλλακτικού DNS.
 • Αντικατάσταση του domain: Ακολουθεί τις ρυθμίσεις ιστορικού παρακολούθησης του λογαριασμού Google σας.
diff --git a/patches/src/main/resources/music/translations/es-rES/strings.xml b/patches/src/main/resources/music/translations/es-rES/strings.xml
index cb7658349..6e6a966e7 100644
--- a/patches/src/main/resources/music/translations/es-rES/strings.xml
+++ b/patches/src/main/resources/music/translations/es-rES/strings.xml
@@ -1,6 +1,7 @@
 
 
     
+    RVX
     Restablecer a valores por defecto.
     
     Reiniciar para cargar el diseño normalmente
@@ -19,6 +20,8 @@
     Oculta los términos del contenedor de servicio.
     
     Barra de Acción
+    Cambiar posición de la barra de acción
+    Mueve la barra de acción debajo del botón de reproducción.
     Ocultar botones Me gusta y No me gusta
     Oculta los botones \"Me gusta\" y \"no me gusta\". No funciona en el diseño del reproductor antiguo.
     Ocultar botón de comentarios
@@ -31,6 +34,9 @@
     Oculta el botón Radio.
     Ocultar botón de compartir
     Oculta el botón Compartir.
+    Ocultar el botón de Canción / Video
+    "Oculta el botón de Canción / Video.
+(Este botón está disponible para algunos usuarios)"
     Ocultar etiquetas de botón de acción
     Oculta las etiquetas de los botones de acción.
     Reemplazar botón de acción de Descarga
@@ -48,6 +54,9 @@ Descarga %2$s desde el sitio web."
     
     Anuncios
     Ocultar anuncios en pantalla completa
+    "Oculta los anuncios en pantalla completa.
+• Algunas veces puede que se vea una pantalla negra vacía en lugar del inicio."
+    Los anuncios en pantalla completa son cerrados.
     Ocultar anuncios generales
     Oculta anuncios generales.
     Ocultar anuncios de música
@@ -56,6 +65,7 @@ Descarga %2$s desde el sitio web."
     Oculta etiqueta de promoción pagada.
     Ocultar popups de promoción premium
     Oculta popups de promoción premium.
+    Los mensajes emergentes promocionando premium son cerrados.
     Ocultar banner de renovación premium
     Oculta banner de renovación premium.
     Ocultar banner de alerta de promoción
@@ -87,6 +97,7 @@ Problemas conocidos:
     Ocultar menú de ir a episodios
     Ocultar menú de ir al podcast
     Ocultar menú Ayuda & Comentarios
+    Ocultar el menú de \"No interesado\"
     Ocultar el pin al menú de marcación rápida
     Ocultar menú de reproducción siguiente
     Ocultar menú de calidad
@@ -182,6 +193,10 @@ Esto no evita la restricción de edad. Solo la acepta automáticamente."
     Barra de navegación
     Activar color personalizado de la barra de navegación
+    Aplica un color en la barra de navegación.
+    Valor de color personalizado para la barra de navegación
+    Escribe un valor hexadecimal para el color de la barra de navegación.
+    Valor de color hexadecimal Inválido para la barra de navegación.
     Ocultar botón de Inicio
     Oculta el botón de Inicio.
     Ocultar botón de Samples
@@ -198,8 +213,37 @@ Esto no evita la restricción de edad. Solo la acepta automáticamente."Oculta las etiquetas en la barra de navegación.
     
     Reproductor
+    Añade un botón de siguiente al minireproductor
+    Añade un botón de siguiente pista al minireproductor.
+    Añade un botón de anterior al minireproductor
+    Añade un botón de pista anterior al minireproductor.
+    Cambia el color del minireproductor
+    Cambia el color del minireproductor para que concuerde con el color del reproductor de la pantalla completa.
+    Cambia el color de fondo del reproductor
+    Cambia el color de fondo del reproductor a un color personalizado.
+    Color principal del fondo del reproductor
+    "Escribe el valor hexadecimal del color primario del fondo del reproductor.
+
+Preferiblemente que sean colores oscuros, la aplicación no soporta temas claros."
+    Color secundario del fondo del reproductor
+    "Escriba el valor hexadecimal del color secundario del reproductor.
+
+Preferiblemente colores oscuros, la aplicación no soporta temas claros."
+    Color de fondo del reproductor inválido.
+    Cambiar posición de la barra de acción
+    Mueve la barra de acción por debajo del botón de reproducción.
+    Deshabilitar el gesto del minireproductor
+    Deshabilita el gesto de deslizamiento para cambiar de pista en el minireproductor.
     Desactivar gesto del reproductor
     Desactivar el gesto de deslizar para cambiar de pista en el reproductor.
+    Activar el minireproductor forzado
+    Habilita que se fuerce el minireproductor cuando se cambia a una nueva pista.
+    Habilitar el gesto de descarte del minireproductor
+    Habilita que se pueda deslizar hacia abajo en el minireproductor para descartarlo.
+    Habilita una barra de acción más gruesa
+    "Habilita una barra de acción más gruesa.
+
+Limitantes: Los segmentos de SponsorBlock no se mostrarán en la barra de acción."
     Activar modo zen
     Añade un tinte gris al reproductor de vídeo para reducir la fatiga visual.
     Activar el modo Zen en podcasts
@@ -212,6 +256,8 @@ Esto no evita la restricción de edad. Solo la acepta automáticamente."Oculta los botones marca de tiempo y emoji al escribir comentarios.
     Ocultar el botón Compartir en pantalla completa
     Oculta el botón Compartir en el reproductor de pantalla completa.
+    Ocultar el interruptor de Canción / Video
+    Oculta el interruptor de Canción / Video en el reproductor.
     Recordar estado de repetición
     Recuerda el estado de la repetición.
     Recordar estado aleatorio
@@ -373,11 +419,18 @@ Toca para ver cómo crear una clave de API."
     Desactivar audio DRC
     Deshabilita DRC (Dynamic Range Compression) aplicado al audio.
     Desactivar vídeo de música en el álbum
+    "Cuando un usuario no-premium reproduce una canción incluida en un álbum, el video musical a veces es reproducido en lugar de la canción oficial en sí.
+
+Encuentra y redirige la canción oficial, sí se detecta un video musical al reproducirse desde un álbum.
+
+Limitantes: Vídeos para niños puede que no se redirijan."
     Tipo de redirección
     Especifica cómo redirigir a la canción oficial.
     Redirigir
     Interruptor de Audio / Video
     Mantén pulsado el interruptor de Audio / Video
+    Desactiva el protocolo QUIC
+    "Deshabilita el protocolo QUIC de CronetEngine."
     Activar registro de depuración
     Imprime el registro de depuración.
     Incluir búfer en registro de depuración
@@ -387,10 +440,20 @@ Toca para ver cómo crear una clave de API."
     Desinfectar enlaces compartidos
     Elimina los parámetros de consulta de seguimiento de las URL al compartir enlaces.
     Falsificar cliente
+    Falsifica el cliente para prevenir problemas en la reproducción.
     Cliente por defecto
+    Define un cliente de por defecto para el falsificado.
     Android Music 4.27.53
     Android Music 5.29.53
     iOS Music 6.21
+    iOS Music 7.04
+    Tipo de vista del historial
+    "• Original: Sigue la configuración del registro del historial de su cuenta de Google, pero la opción del registro del historial pueda no funcionar al tener un DNS o VPN.
+• Reemplazar dominio: Sigue la configuración del registro del historial de su cuenta de Google.
+• Bloquear el registro del historial: El registro de historial está bloqueado."
+    Original
+    Reemplazar dominio
+    Bloquear el registro del historial
     Abrir ajustes predeterminados de la app
     Para abrir los enlaces de YouTube Music en RVX Music, activa \'Abrir enlaces soportados\' y activa las direcciones web soportadas.
     Abrir GmsCore
diff --git a/patches/src/main/resources/music/translations/id-rID/strings.xml b/patches/src/main/resources/music/translations/id-rID/strings.xml
index 4ab8adebe..9982deeb8 100644
--- a/patches/src/main/resources/music/translations/id-rID/strings.xml
+++ b/patches/src/main/resources/music/translations/id-rID/strings.xml
@@ -4,14 +4,14 @@
     RVX
     Atur ulang ke nilai default.
     
-    Mulai ulang untuk memuat layout secara normal
-    Refresh dan mulai ulang
+    Mulai ulang untuk memuat tata letak secara normal
+    Segarkan dan mulai ulang
     
     Akun
     Sembunyikan menu akun
-    Menyembunyikan elemen menu akun menggunakan filter custom.
-    Filter menu Akun
-    Daftar dari nama-nama menu akun ke filter, terpisah oleh garis baru.
+    Menyembunyikan elemen menu akun menggunakan penyaring khusus.
+    Penyaring menu Akun
+    Daftar nama menu akun yang akan disaring, dipisahkan dengan baris baru.
     Sembunyikan komponen kosong
     Menyembunyikan komponen kosong di menu akun.
     Sembunyikan handle
@@ -20,13 +20,13 @@
     Menyembunyikan kontainer ketentuan layanan.
     
     Bilah Tindakan
-    Ubah posisi bilah aksi
-    Pindahkan bilah aksi di bawah tombol putar.
-    Sembunyikan tombol Like dan Dislike
-    Menyembunyikan tombol Like dan Dislike. Itu tidak akan bekerja di layout player lama.
+    Ubah posisi bilah tindakan
+    Pindahkan bilah tindakan di bawah tombol putar.
+    Sembunyikan tombol Suka dan Tidak suka
+    Menyembunyikan tombol Suka dan Tidak suka. Itu tidak akan bekerja di tata letak pemutar lama.
     Sembunyikan tombol Komentar
     Menyembunyikan tombol Komentar.
-    Sembunyikan tombol Save
+    Sembunyikan tombol Simpan
     Menyembunyikan tombol Simpan.
     Sembunyikan tombol Unduh
     Menyembunyikan tombol Unduh.
@@ -37,27 +37,31 @@
     Sembunyikan tombol Lagu / Video
     "Sembunyikan tombol Lagu / Video.
 (Tombol ini hanya tersedia untuk sebagian pengguna)"
-    Sembunyikan tombol bilah tindakan
-    Menyembunyikan bilah dari tombol tindakan.
+    Sembunyikan label tombol tindakan
+    Menyembunyikan label tombol tindakan.
     Ganti tombol tindakan Unduh
-    "Tombol Unduh membuka Downloader eksternal kamu.
+    "Tombol Unduh membuka pengunduh eksternal Anda
 
-• Hanya menggantikan tombol Unduh di player.
-• Tidak bisa menggantikan tombol Unduh di menu flyout atau tab Library."
-    Nama paket downloader eksternal
-    Nama paket aplikasi downloader eksternal yang terinstal, seperti NewPipe atau YTDLnis.
-    Downloader eksternal
+• Hanya mengganti tombol tindakan Unduh di pemutar.
+• Tidak mengganti tombol Unduh di menu flyout atau tab Pustaka."
+    Nama paket pengunduh eksternal
+    Nama paket aplikasi pengunduh eksternal yang terpasang, seperti NewPipe atau YTDLnis.
+    Pengunduh eksternal
     Peringatan
-    "%1$s belum terinstall.
-Download %2$s dari website."
-    %s tidak diinstal. Silakan instal.
+    "%1$s belum terpasang.
+Harap unduh %2$s dari website."
+    %s tidak dipasang. Silakan pasang terlebih dahulu.
     
     Iklan
-    Sembunyikan iklan fullscreen
+    Sembunyikan iklan layar penuh
+    "Menyembunyikan iklan layar penuh.
+
+Keterbatasan:
+• Terkadang Anda mungkin melihat layar hitam kosong, bukan feed beranda."
     Iklan layar penuh ditutup.
     Sembunyikan Iklan Umum
     Menyembunyikan Iklan Umum.
-    Sembunyikan iklan musik
+    Sembunyikan iklan media
     Menyembunyikan iklan sebelum memutar musik.
     Sembunyikan label promosi berbayar
     Menyembunyikan label promosi berbayar.
@@ -67,289 +71,411 @@ Download %2$s dari website."
     Sembunyikan banner pembaruan premium
     Menyembunyikan banner pembaruan premium.
     Sembunyikan banner peringatan promosi
+    Sembunyikan banner peringatan promosi.
     
     Menu flyout
-    Tambah switch Trim silence
-    "Menambahkan tombol Trim silence ke menu flyout playback speed.
+    Tambah tombol Pangkas keheningan
+    "Menambahkan tombol Pangkas keheningan ke menu flyout kecepatan pemutaran.
 
 Info:
 • Fitur ini hanya untuk podcast.
-• Fitur ini masih dalam pengembangan, jadi ini tidak akan stabil."
+• Fitur ini masih dalam pengembangan, jadi mungkin tidak stabil."
     Aktifkan dialog ringkas
-    "Aktifkan dialog ringkas di ponsel.
+    "Mengaktifkan dialog ringkas di ponsel.
 
-Masalah yang diketahui:
-• Gambar album di Tab library juga menjadi lebih kecil.
+Keterbatasan:
+• Gambar album di tab Pustaka juga menjadi lebih kecil ketika diatur dalam model kotak.
 • Tata letak pengatur waktu tidur mungkin terlihat tidak biasa."
-    Sembunyikan tombol Like dan Dislike
+    Sembunyikan tombol Suka dan Tidak suka
     Sembunyikan komponen 3-kolom
     Sembunyikan menu tambahkan ke antrean
-    Sembunyikan menu teks
-    Sembunyikan menu hapus playlist
-    Sembunyikan menu abaikan antrean
+    Sembunyikan menu Teks
+    Sembunyikan menu Hapus playlist
+    Sembunyikan menu Abaikan antrean
     Sembunyikan menu Unduh
-    Sembunyikan menu edit playlist
+    Sembunyikan menu Edit playlist
     Sembunyikan menu Pergi ke album
     Sembunyikan menu Pergi ke artis
     Sembunyikan menu Pergi ke episode
     Sembunyikan menu Pergi ke podcast
-    Sembunyikan menu bantuan & saran
-    Sembunyikan menu putar berikutnya
-    Hide menu Kualitas
-    Sembunyikan menu hapus dari koleksi
-    Sembunyikan hapus dari menu playlist
-    Sembunyikan menu laporkan
-    Sembunyikan menu simpan episode untuk ditonton nanti
-    Sembunyikan menu simpan ke koleksi
-    Sembunyikan menu simpan ke playlist
-    Sembunyikan menu bagikan
-    Sembunyikan menu putar acak
-    Sembunyikan menu waktu tidur
-    Sembunyikan menu mulai radio
-    Sembunyikan menu statistik untuk nerds
-    Sembunyikan menu Subscribe / Unsubscribe
-    Sembunyikan menu kredit lagu
+    Sembunyikan menu Bantuan & saran
+    Sembunyikan menu Tidak tertarik
+    Sembunyikan Pin ke menu Panggil cepat
+    Sembunyikan menu Putar berikutnya
+    Sembunyikan menu Kualitas
+    Sembunyikan menu Hapus dari koleksi
+    Sembunyikan menu Hapus dari playlist
+    Sembunyikan menu Laporkan
+    Sembunyikan menu Simpan episode untuk nanti
+    Sembunyikan menu Simpan ke pustaka
+    Sembunyikan menu Simpan ke playlist
+    Sembunyikan menu Bagikan
+    Sembunyikan menu Putar acak
+    Sembunyikan menu Waktu tidur
+    Sembunyikan menu Mulai radio
+    Sembunyikan menu Statistik untuk nerds
+    Sembunyikan menu Berlangganan / Berhenti Berlangganan
+    Sembunyikan Buka pin dari menu panggilan cepat
+    Sembunyikan menu Lihat kredit lagu
     Lanjutkan menonton
-    Melanjutkan video dari waktu saat ini ketika berlaih ke YouTube.
+    Melanjutkan video dari waktu saat ini ketika beralih ke YouTube.
     Tonton di YouTube
-    Url video tidak valid.
-    Ganti menu hapus antrean
-    Menggantikan menu hapus antrean menjadi tonton di YouTube.
-    Ganti menu laporkan
-    Menggantikan menu laporkan dengan menu Kecepatan pemutaran.
-    Simpan laporkan di komentar
-    Mempertahankan menu laporkan di bagian komentar.
+    Url video tidak sah.
+    Ganti menu Hapus antrean
+    Menggantikan menu Hapus antrean dengan menu Tonton di YouTube.
+    Ganti menu Laporkan
+    Menggantikan menu Laporkan dengan menu Kecepatan pemutaran.
+    Simpan Laporkan di komentar
+    Menyimpan menu Laporkan di bagian komentar tetap utuh.
     
     Umum
     Ganti Halaman Awal
-    Select which page the app opens in.
+    Pilih di halaman mana aplikasi dibuka.
+    Bawaan
+    Tangga lagu
+    Episode untuk Nanti
     Jelajahi
-    Koleksi
-    Disable dislike redirection
-    Disables redirection to the next track when clicking the Dislike button.
+    Riwayat
+    Pustaka
+    Musik yang Disukai
+    Podcast
+    Sampel
+    Cari
+    Berlangganan
+    Nonaktifkan pengalihan tidak suka
+    Menonaktifkan pengalihan ke trek berikutnya ketika mengklik tombol Tidak Suka.
     Nonaktifkan teks otomatis paksa
-    Teks otomatis paksa yang dinonaktifkan.
+    Menonaktifkan teks otomatis agar tidak aktif.
     Aktifkan mode lanskap
-    Mengaktifkan masuk ke mode lanskap dengan rotasi layar di ponsel.
-    Aktifkan filter kustom
+    Mengaktifkan mode lanskap saat memutar layar ponsel.
+    Aktifkan filter khusus
     Mengaktifkan filter kustom untuk menyembunyikan komponen tata letak.
-    Edit filter kustom
+    Edit filter khusus
     
-    Memfilter nama komponen dengan baris yang dipisahkan.
-    Invalid custom filter: %s.
+    Daftar string pembangun jalur komponen yang akan disaring, dipisahkan dengan baris baru.
+    Filter khusus tidak sah: %s.
     Sembunyikan rak tombol
-    Menyembunyikan rak tombol dari beranda dan eksplorasi.
-    Sembunyikan rak korsel
-    Menyembunyikan rak korsel dari beranda dan eksplorasi.
-    Sembunyikan tombol cast
-    Menyembunyikan tombol cast.
+    Menyembunyikan rak tombol di feed.
+    Menyembunyikan rak carousel
+    Menyembunyikan rak carousel di feed.
+    Sembunyikan tombol Cast
+    Menyembunyikan tombol Cast.
     Sembunyikan bilah kategori
-    Menyembunyikan bilah kategori musik di bagian atas beranda.
-    Hide floating button
-    Hides the floating button in the Library tab.
-    Sembunyikan tombol riwayat
-    Menyembunyikan tombol riwayat di toolbar.
-    Hide Notifications button
-    Hides the Notifications button in the toolbar.
-    Hide playlist card shelf
-    Hides the playlist card shelf in the feed.
-    Hide Samples shelf
-    Hides the Samples shelf in the feed.
-    Hide sound search button
-    Hides the sound search button in the search bar.
-    Hide \'Tap to update\' button
-    Hides the \'Tap to update\' button.
-    Hide voice search button
-    Hides the voice search button in the search bar.
-    Restore old style library shelf
-    Returns the Library tab to the old style. (Experimental)
-    Remove viewer discretion dialog
-    "Removes the viewer discretion dialog.
-This does not bypass the age restriction. It just accepts it automatically."
+    Menyembunyikan bilah kategori.
+    Sembunyikan tombol mengambang
+    Menyembunyikan tombol mengambang di tab Pustaka.
+    Sembunyikan tombol Riwayat
+    Menyembunyikan Rombol riwayat di toolbar.
+    Sembunyikan tombol Pemberitahuan
+    Menyembunyikan tombol Pemberitahuan di toolbar.
+    Sembunyikan rak kartu playlist
+    Menyembunyikan rak kartu playlist di feed.
+    Sembunyikan rak Sampel
+    Menyembunyikan rak Sampel di feed.
+    Sembunyikan tombol pencarian suara
+    Menyembunyikan tombol pencarian suara di bilah pencarian.
+    Sembunyikan tombol Ketuk untuk memperbarui
+    Menyembunyikan tombol Ketuk untuk memperbarui.
+    Sembunyikan tombol pencarian suara
+    Menyembunyikan tombol pencarian suara di bilah pencarian.
+    Pulihkan tab Pustaka lama
+    Memulihkan tab Pustaka ke gaya lama. (Eksperimental)
+    Hapus dialog pembatasan penonton
+    "Menghapus dialog pembatasan penonton.
+Ini tidak mengabaikan pembatasan usia. Ini hanya menerimanya secara otomatis."
     Palsukan versi aplikasi
-    "Memalsukan versi klien ke versi lama
+    "Memalsukan versi klien ke versi lama.
 
 • Ini akan mengubah tampilan aplikasi, namun efek samping yang tidak diketahui mungkin terjadi.
-• Jika nanti dinonaktifkan, UI lama mungkin tetap ada hingga aplikasi dihapus data."
+• Jika nanti dinonaktifkan, UI lama mungkin tetap ada hingga data aplikasi dihapus."
     Target pemalsuan versi aplikasi
     Pilih target pemalsuan versi aplikasi.
-    
+    6.42.55 - Nonaktifkan lirik real-time
+    7.16.53 - Pulihkan bilah tindakan lama
     
     Bilah Navigasi
-    Hide Home button
-    Hides the Home button.
-    Hide Samples button
-    Hides the Samples button.
-    Hide Explore button
-    Hides the Explore button.
-    Hide Library button
-    Hides the Library button.
-    Hide Upgrade button
-    Hides the Upgrade button.
-    Menyembunyikan bilah navigasi
-    Sembunyikan bilah navigasi.
+    Aktifkan warna bilah navigasi khusus
+    Atur warna bilah navigasi.
+    Nilai warna bilah navigasi khusus
+    Masukkan kode hex dari warna bilah navigasi.
+    Nilai warna bilah navigasi tidak sah.
+    Sembunyikan tombol Beranda
+    Menyembunyikan tombol Beranda.
+    Sembunyikan tombol Sampel
+    Menyembunyikan tombol Sampel.
+    Sembunyikan tombol Jelajahi
+    Menyembunyikan tombol Jelajahi.
+    Sembunyikan tombol Pustaka
+    Menyembunyikan tombol Pustaka.
+    Sembunyikan tombol Upgrade
+    Menyembunyikan tombol Upgrade.
+    Sembunyikan bilah navigasi
+    Menyembunyikan bilah navigasi.
     Sembunyikan label navigasi
-    Menyembunyikan label di bilah navigasi.
+    Menyembunyikan label di bawah setiap tombol navigasi.
     
-    Player
-    Menonaktifkan gerakan pemutar
-    Nonaktifkan usap untuk mengubah trek di pemutar.
-    Aktifkan mode zen
-    Menambahkan rona abu-abu ke pemutar video untuk mengurangi ketegangan mata.
-    Enable Zen mode in podcasts
-    Also enables Zen mode for podcasts.
-    Hide channel guidelines
-    Hides the channel guidelines at the top of the comments section.
-    Sembunyikan filter overlay double-tap
-    Menyembunyikan overlay gelap yang muncul ketika double-tap to seek.
-    Hide timestamp and emoji buttons
+    Pemutar
+    Menambahkan tombol berikutnya pemutar mini
+    Menambahkan tombol trek berikutnya ke pemutar mini.
+    Tambahkan tombol sebelumnya miniplayer
+    Menambahkan tombol trek sebelumnya ke pemutar mini.
+    Ubah warna pemutar mini
+    Mengubah warna pemutar mini supaya sesuai dengan pemutar layar penuh.
+    Ubah warna latar belakang pemutar
+    Mengubah warna latar belakang pemutar ke warna khusus.
+    Warna primer latar belakang pemutar
+    "Masukkan kode hex dari warna primer latar belakang pemutar.
+
+Gunakan warna gelap jika memungkinkan, karena aplikasi ini tidak mendukung tema terang."
+    Warna sekunder latar belakang pemutar
+    "Masukkan kode hex warna sekunder latar belakang pemutar.
+
+Gunakan warna gelap jika memungkinkan, karena aplikasi ini tidak mendukung tema terang."
+    Warna latar belakang pemutar tidak sah.
+    Ubah posisi seekbar
+    Memindahkan seekbar di bawah tombol putar.
+    Nonaktifkan gerakan pemutar mini
+    Menonaktifkan usapan untuk mengubah trek di pemutar mini.
+    Nonaktifkan gerakan pemutar
+    Menonaktifkan usap untuk mengubah trek di pemutar.
+    Aktifkan pemutar mini paksa
+    Mengaktifkan pemutar mini paksa saat beralih ke trek baru.
+    Aktifkan usapan untuk menutup pemutar mini
+    Mengaktifkan gesek ke bawah untuk menutup pemutar mini.
+    Aktifkan seekbar tebal
+    "Mengaktifkan seekbar yang tebal.
+
+Keterbatasan: Segmen SponsorBlock tidak ditampilkan di seekbar."
+    Aktifkan mode Zen
+    Mengaktifkan warna abu-abu muda untuk latar belakang pemutar untuk mengurangi kelelahan mata.
+    Aktifkan mode Zen di podcast
+    Mengaktifkan mode Zen di podcast.
+    Sembunyikan pedoman saluran
+    Menyembunyikan panduan saluran di bagian atas komentar.
+    Sembunyikan penyaring hamparan ketuk dua kali
+    Menyembunyikan hamparan gelap yang muncul ketika mengetuk dua kali untuk menggeser.
+    Sembunyikan tombol emoji dan stempel waktu
     Hides the timestamp and emoji buttons when typing comments.
-    Hide fullscreen Share button
-    Hides the Share button in the fullscreen player.
+    Sembunyikan tombol Bagikan layar penuh
+    Menyembunyikan tombol Bagikan di pemutar layar penuh.
+    Sembunyikan tombol Lagu / Video
+    Menyembunyikan tombol Lagu / Video di pemutar.
     Ingat keadaan pengulangan
-    Mengingat keadaan pengulangan.
+    Mengingat keadaan tombol pengulangan.
     Ingat keadaan pengacakan
-    Mengingat keadaan pengacakan.
-    Restore old comments popup panels
-    Returns the comments popup panels to the old style.
-    Restore old player background
-    Returns the player background to the old style.
-    Restore old player layout
-    "Returns the player layout to the old style.
-Some features may not work properly in the old player layout."
+    Mengingat status tombol pengacakan.
+    Pulihkan panel sembulan komentar lama
+    Memulihkan panel sembulan komentar ke gaya lama.
+    Pulihkan latar belakang pemutar lama
+    Mengembalikan latar belakang pemutar ke gaya lama.
+    Pulihkan tata letak pemutar lama
+    "Memulihkan tata letak pemutar ke gaya lama.
+Beberapa fitur mungkin tidak berfungsi dengan baik dalam tata letak pemutar lama."
     
+    Menu pengaturan
+    Sembunyikan menu Pusat Keluarga
+    Sembunyikan menu Umum
+    Sembunyikan menu Pemutaran
+    Sembunyikan menu penghematan Data
+    Sembunyikan menu Unduhan & penyimpanan
+    Sembunyikan menu Pemberitahuan
+    Sembunyikan menu Privasi & data
+    Sembunyikan menu Rekomendasi
+    Sembunyikan menu Dapatkan Music premium
+    Sembunyikan menu Tentang
     
     Video
-    Edit kecepatan pemutaran kustom
+    Edit kecepatan pemutaran khusus
     Menambah atau mengubah kecepatan pemutaran yang tersedia.
-    Remember playback speed changes
+    Ingat perubahan kecepatan pemutaran
     Remembers the last playback speed selected.
     Tampilkan toast
-    Menunjukkan toast ketika mengubah playback speed semula.
-    Remember video quality changes
-    Remembers the last video quality selected.
+    Menampilkan toast saat mengubah kecepatan pemutaran bawaan.
+    Ingat perubahan kualitas video
+    Mengingat kualitas video terakhir yang dipilih.
     Tampilkan toast
-    Menunjukkan toast ketika mengubah playback speed semula.
-    Kecepatan pemutaran kustom tidak valid. Atur ulang ke nilai default.
-    Invalid custom playback speeds. Using default values.
-    Changing default speed to %s.
-    Changing default mobile data quality to %s.
-    Failed to set quality.
-    Changing default Wi-Fi quality to %s.
+    Menampilkan toast ketika mengubah kualitas video bawaan.
+    Kecepatan khusus harus kurang dari %sx.
+    Kecepatan pemutaran kustom tidak sah.
+    Mengubah kecepatan bawaan ke %s.
+    Mengubah kualitas data seluler bawaan ke %s.
+    Gagal mengatur kualitas.
+    Mengubah kualitas Wi-Fi bawaan ke %s.
     
     Return YouTube Dislike
-    Enable Return YouTube Dislike
-    Menunjukkan jumlah dislike pada video.
+    Aktifkan Return YouTube Dislike
+    Menampilkan jumlah tidak suka pada video.
     Dislike sebagai persentase
-    Alih-alih jumlah dislike, yang ditampilkan adalah persentase dislike.
-    Tombol like ringkas
-    Menyembunyikan pemisah tombol like.
-    Show a toast if API is unavailable
-    Shows a toast if the Return YouTube Dislike API is unavailable.
+    Menampilkan persentase dislike, bukan jumlah dislike.
+    Tombol suka ringkas
+    Menyembunyikan pemisah tombol suka.
+    Tampilkan perkiraan suka
+    Menampilkan perkiraan jumlah suka video.
+    Tampilkan toast jika API tidak tersedia
+    Menampilkan toast jika API Return YouTube Dislike tidak tersedia.
     Tentang
     ReturnYouTubeDislike.com
     Data disediakan oleh API Return YouTube Dislike. Tekan di sini untuk mempelajari lebih lanjut.
-    Dislikes are temporarily unavailable (API timed out).
-    Dislikes are unavailable (status %d).
+    Dislike untuk sementara tidak tersedia (API kehabisan waktu).
+    Dislike tidak tersedia (status %d).
     Dislike tidak tersedia (batas API client tercapai).
-    Dislikes are unavailable (%s).
+    Dislikes tidak tersedia (%s).
+    Disembunyikan
     
+    Return YouTube Username
+    Aktifkan Return YouTube Username
+    Mengganti penanganan dengan nama pengguna di komentar.
+    Format tampilan
+    Pilih format tampilan nama pengguna.
+    Nama pengguna
+    Nama pengguna (@penganganan)
+    \@penanganan (Nama pengguna)
+    Kunci API Data YouTube
+    Kunci pengembang untuk menggunakan API Data YouTube v3.
+    Tentang kunci API Data YouTube
+    "Kunci Pengembang API Data YouTube v3 diperlukan untuk mengganti penanganan dengan nama pengguna.
+
+Kuota harian untuk kunci API pada paket gratis adalah 10.000, dan 1 kuota digunakan untuk mengganti penanganan dengan nama pengguna untuk 1 komentar.
+
+Klik untuk melihat cara menerbitkan kunci API."
+    Terbitkan kunci pengembang API Data YouTube v3
+    1. Pergi ke <a href=%1$s>Buat proyek baru</a>.<br>2. Klik <b>tombol;</b> BUAT.<br>3. Pergi ke <a href=%2$s>API Data YouTube v3</a>.<br>4. Klik <b>tombol</b> NYALAKAN.<br>5. Klik <b>tombol</b> BUAT KREDENSIAL.<br>6. Pilih <b>pilihan</b> data Publik.<br>7. Klik <b>tombol</b> SELANJUTNYA.<br>8. Salin kunci API.<br><br>※ Kunci API tidak boleh dibagikan dengan orang lain, sehingga tidak disertakan dalam pengaturan Impor / Ekspor.
     
     SponsorBlock
-    Enable SponsorBlock
-    SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos.
-    Show a toast if API is unavailable
-    Shows a toast if the SponsorBlock API is unavailable.
-    Show a toast when skipping automatically
-    Shows a toast when a segment is automatically skipped.
-    Change API URL
-    The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing.
-    API URL reset.
-    API URL is invalid.
-    API URL changed.
-    Change segment behavior
+    Aktifkan SponsorBlock
+    SponsorBlock adalah sistem yang bersumber dari banyak orang untuk melewatkan bagian video YouTube yang mengganggu.
+    Tampilkan toast jika API tidak tersedia
+    Menampilkan toast jika API SponsorBlock tidak tersedia.
+    Menampilkan toast saat melewatkan secara otomatis
+    Menampilkan toast ketika segmen dilewati secara otomatis.
+    Ubah URL API
+    Alamat yang digunakan SponsorBlock untuk melakukan panggilan ke server. Jangan ubah ini kecuali Anda tahu apa yang Anda lakukan.
+    URL API diatur ulang.
+    URL API tidak sah.
+    URL API diubah.
+    Ubah perilaku segmen
     Sponsor
-    Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like.
-    Unpaid / Self Promotion
-    Similar to \'Sponsor\' except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with.
-    Interaction Reminder (Subscribe)
-    A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion.
-    Intermission / Intro Animation
-    An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information.
-    Endcards / Credits
-    Credits or when the YouTube endcards appear. Not for conclusions with information.
-    Preview / Recap / Hook
-    Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere.
-    Filler Tangent / Jokes
-    Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details.
-    Music: Non-Music Section
-    Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category.
-    Skipped sponsor.
-    Skipped self promotion.
-    Skipped annoying reminder.
-    Skipped intro.
-    Skipped intermission.
-    Skipped intermission.
-    Skipped outro.
-    Skipped preview.
-    Skipped preview.
-    Skipped recap.
-    Skipped filler.
-    Skipped a non-music section.
-    Skipped multiple segments.
-    Skip automatically
-    Disable
-    SponsorBlock is temporarily unavailable.
-    SponsorBlock is temporarily unavailable (status %d).
-    SponsorBlock is temporarily unavailable (API timed out).
-    Color:
-    Color changed.
-    Color reset.
-    Invalid color code. Color reset to default.
-    Reset color
-    Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms.
-    About
+    Promosi berbayar, rujukan berbayar, dan iklan langsung. Bukan untuk promosi diri atau promosi gratis untuk tujuan / kreator / situs web / produk yang mereka sukai.
+    Tidak Berbayar / Promosi Sendiri
+    Mirip dengan Sponsor, kecuali untuk promosi yang tidak berbayar atau promosi sendiri. Termasuk bagian tentang barang dagangan, donasi, atau informasi tentang dengan siapa mereka berkolaborasi.
+    Pengingat Interaksi (Berlangganan)
+    Pengingat singkat untuk menyukai, berlangganan, atau mengikuti mereka di tengah-tengah konten. Jika panjang atau tentang sesuatu yang spesifik, sebaiknya berada di bawah promosi diri.
+    Animasi Jeda / Intro
+    Selang waktu tanpa konten yang sebenarnya. Bisa berupa jeda, bingkai statis, atau animasi berulang. Tidak termasuk transisi yang berisi informasi.
+    Kartu Akhir / Kredit
+    Kredit atau saat kartu akhir YouTube muncul. Bukan untuk menyimpulkan informasi.
+    Pratinjau / Rekap / Pengait
+    Kumpulan klip yang menunjukkan apa yang akan datang atau apa yang terjadi dalam video atau dalam video lain dari suatu seri, di mana semua informasi diulang di tempat lain.
+    Pengisi Tidak Relevan / Lelucon
+    Adegan berbelit-belit yang ditambahkan hanya sebagai filler atau candaan yang tidak diperlukan untuk memahami isi utama video. Tidak termasuk bagian yang mengandung konteks atau detail latar belakang.
+    Musik: Bagian Non-Musik
+    Hanya untuk digunakan dalam video musik. Bagian video musik tanpa musik, yang belum tercakup dalam kategori lain.
+    Melewatkan sponsor.
+    Melewatkan promosi diri.
+    Melewatkan pengingat mengganggu.
+    Melewatkan intro.
+    Melewatkan jeda.
+    Melewatkan jeda.
+    Melewatkan outro.
+    Melewatkan pratinjau.
+    Melewatkan pratinjau.
+    Melewatkan rekap.
+    Melewatkan pengisi.
+    Melewatkan bagian non musik.
+    Melewatkan beberapa segmen.
+    Lewati otomatis
+    Nonaktifkan
+    SponsorBlock untuk sementara tidak tersedia.
+    SponsorBlock untuk sementara tidak tersedia (status %d).
+    SponsorBlock untuk sementara tidak tersedia (API habis batas waktunya).
+    Warna:
+    Warna diubah.
+    Warna diatur ulang.
+    Kode warna tidak sah.
+    Atur ulang warna
+    Data disediakan oleh API SponsorBlock. Tekan di sini untuk mempelajari lebih lanjut dan melihat unduhan untuk platform lain.
+    Tentang
     sponsor.ajay.app
     
-    Miscellaneous
+    Lain-lain
     Ekspor / Impor
-    Impor atau ekspor setelan sebagai teks.
-    Export settings to file
-    Import settings from file
-    Import / Export settings as text
-    Failed to export settings.
-    Settings were successfully exported.
+    Impor / Ekspor pengaturan Musik RVX.
+    Ekspor pengaturan ke berkas
+    Impor pengaturan dari berkas
+    Impor / Ekspor pengaturan sebagai teks
+    Gagal mengekspor pengaturan.
+    Pengaturan berhasil diekspor.
     Impor
     Salin
-    Import failed: %s.
-    Reset setelan ke default.
+    Impor gagal: %s.
+    Pengaturan diatur ulang ke bawaan.
     Setelan %d diimpor.
-    Reset
+    Atur ulang
     Setelan disalin ke papan klip.
-    Bypass gambar larangan wilayah
-    Mengganti domain yang ke blokir di negara tertentu sehingga playlist thumbnail, channel avatar, dll bisa di terima.
+    Abaikan pembatasan wilayah gambar
+    Mengabaikan domain yang diblokir di beberapa wilayah sehingga thumbnail playlist, avatar saluran, dll. dapat diterima.
     Ubah lembar berbagi
-    Mengubah dari lembar berbagi dalam aplikasi ke lembar berbagi sistem.
+    Mengubah lembar berbagi dalam aplikasi ke lembar berbagi sistem.
+    Menonaktifkan animasi percikan Cairo
+    Menonaktifkan animasi percikan Cairo saat aplikasi dimulai.
+    Nonaktifkan audio DRC
+    Menonaktifkan DRC (Dynamic Range Compression) yang diterapkan ke audio.
+    Nonaktifkan video musik dalam album
+    "Ketika pengguna non-premium memutar lagu yang termasuk dalam album, video musik terkadang diputar dan bukannya lagu resminya.
+
+Temukan lagu resmi jika video musik terdeteksi diputar dari album.
+
+Keterbatasan: Video anak-anak mungkin tidak dapat dialihkan."
+    Jenis pengalihan
+    Menentukan cara mengalihkan ke lagu resmi.
+    Alihkan
+    Ketuk tombol Lagu / Video
+    Ketuk dan tahan tombol Lagu / Video
+    Nonaktifkan protokol QUIC
+    "Menonaktifkan protokol QUIC CronetEngine."
     Aktifkan pencatatan debug
     Mencetak catatan debug.
-    Enable debug buffer logging
-    Includes the buffer in the debug log.
+    Mengaktifkan pencatatan buffer debug
+    Menyertakan buffer dalam log debug.
     Aktifkan codec opus
-    "Mengaktifkan codec audio opus alih-alih codec audio mp4a."
-    Sanitasi tautan berbagi
-    Menghapus parameter kueri pelacakan dari URL saat membagikan tautan.
-    Open GmsCore
-    Enable cloud messaging to receive notifications.
-    GmsCore is not installed. Install it.
-    Action needed
-    "GmsCore does not have permission to run in the background.
+    "Mengaktifkan codec OPUS jika respon pemutar menyertakannya.
 
-Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation.
+Info:
+• Klien YouTube Music terbaru menggunakan codec audio OPUS secara default.
+• Ini hanya berlaku untuk pengguna yang memalsukan dengan klien yang sangat lama."
+    Bersihkan tautan berbagi
+    Membersihkan tautan berbagi dengan menghapus parameter kueri pelacakan.
+    Palsukan klien
+    Memalsukan klien untuk mencegah masalah pemutaran.
+    Klien bawaan
+    Menentukan klien bawaan untuk pemalsuan.
+    Musik Android 4.27.53
+    Musik Android 5.29.53
+    Musik iOS 6.21
+    Musik iOS 7.04
+    Jenis riwayat tontonan
+    "• Asli: Mengikuti pengaturan riwayat tontonan akun Google, tetapi riwayat tontonan mungkin tidak berfungsi karena DNS atau VPN.
+• Ganti domain: Mengikuti pengaturan riwayat tontonan akun Google.
+• Blokir riwayat tontonan: Riwayat tontonan diblokir."
+    Asli
+    Ganti domain
+    Blokir riwayat tontonan
+    Buka pengaturan aplikasi bawaan
+    Untuk membuka tautan YouTube Music di RVX Music, aktifkan Buka tautan yang didukung dan aktifkan semua alamat web yang Didukung.
+    Buka pengaturan GmsCore
+    Untuk menerima pemberitahuan di RVX Music, aktifkan Cloud Messaging.
+    GmsCore tidak terpasang. Pasang dulu.
+    Diperlukan tindakan
+    "GmsCore tidak memiliki izin untuk berjalan di latar belakang.
 
-This is required for the app to work."
-    Open website
-    "GmsCore battery optimizations must be disabled to prevent issues.
+Ikuti panduan 'Don't kill my app!' untuk perangkat Anda, dan terapkan petunjuk pada pemasangan GmsCore.
 
-Tap on the continue button and disable battery optimizations."
-    Continue
+Hal ini diperlukan agar aplikasi dapat berfungsi."
+    Buka website
+    "Pengoptimalan baterai GmsCore harus dinonaktifkan untuk mencegah masalah.
+
+Menonaktifkan pengoptimalan baterai untuk GmsCore tidak akan berdampak negatif pada penggunaan baterai.
+
+Ketuk tombol lanjutkan dan izinkan perubahan pengoptimalan."
+    Lanjutkan
 
diff --git a/patches/src/main/resources/music/translations/in/strings.xml b/patches/src/main/resources/music/translations/in/strings.xml
index 4ab8adebe..9982deeb8 100644
--- a/patches/src/main/resources/music/translations/in/strings.xml
+++ b/patches/src/main/resources/music/translations/in/strings.xml
@@ -4,14 +4,14 @@
     RVX
     Atur ulang ke nilai default.
     
-    Mulai ulang untuk memuat layout secara normal
-    Refresh dan mulai ulang
+    Mulai ulang untuk memuat tata letak secara normal
+    Segarkan dan mulai ulang
     
     Akun
     Sembunyikan menu akun
-    Menyembunyikan elemen menu akun menggunakan filter custom.
-    Filter menu Akun
-    Daftar dari nama-nama menu akun ke filter, terpisah oleh garis baru.
+    Menyembunyikan elemen menu akun menggunakan penyaring khusus.
+    Penyaring menu Akun
+    Daftar nama menu akun yang akan disaring, dipisahkan dengan baris baru.
     Sembunyikan komponen kosong
     Menyembunyikan komponen kosong di menu akun.
     Sembunyikan handle
@@ -20,13 +20,13 @@
     Menyembunyikan kontainer ketentuan layanan.
     
     Bilah Tindakan
-    Ubah posisi bilah aksi
-    Pindahkan bilah aksi di bawah tombol putar.
-    Sembunyikan tombol Like dan Dislike
-    Menyembunyikan tombol Like dan Dislike. Itu tidak akan bekerja di layout player lama.
+    Ubah posisi bilah tindakan
+    Pindahkan bilah tindakan di bawah tombol putar.
+    Sembunyikan tombol Suka dan Tidak suka
+    Menyembunyikan tombol Suka dan Tidak suka. Itu tidak akan bekerja di tata letak pemutar lama.
     Sembunyikan tombol Komentar
     Menyembunyikan tombol Komentar.
-    Sembunyikan tombol Save
+    Sembunyikan tombol Simpan
     Menyembunyikan tombol Simpan.
     Sembunyikan tombol Unduh
     Menyembunyikan tombol Unduh.
@@ -37,27 +37,31 @@
     Sembunyikan tombol Lagu / Video
     "Sembunyikan tombol Lagu / Video.
 (Tombol ini hanya tersedia untuk sebagian pengguna)"
-    Sembunyikan tombol bilah tindakan
-    Menyembunyikan bilah dari tombol tindakan.
+    Sembunyikan label tombol tindakan
+    Menyembunyikan label tombol tindakan.
     Ganti tombol tindakan Unduh
-    "Tombol Unduh membuka Downloader eksternal kamu.
+    "Tombol Unduh membuka pengunduh eksternal Anda
 
-• Hanya menggantikan tombol Unduh di player.
-• Tidak bisa menggantikan tombol Unduh di menu flyout atau tab Library."
-    Nama paket downloader eksternal
-    Nama paket aplikasi downloader eksternal yang terinstal, seperti NewPipe atau YTDLnis.
-    Downloader eksternal
+• Hanya mengganti tombol tindakan Unduh di pemutar.
+• Tidak mengganti tombol Unduh di menu flyout atau tab Pustaka."
+    Nama paket pengunduh eksternal
+    Nama paket aplikasi pengunduh eksternal yang terpasang, seperti NewPipe atau YTDLnis.
+    Pengunduh eksternal
     Peringatan
-    "%1$s belum terinstall.
-Download %2$s dari website."
-    %s tidak diinstal. Silakan instal.
+    "%1$s belum terpasang.
+Harap unduh %2$s dari website."
+    %s tidak dipasang. Silakan pasang terlebih dahulu.
     
     Iklan
-    Sembunyikan iklan fullscreen
+    Sembunyikan iklan layar penuh
+    "Menyembunyikan iklan layar penuh.
+
+Keterbatasan:
+• Terkadang Anda mungkin melihat layar hitam kosong, bukan feed beranda."
     Iklan layar penuh ditutup.
     Sembunyikan Iklan Umum
     Menyembunyikan Iklan Umum.
-    Sembunyikan iklan musik
+    Sembunyikan iklan media
     Menyembunyikan iklan sebelum memutar musik.
     Sembunyikan label promosi berbayar
     Menyembunyikan label promosi berbayar.
@@ -67,289 +71,411 @@ Download %2$s dari website."
     Sembunyikan banner pembaruan premium
     Menyembunyikan banner pembaruan premium.
     Sembunyikan banner peringatan promosi
+    Sembunyikan banner peringatan promosi.
     
     Menu flyout
-    Tambah switch Trim silence
-    "Menambahkan tombol Trim silence ke menu flyout playback speed.
+    Tambah tombol Pangkas keheningan
+    "Menambahkan tombol Pangkas keheningan ke menu flyout kecepatan pemutaran.
 
 Info:
 • Fitur ini hanya untuk podcast.
-• Fitur ini masih dalam pengembangan, jadi ini tidak akan stabil."
+• Fitur ini masih dalam pengembangan, jadi mungkin tidak stabil."
     Aktifkan dialog ringkas
-    "Aktifkan dialog ringkas di ponsel.
+    "Mengaktifkan dialog ringkas di ponsel.
 
-Masalah yang diketahui:
-• Gambar album di Tab library juga menjadi lebih kecil.
+Keterbatasan:
+• Gambar album di tab Pustaka juga menjadi lebih kecil ketika diatur dalam model kotak.
 • Tata letak pengatur waktu tidur mungkin terlihat tidak biasa."
-    Sembunyikan tombol Like dan Dislike
+    Sembunyikan tombol Suka dan Tidak suka
     Sembunyikan komponen 3-kolom
     Sembunyikan menu tambahkan ke antrean
-    Sembunyikan menu teks
-    Sembunyikan menu hapus playlist
-    Sembunyikan menu abaikan antrean
+    Sembunyikan menu Teks
+    Sembunyikan menu Hapus playlist
+    Sembunyikan menu Abaikan antrean
     Sembunyikan menu Unduh
-    Sembunyikan menu edit playlist
+    Sembunyikan menu Edit playlist
     Sembunyikan menu Pergi ke album
     Sembunyikan menu Pergi ke artis
     Sembunyikan menu Pergi ke episode
     Sembunyikan menu Pergi ke podcast
-    Sembunyikan menu bantuan & saran
-    Sembunyikan menu putar berikutnya
-    Hide menu Kualitas
-    Sembunyikan menu hapus dari koleksi
-    Sembunyikan hapus dari menu playlist
-    Sembunyikan menu laporkan
-    Sembunyikan menu simpan episode untuk ditonton nanti
-    Sembunyikan menu simpan ke koleksi
-    Sembunyikan menu simpan ke playlist
-    Sembunyikan menu bagikan
-    Sembunyikan menu putar acak
-    Sembunyikan menu waktu tidur
-    Sembunyikan menu mulai radio
-    Sembunyikan menu statistik untuk nerds
-    Sembunyikan menu Subscribe / Unsubscribe
-    Sembunyikan menu kredit lagu
+    Sembunyikan menu Bantuan & saran
+    Sembunyikan menu Tidak tertarik
+    Sembunyikan Pin ke menu Panggil cepat
+    Sembunyikan menu Putar berikutnya
+    Sembunyikan menu Kualitas
+    Sembunyikan menu Hapus dari koleksi
+    Sembunyikan menu Hapus dari playlist
+    Sembunyikan menu Laporkan
+    Sembunyikan menu Simpan episode untuk nanti
+    Sembunyikan menu Simpan ke pustaka
+    Sembunyikan menu Simpan ke playlist
+    Sembunyikan menu Bagikan
+    Sembunyikan menu Putar acak
+    Sembunyikan menu Waktu tidur
+    Sembunyikan menu Mulai radio
+    Sembunyikan menu Statistik untuk nerds
+    Sembunyikan menu Berlangganan / Berhenti Berlangganan
+    Sembunyikan Buka pin dari menu panggilan cepat
+    Sembunyikan menu Lihat kredit lagu
     Lanjutkan menonton
-    Melanjutkan video dari waktu saat ini ketika berlaih ke YouTube.
+    Melanjutkan video dari waktu saat ini ketika beralih ke YouTube.
     Tonton di YouTube
-    Url video tidak valid.
-    Ganti menu hapus antrean
-    Menggantikan menu hapus antrean menjadi tonton di YouTube.
-    Ganti menu laporkan
-    Menggantikan menu laporkan dengan menu Kecepatan pemutaran.
-    Simpan laporkan di komentar
-    Mempertahankan menu laporkan di bagian komentar.
+    Url video tidak sah.
+    Ganti menu Hapus antrean
+    Menggantikan menu Hapus antrean dengan menu Tonton di YouTube.
+    Ganti menu Laporkan
+    Menggantikan menu Laporkan dengan menu Kecepatan pemutaran.
+    Simpan Laporkan di komentar
+    Menyimpan menu Laporkan di bagian komentar tetap utuh.
     
     Umum
     Ganti Halaman Awal
-    Select which page the app opens in.
+    Pilih di halaman mana aplikasi dibuka.
+    Bawaan
+    Tangga lagu
+    Episode untuk Nanti
     Jelajahi
-    Koleksi
-    Disable dislike redirection
-    Disables redirection to the next track when clicking the Dislike button.
+    Riwayat
+    Pustaka
+    Musik yang Disukai
+    Podcast
+    Sampel
+    Cari
+    Berlangganan
+    Nonaktifkan pengalihan tidak suka
+    Menonaktifkan pengalihan ke trek berikutnya ketika mengklik tombol Tidak Suka.
     Nonaktifkan teks otomatis paksa
-    Teks otomatis paksa yang dinonaktifkan.
+    Menonaktifkan teks otomatis agar tidak aktif.
     Aktifkan mode lanskap
-    Mengaktifkan masuk ke mode lanskap dengan rotasi layar di ponsel.
-    Aktifkan filter kustom
+    Mengaktifkan mode lanskap saat memutar layar ponsel.
+    Aktifkan filter khusus
     Mengaktifkan filter kustom untuk menyembunyikan komponen tata letak.
-    Edit filter kustom
+    Edit filter khusus
     
-    Memfilter nama komponen dengan baris yang dipisahkan.
-    Invalid custom filter: %s.
+    Daftar string pembangun jalur komponen yang akan disaring, dipisahkan dengan baris baru.
+    Filter khusus tidak sah: %s.
     Sembunyikan rak tombol
-    Menyembunyikan rak tombol dari beranda dan eksplorasi.
-    Sembunyikan rak korsel
-    Menyembunyikan rak korsel dari beranda dan eksplorasi.
-    Sembunyikan tombol cast
-    Menyembunyikan tombol cast.
+    Menyembunyikan rak tombol di feed.
+    Menyembunyikan rak carousel
+    Menyembunyikan rak carousel di feed.
+    Sembunyikan tombol Cast
+    Menyembunyikan tombol Cast.
     Sembunyikan bilah kategori
-    Menyembunyikan bilah kategori musik di bagian atas beranda.
-    Hide floating button
-    Hides the floating button in the Library tab.
-    Sembunyikan tombol riwayat
-    Menyembunyikan tombol riwayat di toolbar.
-    Hide Notifications button
-    Hides the Notifications button in the toolbar.
-    Hide playlist card shelf
-    Hides the playlist card shelf in the feed.
-    Hide Samples shelf
-    Hides the Samples shelf in the feed.
-    Hide sound search button
-    Hides the sound search button in the search bar.
-    Hide \'Tap to update\' button
-    Hides the \'Tap to update\' button.
-    Hide voice search button
-    Hides the voice search button in the search bar.
-    Restore old style library shelf
-    Returns the Library tab to the old style. (Experimental)
-    Remove viewer discretion dialog
-    "Removes the viewer discretion dialog.
-This does not bypass the age restriction. It just accepts it automatically."
+    Menyembunyikan bilah kategori.
+    Sembunyikan tombol mengambang
+    Menyembunyikan tombol mengambang di tab Pustaka.
+    Sembunyikan tombol Riwayat
+    Menyembunyikan Rombol riwayat di toolbar.
+    Sembunyikan tombol Pemberitahuan
+    Menyembunyikan tombol Pemberitahuan di toolbar.
+    Sembunyikan rak kartu playlist
+    Menyembunyikan rak kartu playlist di feed.
+    Sembunyikan rak Sampel
+    Menyembunyikan rak Sampel di feed.
+    Sembunyikan tombol pencarian suara
+    Menyembunyikan tombol pencarian suara di bilah pencarian.
+    Sembunyikan tombol Ketuk untuk memperbarui
+    Menyembunyikan tombol Ketuk untuk memperbarui.
+    Sembunyikan tombol pencarian suara
+    Menyembunyikan tombol pencarian suara di bilah pencarian.
+    Pulihkan tab Pustaka lama
+    Memulihkan tab Pustaka ke gaya lama. (Eksperimental)
+    Hapus dialog pembatasan penonton
+    "Menghapus dialog pembatasan penonton.
+Ini tidak mengabaikan pembatasan usia. Ini hanya menerimanya secara otomatis."
     Palsukan versi aplikasi
-    "Memalsukan versi klien ke versi lama
+    "Memalsukan versi klien ke versi lama.
 
 • Ini akan mengubah tampilan aplikasi, namun efek samping yang tidak diketahui mungkin terjadi.
-• Jika nanti dinonaktifkan, UI lama mungkin tetap ada hingga aplikasi dihapus data."
+• Jika nanti dinonaktifkan, UI lama mungkin tetap ada hingga data aplikasi dihapus."
     Target pemalsuan versi aplikasi
     Pilih target pemalsuan versi aplikasi.
-    
+    6.42.55 - Nonaktifkan lirik real-time
+    7.16.53 - Pulihkan bilah tindakan lama
     
     Bilah Navigasi
-    Hide Home button
-    Hides the Home button.
-    Hide Samples button
-    Hides the Samples button.
-    Hide Explore button
-    Hides the Explore button.
-    Hide Library button
-    Hides the Library button.
-    Hide Upgrade button
-    Hides the Upgrade button.
-    Menyembunyikan bilah navigasi
-    Sembunyikan bilah navigasi.
+    Aktifkan warna bilah navigasi khusus
+    Atur warna bilah navigasi.
+    Nilai warna bilah navigasi khusus
+    Masukkan kode hex dari warna bilah navigasi.
+    Nilai warna bilah navigasi tidak sah.
+    Sembunyikan tombol Beranda
+    Menyembunyikan tombol Beranda.
+    Sembunyikan tombol Sampel
+    Menyembunyikan tombol Sampel.
+    Sembunyikan tombol Jelajahi
+    Menyembunyikan tombol Jelajahi.
+    Sembunyikan tombol Pustaka
+    Menyembunyikan tombol Pustaka.
+    Sembunyikan tombol Upgrade
+    Menyembunyikan tombol Upgrade.
+    Sembunyikan bilah navigasi
+    Menyembunyikan bilah navigasi.
     Sembunyikan label navigasi
-    Menyembunyikan label di bilah navigasi.
+    Menyembunyikan label di bawah setiap tombol navigasi.
     
-    Player
-    Menonaktifkan gerakan pemutar
-    Nonaktifkan usap untuk mengubah trek di pemutar.
-    Aktifkan mode zen
-    Menambahkan rona abu-abu ke pemutar video untuk mengurangi ketegangan mata.
-    Enable Zen mode in podcasts
-    Also enables Zen mode for podcasts.
-    Hide channel guidelines
-    Hides the channel guidelines at the top of the comments section.
-    Sembunyikan filter overlay double-tap
-    Menyembunyikan overlay gelap yang muncul ketika double-tap to seek.
-    Hide timestamp and emoji buttons
+    Pemutar
+    Menambahkan tombol berikutnya pemutar mini
+    Menambahkan tombol trek berikutnya ke pemutar mini.
+    Tambahkan tombol sebelumnya miniplayer
+    Menambahkan tombol trek sebelumnya ke pemutar mini.
+    Ubah warna pemutar mini
+    Mengubah warna pemutar mini supaya sesuai dengan pemutar layar penuh.
+    Ubah warna latar belakang pemutar
+    Mengubah warna latar belakang pemutar ke warna khusus.
+    Warna primer latar belakang pemutar
+    "Masukkan kode hex dari warna primer latar belakang pemutar.
+
+Gunakan warna gelap jika memungkinkan, karena aplikasi ini tidak mendukung tema terang."
+    Warna sekunder latar belakang pemutar
+    "Masukkan kode hex warna sekunder latar belakang pemutar.
+
+Gunakan warna gelap jika memungkinkan, karena aplikasi ini tidak mendukung tema terang."
+    Warna latar belakang pemutar tidak sah.
+    Ubah posisi seekbar
+    Memindahkan seekbar di bawah tombol putar.
+    Nonaktifkan gerakan pemutar mini
+    Menonaktifkan usapan untuk mengubah trek di pemutar mini.
+    Nonaktifkan gerakan pemutar
+    Menonaktifkan usap untuk mengubah trek di pemutar.
+    Aktifkan pemutar mini paksa
+    Mengaktifkan pemutar mini paksa saat beralih ke trek baru.
+    Aktifkan usapan untuk menutup pemutar mini
+    Mengaktifkan gesek ke bawah untuk menutup pemutar mini.
+    Aktifkan seekbar tebal
+    "Mengaktifkan seekbar yang tebal.
+
+Keterbatasan: Segmen SponsorBlock tidak ditampilkan di seekbar."
+    Aktifkan mode Zen
+    Mengaktifkan warna abu-abu muda untuk latar belakang pemutar untuk mengurangi kelelahan mata.
+    Aktifkan mode Zen di podcast
+    Mengaktifkan mode Zen di podcast.
+    Sembunyikan pedoman saluran
+    Menyembunyikan panduan saluran di bagian atas komentar.
+    Sembunyikan penyaring hamparan ketuk dua kali
+    Menyembunyikan hamparan gelap yang muncul ketika mengetuk dua kali untuk menggeser.
+    Sembunyikan tombol emoji dan stempel waktu
     Hides the timestamp and emoji buttons when typing comments.
-    Hide fullscreen Share button
-    Hides the Share button in the fullscreen player.
+    Sembunyikan tombol Bagikan layar penuh
+    Menyembunyikan tombol Bagikan di pemutar layar penuh.
+    Sembunyikan tombol Lagu / Video
+    Menyembunyikan tombol Lagu / Video di pemutar.
     Ingat keadaan pengulangan
-    Mengingat keadaan pengulangan.
+    Mengingat keadaan tombol pengulangan.
     Ingat keadaan pengacakan
-    Mengingat keadaan pengacakan.
-    Restore old comments popup panels
-    Returns the comments popup panels to the old style.
-    Restore old player background
-    Returns the player background to the old style.
-    Restore old player layout
-    "Returns the player layout to the old style.
-Some features may not work properly in the old player layout."
+    Mengingat status tombol pengacakan.
+    Pulihkan panel sembulan komentar lama
+    Memulihkan panel sembulan komentar ke gaya lama.
+    Pulihkan latar belakang pemutar lama
+    Mengembalikan latar belakang pemutar ke gaya lama.
+    Pulihkan tata letak pemutar lama
+    "Memulihkan tata letak pemutar ke gaya lama.
+Beberapa fitur mungkin tidak berfungsi dengan baik dalam tata letak pemutar lama."
     
+    Menu pengaturan
+    Sembunyikan menu Pusat Keluarga
+    Sembunyikan menu Umum
+    Sembunyikan menu Pemutaran
+    Sembunyikan menu penghematan Data
+    Sembunyikan menu Unduhan & penyimpanan
+    Sembunyikan menu Pemberitahuan
+    Sembunyikan menu Privasi & data
+    Sembunyikan menu Rekomendasi
+    Sembunyikan menu Dapatkan Music premium
+    Sembunyikan menu Tentang
     
     Video
-    Edit kecepatan pemutaran kustom
+    Edit kecepatan pemutaran khusus
     Menambah atau mengubah kecepatan pemutaran yang tersedia.
-    Remember playback speed changes
+    Ingat perubahan kecepatan pemutaran
     Remembers the last playback speed selected.
     Tampilkan toast
-    Menunjukkan toast ketika mengubah playback speed semula.
-    Remember video quality changes
-    Remembers the last video quality selected.
+    Menampilkan toast saat mengubah kecepatan pemutaran bawaan.
+    Ingat perubahan kualitas video
+    Mengingat kualitas video terakhir yang dipilih.
     Tampilkan toast
-    Menunjukkan toast ketika mengubah playback speed semula.
-    Kecepatan pemutaran kustom tidak valid. Atur ulang ke nilai default.
-    Invalid custom playback speeds. Using default values.
-    Changing default speed to %s.
-    Changing default mobile data quality to %s.
-    Failed to set quality.
-    Changing default Wi-Fi quality to %s.
+    Menampilkan toast ketika mengubah kualitas video bawaan.
+    Kecepatan khusus harus kurang dari %sx.
+    Kecepatan pemutaran kustom tidak sah.
+    Mengubah kecepatan bawaan ke %s.
+    Mengubah kualitas data seluler bawaan ke %s.
+    Gagal mengatur kualitas.
+    Mengubah kualitas Wi-Fi bawaan ke %s.
     
     Return YouTube Dislike
-    Enable Return YouTube Dislike
-    Menunjukkan jumlah dislike pada video.
+    Aktifkan Return YouTube Dislike
+    Menampilkan jumlah tidak suka pada video.
     Dislike sebagai persentase
-    Alih-alih jumlah dislike, yang ditampilkan adalah persentase dislike.
-    Tombol like ringkas
-    Menyembunyikan pemisah tombol like.
-    Show a toast if API is unavailable
-    Shows a toast if the Return YouTube Dislike API is unavailable.
+    Menampilkan persentase dislike, bukan jumlah dislike.
+    Tombol suka ringkas
+    Menyembunyikan pemisah tombol suka.
+    Tampilkan perkiraan suka
+    Menampilkan perkiraan jumlah suka video.
+    Tampilkan toast jika API tidak tersedia
+    Menampilkan toast jika API Return YouTube Dislike tidak tersedia.
     Tentang
     ReturnYouTubeDislike.com
     Data disediakan oleh API Return YouTube Dislike. Tekan di sini untuk mempelajari lebih lanjut.
-    Dislikes are temporarily unavailable (API timed out).
-    Dislikes are unavailable (status %d).
+    Dislike untuk sementara tidak tersedia (API kehabisan waktu).
+    Dislike tidak tersedia (status %d).
     Dislike tidak tersedia (batas API client tercapai).
-    Dislikes are unavailable (%s).
+    Dislikes tidak tersedia (%s).
+    Disembunyikan
     
+    Return YouTube Username
+    Aktifkan Return YouTube Username
+    Mengganti penanganan dengan nama pengguna di komentar.
+    Format tampilan
+    Pilih format tampilan nama pengguna.
+    Nama pengguna
+    Nama pengguna (@penganganan)
+    \@penanganan (Nama pengguna)
+    Kunci API Data YouTube
+    Kunci pengembang untuk menggunakan API Data YouTube v3.
+    Tentang kunci API Data YouTube
+    "Kunci Pengembang API Data YouTube v3 diperlukan untuk mengganti penanganan dengan nama pengguna.
+
+Kuota harian untuk kunci API pada paket gratis adalah 10.000, dan 1 kuota digunakan untuk mengganti penanganan dengan nama pengguna untuk 1 komentar.
+
+Klik untuk melihat cara menerbitkan kunci API."
+    Terbitkan kunci pengembang API Data YouTube v3
+    1. Pergi ke <a href=%1$s>Buat proyek baru</a>.<br>2. Klik <b>tombol;</b> BUAT.<br>3. Pergi ke <a href=%2$s>API Data YouTube v3</a>.<br>4. Klik <b>tombol</b> NYALAKAN.<br>5. Klik <b>tombol</b> BUAT KREDENSIAL.<br>6. Pilih <b>pilihan</b> data Publik.<br>7. Klik <b>tombol</b> SELANJUTNYA.<br>8. Salin kunci API.<br><br>※ Kunci API tidak boleh dibagikan dengan orang lain, sehingga tidak disertakan dalam pengaturan Impor / Ekspor.
     
     SponsorBlock
-    Enable SponsorBlock
-    SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos.
-    Show a toast if API is unavailable
-    Shows a toast if the SponsorBlock API is unavailable.
-    Show a toast when skipping automatically
-    Shows a toast when a segment is automatically skipped.
-    Change API URL
-    The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing.
-    API URL reset.
-    API URL is invalid.
-    API URL changed.
-    Change segment behavior
+    Aktifkan SponsorBlock
+    SponsorBlock adalah sistem yang bersumber dari banyak orang untuk melewatkan bagian video YouTube yang mengganggu.
+    Tampilkan toast jika API tidak tersedia
+    Menampilkan toast jika API SponsorBlock tidak tersedia.
+    Menampilkan toast saat melewatkan secara otomatis
+    Menampilkan toast ketika segmen dilewati secara otomatis.
+    Ubah URL API
+    Alamat yang digunakan SponsorBlock untuk melakukan panggilan ke server. Jangan ubah ini kecuali Anda tahu apa yang Anda lakukan.
+    URL API diatur ulang.
+    URL API tidak sah.
+    URL API diubah.
+    Ubah perilaku segmen
     Sponsor
-    Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like.
-    Unpaid / Self Promotion
-    Similar to \'Sponsor\' except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with.
-    Interaction Reminder (Subscribe)
-    A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion.
-    Intermission / Intro Animation
-    An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information.
-    Endcards / Credits
-    Credits or when the YouTube endcards appear. Not for conclusions with information.
-    Preview / Recap / Hook
-    Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere.
-    Filler Tangent / Jokes
-    Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details.
-    Music: Non-Music Section
-    Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category.
-    Skipped sponsor.
-    Skipped self promotion.
-    Skipped annoying reminder.
-    Skipped intro.
-    Skipped intermission.
-    Skipped intermission.
-    Skipped outro.
-    Skipped preview.
-    Skipped preview.
-    Skipped recap.
-    Skipped filler.
-    Skipped a non-music section.
-    Skipped multiple segments.
-    Skip automatically
-    Disable
-    SponsorBlock is temporarily unavailable.
-    SponsorBlock is temporarily unavailable (status %d).
-    SponsorBlock is temporarily unavailable (API timed out).
-    Color:
-    Color changed.
-    Color reset.
-    Invalid color code. Color reset to default.
-    Reset color
-    Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms.
-    About
+    Promosi berbayar, rujukan berbayar, dan iklan langsung. Bukan untuk promosi diri atau promosi gratis untuk tujuan / kreator / situs web / produk yang mereka sukai.
+    Tidak Berbayar / Promosi Sendiri
+    Mirip dengan Sponsor, kecuali untuk promosi yang tidak berbayar atau promosi sendiri. Termasuk bagian tentang barang dagangan, donasi, atau informasi tentang dengan siapa mereka berkolaborasi.
+    Pengingat Interaksi (Berlangganan)
+    Pengingat singkat untuk menyukai, berlangganan, atau mengikuti mereka di tengah-tengah konten. Jika panjang atau tentang sesuatu yang spesifik, sebaiknya berada di bawah promosi diri.
+    Animasi Jeda / Intro
+    Selang waktu tanpa konten yang sebenarnya. Bisa berupa jeda, bingkai statis, atau animasi berulang. Tidak termasuk transisi yang berisi informasi.
+    Kartu Akhir / Kredit
+    Kredit atau saat kartu akhir YouTube muncul. Bukan untuk menyimpulkan informasi.
+    Pratinjau / Rekap / Pengait
+    Kumpulan klip yang menunjukkan apa yang akan datang atau apa yang terjadi dalam video atau dalam video lain dari suatu seri, di mana semua informasi diulang di tempat lain.
+    Pengisi Tidak Relevan / Lelucon
+    Adegan berbelit-belit yang ditambahkan hanya sebagai filler atau candaan yang tidak diperlukan untuk memahami isi utama video. Tidak termasuk bagian yang mengandung konteks atau detail latar belakang.
+    Musik: Bagian Non-Musik
+    Hanya untuk digunakan dalam video musik. Bagian video musik tanpa musik, yang belum tercakup dalam kategori lain.
+    Melewatkan sponsor.
+    Melewatkan promosi diri.
+    Melewatkan pengingat mengganggu.
+    Melewatkan intro.
+    Melewatkan jeda.
+    Melewatkan jeda.
+    Melewatkan outro.
+    Melewatkan pratinjau.
+    Melewatkan pratinjau.
+    Melewatkan rekap.
+    Melewatkan pengisi.
+    Melewatkan bagian non musik.
+    Melewatkan beberapa segmen.
+    Lewati otomatis
+    Nonaktifkan
+    SponsorBlock untuk sementara tidak tersedia.
+    SponsorBlock untuk sementara tidak tersedia (status %d).
+    SponsorBlock untuk sementara tidak tersedia (API habis batas waktunya).
+    Warna:
+    Warna diubah.
+    Warna diatur ulang.
+    Kode warna tidak sah.
+    Atur ulang warna
+    Data disediakan oleh API SponsorBlock. Tekan di sini untuk mempelajari lebih lanjut dan melihat unduhan untuk platform lain.
+    Tentang
     sponsor.ajay.app
     
-    Miscellaneous
+    Lain-lain
     Ekspor / Impor
-    Impor atau ekspor setelan sebagai teks.
-    Export settings to file
-    Import settings from file
-    Import / Export settings as text
-    Failed to export settings.
-    Settings were successfully exported.
+    Impor / Ekspor pengaturan Musik RVX.
+    Ekspor pengaturan ke berkas
+    Impor pengaturan dari berkas
+    Impor / Ekspor pengaturan sebagai teks
+    Gagal mengekspor pengaturan.
+    Pengaturan berhasil diekspor.
     Impor
     Salin
-    Import failed: %s.
-    Reset setelan ke default.
+    Impor gagal: %s.
+    Pengaturan diatur ulang ke bawaan.
     Setelan %d diimpor.
-    Reset
+    Atur ulang
     Setelan disalin ke papan klip.
-    Bypass gambar larangan wilayah
-    Mengganti domain yang ke blokir di negara tertentu sehingga playlist thumbnail, channel avatar, dll bisa di terima.
+    Abaikan pembatasan wilayah gambar
+    Mengabaikan domain yang diblokir di beberapa wilayah sehingga thumbnail playlist, avatar saluran, dll. dapat diterima.
     Ubah lembar berbagi
-    Mengubah dari lembar berbagi dalam aplikasi ke lembar berbagi sistem.
+    Mengubah lembar berbagi dalam aplikasi ke lembar berbagi sistem.
+    Menonaktifkan animasi percikan Cairo
+    Menonaktifkan animasi percikan Cairo saat aplikasi dimulai.
+    Nonaktifkan audio DRC
+    Menonaktifkan DRC (Dynamic Range Compression) yang diterapkan ke audio.
+    Nonaktifkan video musik dalam album
+    "Ketika pengguna non-premium memutar lagu yang termasuk dalam album, video musik terkadang diputar dan bukannya lagu resminya.
+
+Temukan lagu resmi jika video musik terdeteksi diputar dari album.
+
+Keterbatasan: Video anak-anak mungkin tidak dapat dialihkan."
+    Jenis pengalihan
+    Menentukan cara mengalihkan ke lagu resmi.
+    Alihkan
+    Ketuk tombol Lagu / Video
+    Ketuk dan tahan tombol Lagu / Video
+    Nonaktifkan protokol QUIC
+    "Menonaktifkan protokol QUIC CronetEngine."
     Aktifkan pencatatan debug
     Mencetak catatan debug.
-    Enable debug buffer logging
-    Includes the buffer in the debug log.
+    Mengaktifkan pencatatan buffer debug
+    Menyertakan buffer dalam log debug.
     Aktifkan codec opus
-    "Mengaktifkan codec audio opus alih-alih codec audio mp4a."
-    Sanitasi tautan berbagi
-    Menghapus parameter kueri pelacakan dari URL saat membagikan tautan.
-    Open GmsCore
-    Enable cloud messaging to receive notifications.
-    GmsCore is not installed. Install it.
-    Action needed
-    "GmsCore does not have permission to run in the background.
+    "Mengaktifkan codec OPUS jika respon pemutar menyertakannya.
 
-Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation.
+Info:
+• Klien YouTube Music terbaru menggunakan codec audio OPUS secara default.
+• Ini hanya berlaku untuk pengguna yang memalsukan dengan klien yang sangat lama."
+    Bersihkan tautan berbagi
+    Membersihkan tautan berbagi dengan menghapus parameter kueri pelacakan.
+    Palsukan klien
+    Memalsukan klien untuk mencegah masalah pemutaran.
+    Klien bawaan
+    Menentukan klien bawaan untuk pemalsuan.
+    Musik Android 4.27.53
+    Musik Android 5.29.53
+    Musik iOS 6.21
+    Musik iOS 7.04
+    Jenis riwayat tontonan
+    "• Asli: Mengikuti pengaturan riwayat tontonan akun Google, tetapi riwayat tontonan mungkin tidak berfungsi karena DNS atau VPN.
+• Ganti domain: Mengikuti pengaturan riwayat tontonan akun Google.
+• Blokir riwayat tontonan: Riwayat tontonan diblokir."
+    Asli
+    Ganti domain
+    Blokir riwayat tontonan
+    Buka pengaturan aplikasi bawaan
+    Untuk membuka tautan YouTube Music di RVX Music, aktifkan Buka tautan yang didukung dan aktifkan semua alamat web yang Didukung.
+    Buka pengaturan GmsCore
+    Untuk menerima pemberitahuan di RVX Music, aktifkan Cloud Messaging.
+    GmsCore tidak terpasang. Pasang dulu.
+    Diperlukan tindakan
+    "GmsCore tidak memiliki izin untuk berjalan di latar belakang.
 
-This is required for the app to work."
-    Open website
-    "GmsCore battery optimizations must be disabled to prevent issues.
+Ikuti panduan 'Don't kill my app!' untuk perangkat Anda, dan terapkan petunjuk pada pemasangan GmsCore.
 
-Tap on the continue button and disable battery optimizations."
-    Continue
+Hal ini diperlukan agar aplikasi dapat berfungsi."
+    Buka website
+    "Pengoptimalan baterai GmsCore harus dinonaktifkan untuk mencegah masalah.
+
+Menonaktifkan pengoptimalan baterai untuk GmsCore tidak akan berdampak negatif pada penggunaan baterai.
+
+Ketuk tombol lanjutkan dan izinkan perubahan pengoptimalan."
+    Lanjutkan
 
diff --git a/patches/src/main/resources/music/translations/ja-rJP/strings.xml b/patches/src/main/resources/music/translations/ja-rJP/strings.xml
index f59a009e1..841b47b0e 100644
--- a/patches/src/main/resources/music/translations/ja-rJP/strings.xml
+++ b/patches/src/main/resources/music/translations/ja-rJP/strings.xml
@@ -54,6 +54,11 @@
     
     広告
     全画面広告を非表示
+    "全画面広告を隠す
+
+注意:
+・ホームフィードの代わりに真っ黒な画面が表示されることがあります。"
+    全画面広告を閉じました。
     一般広告を非表示
     一般広告を非表示にします。
     音楽の広告を非表示
@@ -62,6 +67,7 @@
     有料プロモーションラベルを非表示にします。
     プレミアムプロモーションポップアップを非表示
     プレミアムプロモーションポップアップを非表示にします。
+    プレミアムプロモーションのポップアップは閉じられました。
     プレミアム更新バナーを非表示
     プレミアム更新バナーを非表示にします。
     プロモーションバナーを非表示
@@ -93,6 +99,7 @@
     「エピソードに移動」を非表示
     「ポッドキャストに移動」を非表示
     「ヘルプとフィードバック」を非表示
+    「興味なし」を非表示
     「[クイック アクセス] に固定」を非表示
     「次に再生」を非表示
     画質メニューを非表示
@@ -124,8 +131,17 @@
     全般
     スタートページを変更
     アプリのスタートページを変更します。
+    デフォルト
+    チャート
+    次に再生
     探索
+    履歴
     ライブラリ
+    高く評価した音楽
+    ポッドキャスト
+    サンプル
+    検索
+    登録チャンネル
     低評価リダイレクトを無効化
     低評価ボタンを押したとき、次の曲へのリダイレクトするのを無効にする。
     字幕の強制を無効化
@@ -197,6 +213,18 @@
     ナビゲーションバーを非表示にします。
     ナビゲーションバーのラベルを非表示
     ナビゲーションバーのラベルを非表示にします。
+    サンプルボタンを置換
+    「サンプル」を「検索」に置き換えます。
+    アップグレードボタンを置換
+    「アップグレード」を「設定」に置き換えます。
+    ボタンの置き換えについて
+    "この機能は実験的なものです。
+YouTube Music の検索や設定などのアクティビティは公開されていないため、このパッチには構造的な制限があります。
+
+既知の問題:
+・検索や設定などの置換されたアクティビティを閉じると、スタートページが開きます。
+
+クリックすると「スタートページの変更」設定が開きます。"
     
     プレーヤー
     「次の曲に進むボタン」を表示
@@ -222,8 +250,14 @@
     ミニプレイヤーでスワイプによる曲の変更を無効にします。
     プレイヤージェスチャーを無効にする
     プレイヤーでスワイプによる曲の変更を無効にします。
+    ミニプレイヤーの強制
+    新しいトラックに切り替えたときに強制的にミニプレーヤーを有効にします。
     スワイプしてミニプレーヤーを閉じる
     下にスワイプしてミニプレーヤーを閉じられるようにします。
+    太いシークバーを有効化
+    "太いシークバーを有効にします。
+
+注意: SponsorBlock セグメントはシークバーに表示されません。"
     Zen モードを有効化
     動画プレーヤーに灰色の色合いを追加し、目の疲れを軽減します。
     ポッドキャストでZenモードを有効化
@@ -399,11 +433,18 @@ API キーの発行方法については、ここをタップしてください
     DRCオーディオを無効にする
     音声に適用されるDRC (Dynamic Range Compression) を無効にします。
     アルバム内のミュージックビデオを無効にする
+    "非プレミアムユーザーがアルバムに含まれる曲を再生すると、公式の曲ではなくMVが再生されることがあります。
+
+アルバムからMVの再生が検出された場合は、公式楽曲を検索します。
+
+注意: キッズビデオはリダイレクトされない場合があります。"
     リダイレクトのタイプ
     公式楽曲にリダイレクトする方法を指定します。
     リダイレクト
     曲 / 動画 の切り替えをタップ
     曲 / 動画 の切り替えをタップし長押し
+    QUIC プロトコルを無効化
+    "CronetEngine の QUIC プロトコルを無効化します。"
     デバッグログ
     デバッグログを出力します。
     デバッグバッファログを有効化
@@ -415,10 +456,23 @@ API キーの発行方法については、ここをタップしてください
     クライアントを偽装
     再生の問題を防ぐためにクライアントを偽装します。
     既定のクライアント
+    偽装するデフォルトのクライアントを定義します。
     Android Music 4.27.53
     Android Music 5.29.53
     iOS Music 6.21
     iOS Music 7.04
+    プレイヤーパラメータを偽装
+    "再生の問題を防ぐためにプレイヤーパラメーターを偽装します。
+
+副作用:
+• 字幕が下部ではなくプレーヤーの上部に配置されることがあります。"
+    再生履歴の種類
+    "・オリジナル: Google アカウントの視聴履歴設定に従いますが、DNSやVPNにより視聴履歴が機能しない場合があります。
+・ドメインを置き換えます:  Google アカウントの視聴履歴設定に従います。
+・視聴履歴をブロックします:  視聴履歴をブロックします。"
+    オリジナル
+    ドメインを置換
+    再生履歴をブロック
     「デフォルトで開く」の設定
     RVX Music でYouTube Music のURLを開くには、「対応リンクを開く」を有効にし、サポートされているURLを有効にします。
     GmsCoreを開く
diff --git a/patches/src/main/resources/music/translations/ko-rKR/strings.xml b/patches/src/main/resources/music/translations/ko-rKR/strings.xml
index 4dd23af23..6bdbd316c 100644
--- a/patches/src/main/resources/music/translations/ko-rKR/strings.xml
+++ b/patches/src/main/resources/music/translations/ko-rKR/strings.xml
@@ -212,6 +212,18 @@
     하단바를 숨깁니다.
     하단바 버튼 라벨 제거
     하단바 버튼에서 라벨을 숨깁니다.
+    샘플 버튼 변경
+    샘플 버튼을 검색 버튼으로 변경합니다.
+    업그레이드 버튼 변경
+    업그레이드 버튼을 설정 버튼으로 변경합니다.
+    버튼 변경에 대한 정보
+    "이 기능은 실험 기능입니다.
+YouTube Music의 검색 및 설정과 같은 활동은 공개되지 않으므로 패치에는 구조적인 제한이 있습니다.
+
+알려진 문제:
+• 검색 및 설정과 같은 교체된 활동이 닫히면 앱 시작 페이지가 열립니다.
+
+앱 시작 페이지 설정을 열려면 여기를 누르세요."
     
     플레이어
     미니 플레이어 다음 재생 버튼 추가
@@ -426,7 +438,7 @@ API Key를 발급받는 방법을 보려면 여기를 누르세요."
 앨범에서 뮤직 비디오가 재생되는 것이 감지되면 정식 음원을 찾아줍니다.
 
 알려진 문제점:
-• Kids 동영상은 리디렉션되지 않을 수 있습니다."
+• Kids 동영상은 리다이렉션되지 않을 수 있습니다."
     리다이렉션 유형
     정식 음원으로 리다이렉션하는 방법을 지정할 수 있습니다.
     리다이렉션
@@ -454,6 +466,11 @@ API Key를 발급받는 방법을 보려면 여기를 누르세요."
     Android Music 5.29.53
     iOS Music 6.21
     iOS Music 7.04
+    플레이어 매개변수 변경하기
+    "플레이어 매개변수를 변경하여 재생 문제를 방지할 수 있습니다.
+
+알려진 문제점:
+• 자막이 플레이어 하단이 아닌 상단에 위치하는 경우가 있습니다."
     시청 기록 유형
     "• 기본값: Google 계정의 시청 기록 설정을 따르지만 DNS 또는 VPN으로 인하여 시청 기록이 작동되지 않을 수 있습니다.
 • 도메인 변경: Google 계정의 시청 기록 설정을 따릅니다.
diff --git a/patches/src/main/resources/music/translations/pl-rPL/strings.xml b/patches/src/main/resources/music/translations/pl-rPL/strings.xml
index 884e5c140..d69e52c5c 100644
--- a/patches/src/main/resources/music/translations/pl-rPL/strings.xml
+++ b/patches/src/main/resources/music/translations/pl-rPL/strings.xml
@@ -213,6 +213,18 @@ Nie pomija to ograniczeń wiekowych, lecz akceptuje je automatycznie."
     Ukrywa pasek nawigacji.
     Ukryj nazwy w pasku nawigacji
     Ukrywa nazwy każdego przycisku w pasku nawigacji.
+    Zastąp przycisk sampli
+    Zastępuje przycisk sampli przyciskiem wyszukiwania.
+    Zastąp przycisk przejścia na YouTube Premium
+    Zastępuje przycisk przejścia na YouTube Premium przyciskiem ustawień.
+    O zastępowaniu przycisków
+    "Ta funkcja jest eksperymentalna.
+Istnieją strukturalne ograniczenia łatki, ponieważ działania takie jak wyszukiwanie i ustawienia w YouTube Music nie są publiczne.
+
+Znane problemy:
+• Po zamknięciu zastąpionego działania, takiego jak wyszukiwanie i ustawienia, otwierana jest strona startowa.
+
+Kliknij, by otworzyć ustawienia 'Zmień stronę startową'."
     
     Odtwarzacz
     Dodaj przycisk do następnego utworu w miniodtwarzaczu
@@ -453,6 +465,11 @@ Informacje:
     Android Music 5.29.53
     iOS Music 6.21
     iOS Music 7.04
+    Oszukuj parametry odtwarzacza
+    "Oszukuje parametry odtwarzacza, by zapobiec problemom z odtwarzaniem.
+
+Efekt uboczny:
+• Czasem napisy są u góry odtwarzacza, zamiast na dole"
     Typ historii oglądania
     "• Oryginalny: Stosuje się do ustawień historii oglądania konta Google, lecz historia oglądania może nie działać przy używaniu DNS lub VPN
 • Zastąpienie domeny: Stosuje się do ustawień historii oglądania konta Google
diff --git a/patches/src/main/resources/music/translations/pt-rBR/strings.xml b/patches/src/main/resources/music/translations/pt-rBR/strings.xml
index e98048dba..356d67cc6 100644
--- a/patches/src/main/resources/music/translations/pt-rBR/strings.xml
+++ b/patches/src/main/resources/music/translations/pt-rBR/strings.xml
@@ -54,6 +54,10 @@ Por favor, baixe %2$s do site."
     
     Anúncios
     Ocultar anúncios em tela cheia
+    "Oculta anúncios de tela cheia.
+
+Limitações:
+• Às vezes você pode ver uma tela preta em branco ao invés do feed inicial."
     Os anúncios em tela cheia são fechados.
     Ocultar anúncios gerais
     Oculta anúncios gerais.
@@ -239,7 +243,9 @@ Use cores escuras se possível, pois o aplicativo não suporta temas claros."Ativar deslizar para dispensar o mini reprodutor
     Ativa deslizar para baixo para fechar o mini reprodutor.
     Ativar barra de busca espessa
-    "Ativa a barra de busca espessa."
+    "Ativar a barra de busca espessa.
+
+Limitações: segmentos de patrocinadores não são exibidos na barra de busca."
     Ativar modo Calmo
     Ativa uma cor cinza claro para o fundo do reprodutor para reduzir o cansaço visual.
     Ativar o modo Calmo em podcasts
@@ -415,11 +421,18 @@ Clique para ver como emitir uma chave de API."
     Desativar áudio DRC
     Desativa o DRC (Compressão de faixa dinâmica) aplicada ao áudio.
     Desativar vídeo da música no álbum
+    "Quando um usuário não premium reproduz uma música incluída em um álbum, o vídeo da música é tocado às vezes em vez da música oficial.
+
+Encontre a música oficial se um vídeo de música for detectado tocando de um álbum.
+
+Limitações: Vídeos infantis não podem ser redirecionados."
     Tipo de redirecionamento
     Especifica como redirecionar para a música oficial.
     Redirecionar
     Tocar alternador de Música / Vídeo
     Tocar e segurar alternador de Música / Vídeo
+    Desativar protocolo QUIC
+    "Desativa o protocolo QUIC da CronetEngine."
     Ativar o relatório de depuração
     Imprime o relatório de depuração
     Ativar o registro de depuração do buffer
diff --git a/patches/src/main/resources/music/translations/ru-rRU/strings.xml b/patches/src/main/resources/music/translations/ru-rRU/strings.xml
index 5dce8aa0e..6272aedbb 100644
--- a/patches/src/main/resources/music/translations/ru-rRU/strings.xml
+++ b/patches/src/main/resources/music/translations/ru-rRU/strings.xml
@@ -213,6 +213,18 @@
     Скрывает панель навигации.
     Скрыть подписи кнопок навигации
     Скрывает подписи под кнопками навигации.
+    Замена кнопки \"Сэмплы\"
+    Заменяет кнопку \"Сэмпли\" на кнопку \"Поиск\".
+    Замена кнопки \"Обновить\"
+    Заменяет кнопку \"Обновить\" на кнопку \"Настройки\".
+    О замене кнопки
+    "Эта функция является экспериментальной.
+Такие действия, как Поиск и Настройки в YouTube Music, не являются общедоступными, поэтому патч имеет структурные ограничения.
+
+Известные проблемы:
+• При закрытии замененных действий, таких как Поиск и Настройки, открывается начальная страница.
+
+Нажмите, чтобы открыть \"Изменить начальную страницу\"."
     
     Плеер
     Добавить кнопку следующее в миниплеер
@@ -451,6 +463,11 @@
     Android Music 5.29.53
     iOS Music 6.21
     iOS Music 7.04
+    Подменить параметр плеера
+    "Подменяет параметр плеера, чтобы предотвратить проблемы с воспроизведением.
+
+Побочный эффект:
+• Иногда субтитры располагаются в верхней части плеера, а не в нижней."
     Тип истории просмотра
     "• Оригинальный: Соответствует настройкам истории просмотров аккаунта Google, но история просмотров может не работать через DNS или VPN.
 • Заменить домен: Настройка истории просмотров аккаунта Google.
diff --git a/patches/src/main/resources/music/translations/uk-rUA/strings.xml b/patches/src/main/resources/music/translations/uk-rUA/strings.xml
index 547982fb2..b1d6d001b 100644
--- a/patches/src/main/resources/music/translations/uk-rUA/strings.xml
+++ b/patches/src/main/resources/music/translations/uk-rUA/strings.xml
@@ -130,7 +130,7 @@
     
     Загальні
     Змінити початкову сторінку
-    Виберіть сторінку з якої буде стартувати додаток.
+    Виберіть сторінку з якої буде стартувати застосунок.
     За замовчуванням
     Хіт-паради
     Послухати пізніше
@@ -183,13 +183,13 @@
     Вилучити діалог про небажаний контент
     "Вилучає діалог про небажаний контент.
 Це не обходить вікові обмеження. Просто приймає їх автоматично."
-    Підробити версію програми
-    "Підміна версії клієнта на старішу версію.
+    Підмінити версію програми
+    "Підміняє версію клієнта на старішу версію.
 
-• Це змінить зовнішній вигляд програми, але можуть виникнути невідомі побічні ефекти.
-• Якщо пізніше вимкнути, старий інтерфейс може залишитися, доки не буде очищено дані програми."
-    Підробити цільову версію програми
-    Виберіть зі списку цільову версію підробки програми.
+• Це змінить зовнішній вигляд застосунку, але можуть виникнути невідомі побічні ефекти.
+• Якщо пізніше вимкнути, старий інтерфейс може залишитися, доки не буде очищено дані застосунку."
+    Підмінити цільову версію застосунку
+    Виберіть зі списку цільову версію для підміни.
     6.42.55 - Вимкнення динамічних текстів
     7.16.53 - Відновлення старої панелі дій
     
@@ -213,6 +213,17 @@
     Приховує панель навігації.
     Приховати підписи кнопок навігації
     Приховує підпис під кожною навігаційною кнопкою.
+    Заміна кнопки \"Семпли\"
+    Замінює кнопку \"Семпли\" на кнопку \"Пошук\".
+    Заміна кнопки \"Оновити\"
+    Замінює кнопку \"Оновити\" на кнопку \"Налаштування\".
+    Про заміну кнопки
+    "Ця функція є експериментальною.
+Існують структурні обмеження патчу, оскільки такі Activities, як Пошук і Налаштування в YouTube Music, не є загальнодоступними.
+Відомі проблеми:
+• Коли замінені Activity як Пошук і Налаштування закрито, відкривається початкова сторінка.
+
+Натисніть, щоб відкрити \"Змінити початкову сторінку\"."
     
     Плеєр
     Додати кнопку наступне у мініплеєр
@@ -226,11 +237,11 @@
     Основний колір фону плеєра
     "Введіть hex код основного кольору фону плеєра.
 
-По можливості використовуйте темні кольори, оскільки програма не підтримує світлі теми."
+По можливості використовуйте темні кольори, оскільки застосунок не підтримує світлі теми."
     Вторинний колір фону плеєра
     "Введіть hex код вторинного кольору фону плеєра.
 
-По можливості використовуйте темні кольори, оскільки програма не підтримує світлі теми."
+По можливості використовуйте темні кольори, оскільки застосунок не підтримує світлі теми."
     Недійсний колір фону плеєра.
     Змінити положення панелі прогресу
     Переміщує панель прогресу під кнопку відтворення.
@@ -445,14 +456,19 @@
 • Це буде корисно лише для користувачів, які користуються дуже старими клієнтами."
     Обробляти поширення посилань
     Видаляє параметри запиту відстеження з посилання перед тим, як поділитися ним.
-    Підміна клієнта
-    Підміна клієнта щоб запобігти проблемам відтворення.
+    Підмінити клієнт
+    Підмінює клієнт, щоб запобігти проблемам із відтворенням.
     Клієнт за замовчуванням
     Визначає клієнт за замовчуванням для підробки.
     Android Music 4.27.53
     Android Music 5.29.53
     iOS Music 6.21
     iOS Music 7.04
+    Підмінити параметр плеєра
+    "Підмінює параметр плеєра, щоб запобігти проблемам із відтворенням.
+
+Побічний ефект:
+• Іноді субтитри розташовані у верхній частині плеєра, а не внизу."
     Тип історії перегляду
     "• Оригінальний: Відповідає налаштуванням історії переглядів облікового запису Google, але історія переглядів може не працювати через DNS або VPN.
 • Замінити домен: Дотримується налаштувань історії переглядів облікового запису Google.
@@ -470,7 +486,7 @@
 
 Дотримуйтесь посібника \"Don't kill my app\" для вашого пристрою і застосуйте інструкції для встановлення GmsCore.
 
-Це необхідно для того, щоб програма працювала."
+Це необхідно для того, щоб застосунок працював."
     Відкрити сайт
     "Необхідно вимкнути оптимізацію енергії для MicroG GmsCore, щоб запобігти проблемам.
 
diff --git a/patches/src/main/resources/music/translations/vi-rVN/strings.xml b/patches/src/main/resources/music/translations/vi-rVN/strings.xml
index ef300764f..5cd971de8 100644
--- a/patches/src/main/resources/music/translations/vi-rVN/strings.xml
+++ b/patches/src/main/resources/music/translations/vi-rVN/strings.xml
@@ -212,6 +212,18 @@ Hạn chế:
     Ẩn thanh điều hướng.
     Ẩn tên bên dưới nút
     Ẩn tên bên dưới các nút trên thanh điều hướng.
+    Thay thế nút Đoạn nhạc
+    Thay thế nút Đoạn nhạc bằng nút Tìm kiếm.
+    Thay thế nút Nâng cấp
+    Thay thế nút Nâng cấp bằng nút Cài đặt.
+    Về tính năng thay thế nút
+    "Hiện tính năng này đang trong giai đoạn thử nghiệm.
+Bản vá có một số hạn chế về mặt cấu trúc vì các hoạt động như Tìm kiếm và Cài đặt trong YouTube Music không được công khai.
+
+Các sự cố đã biết:
+• Khi một hoạt động thay thế như Tìm kiếm và Cài đặt bị đóng, trang khởi động sẽ được mở.
+
+Bạn có thể mở Thay đổi cài đặt trang khởi động bằng cách nhấp vào đây."
     
     Trình phát
     Nút tiếp theo trong trình phát thu nhỏ
@@ -452,6 +464,11 @@ Chi tiết:
     Android Music 5.29.53
     iOS Music 6.21
     iOS Music 7.04
+    Giả mạo thông số trình phát
+    "Giả mạo thông số trình phát để khắc phục sự cố phát.
+
+Hạn chế:
+• Đôi khi phụ đề hiển thị ở phía trên của trình phát thay vì ở phía dưới như thường lệ."
     Kiểu nhật ký
     "• Gốc: Tuân theo cài đặt nhật ký xem của tài khoản Google, nhưng có thể không hoạt động do DNS hoặc VPN.
 • Thay thế miền: Vẫn tuân theo cài đặt nhật ký xem của tài khoản Google.
diff --git a/patches/src/main/resources/youtube/settings/host/values/arrays.xml b/patches/src/main/resources/youtube/settings/host/values/arrays.xml
index c3dd9f126..9ca518f23 100644
--- a/patches/src/main/resources/youtube/settings/host/values/arrays.xml
+++ b/patches/src/main/resources/youtube/settings/host/values/arrays.xml
@@ -22,19 +22,21 @@
         MIDDLE
         END
     
-    
-        @string/revanced_change_layout_entry_1
-        @string/revanced_change_layout_entry_2
-        @string/revanced_change_layout_entry_3
-        @string/revanced_change_layout_entry_4
-        @string/revanced_change_layout_entry_5
+    
+        @string/revanced_change_form_factor_entry_1
+        @string/revanced_change_form_factor_entry_2
+        @string/revanced_change_form_factor_entry_3
+        @string/revanced_change_form_factor_entry_4
+        @string/revanced_change_form_factor_entry_5
+        @string/revanced_change_form_factor_entry_6
     
-    
-        ORIGINAL
-        SMALL_FORM_FACTOR
-        SMALL_FORM_FACTOR_WIDTH_DP
-        LARGE_FORM_FACTOR
-        LARGE_FORM_FACTOR_WIDTH_DP
+    
+        DEFAULT
+        SMALL
+        SMALL_WIDTH_DP
+        LARGE
+        LARGE_WIDTH_DP
+        AUTOMOTIVE
     
     
         @string/revanced_change_start_page_entry_default
diff --git a/patches/src/main/resources/youtube/settings/host/values/strings.xml b/patches/src/main/resources/youtube/settings/host/values/strings.xml
index dae622313..b174045f1 100644
--- a/patches/src/main/resources/youtube/settings/host/values/strings.xml
+++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml
@@ -439,17 +439,27 @@ Limitation: Back button on the toolbar may not work."
     "Removes the viewer discretion dialog.
 This does not bypass the age restriction. It just accepts it automatically."
 
-    Change layout
-    Original
-    Phone
-    Phone (Max 480 dp)
-    Tablet
-    Tablet (Min 600 dp)
     Change live ring click action
     "Channel opens when the live ring is clicked.
 
 Limitation: When the Shorts live stream is opened in regular player due to the 'Open Shorts in regular player' setting, channel does not open."
     Live stream opens when the live ring is clicked.
+    Layout form factor
+    Default
+    Phone
+    Phone (Max 480 dp)
+    Tablet
+    Tablet (Min 600 dp)
+    Automotive
+    "Changes include:
+
+Tablet layout
+• Community posts are hidden.
+
+Automotive layout
+• Shorts open in the regular player.
+• Feed is organized by topics and channels.
+• Video description cannot be opened when 'Spoof streaming data' is turned off."
     Spoof app version
     Version spoofed
     Version not spoofed
@@ -2058,7 +2068,8 @@ Tap the continue button and allow optimization changes."
 • Disable forced auto audio tracks is not available.
 • Kids videos may not play when logged out or in incognito mode."
     • There may be playback issues (PoToken required).
-    "• Movies or paid videos may not play.
+    "• Stable volume is not available.
+• Movies or paid videos may not play.
 • Kids videos may not play when logged out or in incognito mode."
     Force iOS AVC (H.264)
     Video codec is forced to AVC (H.264).
@@ -2066,6 +2077,14 @@ Tap the continue button and allow optimization changes."
     "Enabling this might improve battery life and fix playback stuttering.
 
 AVC has a maximum resolution of 1080p, Opus audio codec is not available, and video playback will use more internet data than VP9 or AV1."
+    Skip Onesie response encryption
+    "Skip Onesie response encryption.
+
+• Fixes a new type of playback issue that some users are experiencing.
+• AV1 codec may not be available."
+    "Do not skip Onesie response encryption.
+
+• Some users may experience a new type of playback issue."
     Show in Stats for nerds
     Client used to fetch streaming data is shown in Stats for nerds.
     Client used to fetch streaming data is hidden in Stats for nerds.
diff --git a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
index c73dcf55a..136f93c31 100644
--- a/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
+++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml
@@ -269,12 +269,12 @@
         
 
-        
-
         
 
+        
+
         
+
+            
+
+            
     コミュニティ投稿
-    フィードとチャンネルのコミュニティの投稿を非表示または表示します。
+    フィードとチャンネルのコミュニティ投稿を非表示または表示します。
     チャンネルから非表示
     チャンネル内から非表示にします。
     チャンネル内から非表示にします。
@@ -412,17 +412,27 @@ DeArrow の詳細については、ここをタップしてください。""年齢制限ダイアログを削除します。 
 
 注意: 年齢制限を回避することはできませんが、自動的に同意します。"
-    レイアウトを変更
-    オリジナル
-    スマホ
-    スマホ(最大 480 dp)
-    タブレット
-    タブレット(最小 600 dp)
     ライブ状態のアイコンをタップした際の動作を変更
     "現在の設定: 「ライブ」状態のアイコンをタップすると、チャンネルが開きます。
 
 注意: 「通常のプレーヤーでショートを再生」設定を有効にしている場合、ショートのライブ配信を通常のプレーヤーで開くとチャンネルは開きません。"
     現在の設定: 「ライブ」状態のアイコンをタップすると、ライブ配信が開きます。\n\n注意: 「通常のプレーヤーでショートを再生」設定を有効にしている場合、ショートのライブ配信を通常のプレーヤーで開くとチャンネルは開きません。
+    レイアウトの変更
+    デフォルト
+    スマホ
+    スマホ(最大 480 dp)
+    タブレット
+    タブレット(最小 600 dp)
+    Android Auto
+    "注意:
+
+「タブレット」レイアウトを選択した場合:
+・コミュニティ投稿は非表示になります。
+
+「Android Auto」レイアウトを選択した場合:
+・ショートは通常のプレーヤーで開きます。
+・フィードはトピックとチャンネルごとに整理されます。
+・「ストリーミングデータを偽装」がオフの場合、動画の概要欄を開くことができません。"
     アプリのバージョンを偽装
     アプリのバージョンを偽装できます。
     アプリのバージョンを偽装できます。
@@ -1569,7 +1579,7 @@ DeArrow の詳細については、ここをタップしてください。"
-    この設定をオンにした場合、バッファリングの問題が発生する可能性があります。
+    この設定をオンにした場合、動画が再生できない問題が発生する可能性があります。
     スキップ時にトーストを表示
     スキップ時にトーストを表示します。
     スキップ時にトーストを表示します。
@@ -1899,11 +1909,11 @@ GmsCore の電池の最適化を無効にしても、バッテリーの使用に
     設定をクリップボードにコピーしました。
     
     ストリーミングデータを偽装
-    バッファリングの問題を防ぐためにストリーミングデータを偽装します。
+    動画が再生できない問題を防ぐためにストリーミングデータを偽装します。
     ストリーミングデータを偽装
     ストリーミングデータを偽装していない場合、動画の再生ができない可能性があります。
     "ストリーミングデータを偽装していない場合、動画の再生ができない可能性があります。"
-    この設定をオフにした場合、バッファリングの問題が発生する可能性があります。
+    この設定をオフにした場合、動画が再生できない問題が発生する可能性があります。
     偽装するクライアントの種類
     "Android TV
 (Google アカウントへのログインが必要)"
@@ -1916,11 +1926,12 @@ GmsCore の電池の最適化を無効にしても、バッテリーの使用に
 (Google アカウントへのログインが必要)"
     ストリーミングデータを偽装することによる副作用
     "・「音声トラック」メニューは表示されません。
-・「一定音量」は使用できません。
+・「一定音量」は利用できません。
 ・「音声トラックの強制を無効化」は利用できません。
 ・ログインをしていない場合やシークレットモードでは、子供向け動画は再生できない可能性があります。"
     ・動画が再生できない可能性があります(PoToken が必要です)。
-    "・映画や有料動画は再生できない可能性があります。
+    "・「一定音量」は利用できません。
+・映画や有料動画は再生できない可能性があります。
 ・ログインをしていない場合やシークレットモードでは、子供向け動画は再生できない可能性があります。"
     iOS クライアントで AVC (H.264) を強制
     iOS クライアントで AVC (H.264) を強制します。
@@ -1931,6 +1942,11 @@ GmsCore の電池の最適化を無効にしても、バッテリーの使用に
 ・AVC (H.264) の最大解像度は 1080p です。
 ・Opus オーディオコーデックは使用できません。
 ・動画の再生時には VP9 や AV1 よりも多くの通信量を消費します。"
+    ワンジーレスポンス暗号化をスキップ
+    "ワンジーレスポンス暗号化をスキップします。これにより、一部のユーザーが遭遇している新しいタイプの「動画が再生できない」問題を修正します。
+
+注意: AV1 コーデックは利用できない可能性があります。"
+    "ワンジーレスポンス暗号化をスキップします。これにより、一部のユーザーが遭遇している新しいタイプの「動画が再生できない」問題を修正します。\n\n注意: AV1 コーデックは利用できない可能性があります。"
     統計情報に偽装したクライアントを表示
     統計情報に偽装したクライアントを表示します。
     統計情報に偽装したクライアントを表示します。
diff --git a/patches/src/main/resources/youtube/translations/ko-rKR/strings.xml b/patches/src/main/resources/youtube/translations/ko-rKR/strings.xml
index df6d5774c..8818e060a 100644
--- a/patches/src/main/resources/youtube/translations/ko-rKR/strings.xml
+++ b/patches/src/main/resources/youtube/translations/ko-rKR/strings.xml
@@ -297,7 +297,7 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요."
     "필터링할 키워드 및 구문을 줄바꿈으로 구분하여 설정합니다.
 
 • 필터링 키워드는 채널 이름 또는 동영상 제목에 표시되는 모든 텍스트가 될 수 있습니다.
-• 가운데 대문자가 있는 단어는 대소문자를 함께 입력해야 합니다 (예: iPhone, TikTok, LeBlanc)."
+• 대문자가 있는 단어는 대소문자를 함께 입력해야 합니다 (예: iPhone, TikTok, LeBlanc)."
     키워드 필터링에 대한 정보
     "홈 / 구독 / 검색 결과가 필터링되어 키워드 구문과 일치하는 콘텐츠가 숨겨집니다.
 
@@ -411,18 +411,28 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요."
     동영상들 사이에서 회색 구분선이 표시됩니다.
     시청 경고 다이얼로그 제거하기
     "다음 동영상을 시청하기 전에 표시되는 시청 경고 다이얼로그를 제거합니다:\n• 연령 제한 동영상\n• 혐오감을 주는 동영상\n• 자살 또는 자해와 관련된 동영상 ...\n\n이 설정은 다이얼로그를 자동으로 허용하기만 하며 연령 제한(성인인증 절차)을 우회할 수 없습니다."
-    레이아웃 변경하기
-    기기 기본값 사용
-    
-    폰 (최대 너비: 480 dp)
-    태블릿
-    태블릿 (최소 너비: 600 dp)
     라이브 링 누르기 동작 변경하기
     "라이브 링이 표시된 채널 아이콘을 누르면 채널 프로필으로 연결됩니다.
 
 알려진 문제점: 
 • '일반 플레이어에서 Shorts 재생하기' 설정으로 인하여 일반 플레이어에서 Shorts 실시간 스트림이 재생된다면, 채널 프로필으로 연결되지 않을 수 있습니다."
     라이브 링이 표시된 채널 아이콘을 누르면 실시간 스트림 동영상으로 연결됩니다.
+    레이아웃 폼 팩터 변경하기
+    기기 기본값 사용
+    
+    폰 (최대 너비: 480 dp)
+    태블릿
+    태블릿 (최소 너비: 600 dp)
+    오토모티브
+    "변경 사항:
+
+태블릿 레이아웃
+• 커뮤니티 게시물이 숨겨집니다.
+
+오토모티브 레이아웃
+• Shorts가 일반 플레이어에서 재생됩니다.
+• 피드가 주제와 채널별로 구성됩니다.
+• '스트리밍 데이터 변경하기'가 비활성화되어있으면 동영상 설명을 열 수 없습니다."
     앱 버전 변경하기
     앱 버전을 변경합니다.
     앱 버전을 변경하지 않습니다.
@@ -925,13 +935,13 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요."
     참여 패널 비활성화하기
     참여 패널을 비활성화합니다.
     참여 패널을 활성화합니다.
-    동영상이 시작할 때, 전체 화면 모드로 들어가기
-    "동영상이 시작할 때, 전체 화면 모드로 들어갑니다.
+    동영상이 시작할 때, 전체 화면으로 들어가기
+    "동영상이 시작할 때, 전체 화면으로 들어갑니다.
 
 알려진 문제점: 
 • 플레이어가 최소화되어 있거나 PIP 모드 또는 백그라운드 재생에 있는 경우에는 작동되지 않습니다."
-    동영상이 시작할 때, 전체 화면 모드로 들어가지 않습니다.
-    동영상이 종료할 때, 전체 화면 모드에서 나가기
+    동영상이 시작할 때, 전체 화면으로 들어가지 않습니다.
+    동영상이 종료할 때, 전체 화면에서 나가기
     사용 안함
     세로 모드
     가로 모드
@@ -1476,14 +1486,14 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요."
     
     스와이프 제스처
     스와이프 제스처로 자동 밝기 활성화하기
-    스와이프 제스처로 밝기가 0이 되면 자동 밝기를 활성화합니다.
-    스와이프 제스처로 밝기가 0이 되면 자동 밝기를 활성화하지 않습니다.
+    전체 화면에서 스와이프하여 밝기가 0이 되면 자동 밝기를 활성화합니다.
+    전체 화면에서 스와이프하여 밝기가 0이 되더라도 자동 밝기를 활성화하지 않습니다.
     스와이프 제스처로 밝기 조절 활성화하기
-    스와이프 제스처로 밝기 조절을 활성화합니다.
-    스와이프 제스처로 밝기 조절을 비활성화합니다.
+    전체 화면 왼쪽에서 위로/아래로 스와이프하여 밝기 조절합니다.
+    전체 화면 왼쪽에서 위로/아래로 스와이프하여 밝기 조절하지 않습니다.
     스와이프 제스처로 볼륨 조절 활성화하기
-    스와이프 제스처로 볼륨 조절을 활성화합니다.
-    스와이프 제스처로 볼륨 조절을 비활성화합니다.
+    전체 화면 오른쪽에서 위로/아래로 스와이프하여 볼륨 조절합니다.
+    전체 화면 오른쪽에서 위로/아래로 스와이프하여 볼륨 조절하지 않습니다.
     화면 밝기 값 저장 및 복원 활성화하기
     전체 화면에서 나가거나 들어갈 때마다 화면 밝기 값을 저장 및 복원합니다.
     전체 화면에서 나가거나 들어갈 때마다 화면 밝기 값을 저장 및 복원하지 않습니다.
@@ -1493,9 +1503,9 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요."
     길게 눌러서 스와이프 제스처를 사용할 때, 진동 피드백 활성화하기
     진동 피드백을 활성화합니다.
     진동 피드백을 비활성화합니다.
-    잠금 화면 모드에서 스와이프 제스처 활성화하기
-    잠금 화면 모드에서 스와이프 제스처를 활성화합니다.
-    잠금 화면 모드에서 스와이프 제스처를 비활성화합니다.
+    잠금 화면에서 스와이프 제스처 활성화하기
+    잠금 화면에서 스와이프 제스처를 활성화합니다.
+    잠금 화면에서 스와이프 제스처를 비활성화합니다.
     스와이프 오버레이 배경 투명도
     스와이프 오버레이 배경 투명도 값을 지정할 수 있습니다.
     스와이프 한계치
@@ -1517,17 +1527,17 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요."
     HDR 자동 밝기를 비활성화합니다.
     HDR 자동 밝기를 활성화합니다.
     스와이프 제스처로 동영상 전환 비활성화하기
-    전체 화면 모드에서 위로/아래로 스와이프하여 다음/이전 동영상으로 전환하지 않습니다.
-    전체 화면 모드에서 위로/아래로 스와이프하여 다음/이전 동영상으로 전환합니다.
-    스와이프 제스처로 전체 화면 모드 전환 비활성화하기 (플레이어 하단)
-    플레이어 하단에서 아래로 스와이프하여 전체 화면 모드로 들어가지 않습니다.
-    플레이어 하단에서 아래로 스와이프하여 전체 화면 모드로 들어갑니다.
-    스와이프 제스처로 전체 화면 모드 전환 비활성화하기 (플레이어 내부)
-    플레이어 내부에서 위로 스와이프하여 전체 화면 모드로 들어가지 않습니다.
-    플레이어 내부에서 위로 스와이프하여 전체 화면 모드로 들어갑니다.
-    스와이프 제스처로 전체 화면 모드 종료 비활성화하기
-    전체 화면에서 아래로 스와이프하여 전체 화면 모드를 나가지 않습니다.
-    전체 화면에서 아래로 스와이프하여 전체 화면 모드를 나갑니다.
+    전체 화면 중앙에서 위로/아래로 스와이프하여 다음/이전 동영상으로 전환하지 않습니다.
+    전체 화면 중앙에서 위로/아래로 스와이프하여 다음/이전 동영상으로 전환합니다.
+    스와이프 제스처로 전체 화면 전환 비활성화하기 (플레이어 하단)
+    플레이어 하단에서 아래로 스와이프하여 전체 화면으로 들어가지 않습니다.
+    플레이어 하단에서 아래로 스와이프하여 전체 화면으로 들어갑니다.
+    스와이프 제스처로 전체 화면 전환 비활성화하기 (플레이어 내부)
+    플레이어 내부에서 위로 스와이프하여 전체 화면으로 들어가지 않습니다.
+    플레이어 내부에서 위로 스와이프하여 전체 화면으로 들어갑니다.
+    스와이프 제스처로 전체 화면 종료 비활성화하기
+    전체 화면에서 아래로 스와이프하여 전체 화면을 나가지 않습니다.
+    전체 화면에서 아래로 스와이프하여 전체 화면을 나갑니다.
     자동
     
     동영상
@@ -1904,7 +1914,7 @@ GmsCore 앱 배터리 최적화를 비활성화(제한 없음)하더라도, 배
     설정을 클립보드에 복사하였습니다.
     
     스트리밍 데이터 변경하기
-    스트리밍 데이터를 변경하여 재생 문제를 방지할 수 있습니다.
+    스트리밍 데이터를 변경하여 동영상 재생 문제를 방지할 수 있습니다.
     스트리밍 데이터 변경하기
     스트리밍 데이터를 변경합니다.
     "스트리밍 데이터를 변경하지 않습니다.\n동영상 재생 문제가 발생할 수 있습니다."
@@ -1914,7 +1924,7 @@ GmsCore 앱 배터리 최적화를 비활성화(제한 없음)하더라도, 배
 (로그인이 요구됨)"
     Android VR
     "Android VR
-(미인증)"
+(인증 없음)"
     "iOS
 (PoToken이 요구됨)"
     "iOS TV
@@ -1926,10 +1936,10 @@ GmsCore 앱 배터리 최적화를 비활성화(제한 없음)하더라도, 배
 • 사용자가 로그인을 하지 않았거나 시크릿 모드에서는 Kids 동영상이 재생되지 않을 수 있습니다.
 • VR은 Kids // TV는 재생목록과 음악 동영상에서 다른 클라이언트가 사용될 수 있습니다.
 • TV는 AV1 코덱이 지원되지 않습니다.
-• VR은 화질 저하 문제가 발생할 수 있습니다."
-    • 재생 문제가 발생할 수 있습니다 (PoToken이 요구됨).\n• 영화, 유료, 비공개 그리고 연령 제한 동영상에서 다른 클라이언트가 사용될 수 있습니다.
-    "• 영화 또는 유료 동영상이 재생되지 않을 수 있습니다.
-• 안정적인 볼륨을 사용할 수 없습니다.
+• 인증 없음: 사용자 로그인 정보를 사용하지 않음"
+    • 동영상 재생 문제가 발생할 수 있습니다 (PoToken이 요구됨).\n• 영화, 유료, 비공개 그리고 연령 제한 동영상에서 다른 클라이언트가 사용될 수 있습니다.
+    "• 안정적인 볼륨을 사용할 수 없습니다.
+• 영화 또는 유료 동영상이 재생되지 않을 수 있습니다.
 • 사용자가 로그인을 하지 않았거나 시크릿 모드에서는 Kids 동영상이 재생되지 않을 수 있습니다.
 • 음악 동영상에서 다른 클라이언트가 사용될 수 있습니다.
 • AV1 코덱이 지원되지 않습니다."
@@ -1939,6 +1949,14 @@ GmsCore 앱 배터리 최적화를 비활성화(제한 없음)하더라도, 배
     "이 설정를 활성화하면 배터리 수명이 향상되고, 동영상 재생 끊김 문제가 해결될 수 있습니다.
 
 AVC의 최대 화질 값은 1080p이고, OPUS 코덱을 사용불가 및 HDR 동영상을 재생할 수 없으며, 동영상을 재생했을 경우에는 VP9 또는 AV1보다 더 많은 모바일 데이터를 사용되오니 주의하세요."
+    Onesie 응답 암호화 건너뛰기
+    "Onesie 응답 암호화를 건너뜁니다.
+
+• 일부 사용자에게 발생되는 새로운 유형의 동영상 재생 문제가 고쳐질 수 있습니다.
+• AV1 코덱을 사용하지 못할 수 있습니다."
+    "Onesie 응답 암호화를 건너뛰지 않습니다.
+
+• 일부 사용자에게 새로운 유형의 동영상 재생 문제가 발생할 수 있습니다."
     전문 통계에서 표시하기
     스트리밍 데이터를 가져오는 데 사용되는 클라이언트가 전문 통계에서 표시됩니다.\n\n• 만약 선택한 기본 클라이언트가 재생할 수 없는 동영상을 재생하려하거나 클라이언트 재생 문제가 발생하면, 그 문제를 해결하기 위해 다른 클라이언트가 사용되는 경우도 있기 때문에 전문 통계에서 다르게 표시될 수 있습니다.
     스트리밍 데이터를 가져오는 데 사용되는 클라이언트가 전문 통계에서 숨겨집니다.\n\n• 만약 선택한 기본 클라이언트가 재생할 수 없는 동영상을 재생하려하거나 클라이언트 재생 문제가 발생하면, 그 문제를 해결하기 위해 다른 클라이언트가 사용되는 경우도 있기 때문에 전문 통계에서 다르게 표시될 수 있습니다.
@@ -1946,9 +1964,9 @@ AVC의 최대 화질 값은 1080p이고, OPUS 코덱을 사용불가 및 HDR 동
     
     PoToken & VisitorData
     PoToken 등록하기
-    정보 메뉴와 연결된 페이지에 있는 사용법을 따라하여 발급받은 PoToken을 등록할 수 있습니다.
+    \'신뢰할 수 있는 브라우저에서 BotGuard에 의해 발급된 PoToken\'을 등록할 수 있습니다.\n\n• PoToken 발급 방법은 \'PoToken & VisitorData에 대한 정보\' 메뉴와 연결된 페이지에 나와있습니다.
     VisitorData 등록하기
-    정보 메뉴와 연결된 페이지에 있는 사용법을 따라하여 발급받은 VisitorData를 등록할 수 있습니다.
+    \'신뢰할 수 있는 브라우저에서 BotGuard에 의해 발급된 VisitorData\'를 등록할 수 있습니다.\n\n• VisitorData 발급 방법은 \'PoToken & VisitorData에 대한 정보\' 메뉴와 연결된 페이지에 나와있습니다.
     PoToken & VisitorData에 대한 정보
     "일부 클라이언트는 유효한 스트리밍 데이터 응답을 받기 위해 PoToken과 VisitorData가 요구됩니다.
 
diff --git a/patches/src/main/resources/youtube/translations/pl-rPL/strings.xml b/patches/src/main/resources/youtube/translations/pl-rPL/strings.xml
index 9d6bbba59..2a409d692 100644
--- a/patches/src/main/resources/youtube/translations/pl-rPL/strings.xml
+++ b/patches/src/main/resources/youtube/translations/pl-rPL/strings.xml
@@ -409,17 +409,27 @@ Ograniczenie: Przycisk wstecz na pasku narzędzi może nie działać."
     Usuń okno dialogowe treści ograniczonej do oglądania
     "Usuwa okno dialogowe treści ograniczonej do oglądania.
 Nie pomija to ograniczeń wiekowych, lecz akceptuje je automatycznie."
-    Układ aplikacji
-    Oryginalny
-    Telefonowy
-    Telefonowy (max. 480 dpi)
-    Tabletowy
-    Tabletowy (max. 600 dpi)
     Zachowanie po kliknięciu okręgu od transmisji na żywo
     "Otwarcie kanału
 
 Ograniczenie: jeśli transmisja na żywo Shortsa jest otwierana w zwykłym odtwarzaczu przy użyciu opcji 'Otwieranie Shortsów w zwykłym odtwarzaczu', kanał się nie otworzy."
     Otwarcie transmisji na żywo
+    Współczynik kształtu układu aplikacji
+    Domyślny
+    Telefonowy
+    Telefonowy (max. 480 dpi)
+    Tabletowy
+    Tabletowy (max. 600 dpi)
+    Samochodowy
+    "Zmiany zawierają:
+
+Układ tabletowy:
+• Posty społeczności są ukryte
+
+Układ samochodowy:
+• Shortsy otwierają się w zwykłym odtwarzaczu
+• Strona główna jest sortowana po tematach i kanałach
+• Opis filmu nie może być otwarty po wyłączeniu 'Oszukuj strumień danych'"
     Oszukiwanie wersji aplikacji
     Włączone
     Wyłączone
@@ -1921,7 +1931,8 @@ Stuknij przycisk kontynuacji i zezwól na zmiany w optymalizacji."
 • Automatyczne wymuszenie ścieżki dźwiękowej nie jest dostępne
 • Filmy dla dzieci mogą się nie odtwarzać będąc zalogowanym lub w trybie incognito"
     • Mogą wystąpić problemy z odtwarzaniem (wymagany PoToken)
-    "• Filmy kinowe lub płatne mogą się nie odtwarzać
+    "• Stabilna głośność nie jest dostępna
+• Filmy kinowe lub płatne mogą się nie odtwarzać
 • Filmy dla dzieci mogą się nie odtwarzać będąc zalogowanym lub w trybie incognito"
     Wymuś kodek iOS AVC (H.264)
     Wymuszony
@@ -1929,6 +1940,14 @@ Stuknij przycisk kontynuacji i zezwól na zmiany w optymalizacji."
     "Włączenie tego może poprawić czas pracy baterii i naprawić zacinanie się odtwarzacza.
 
 AVC obsługuje maksymalnie rozdzielczość 1080p, kodek audio OPUS nie jest dostępny, a odtwarzanie wideo będzie zużywać więcej danych internetowych niż VP9 lub AV1."
+    Pomijaj szyfrowanie odpowiedzi Onesie
+    "Tak
+
+• Naprawia nowy typ problemu z odtwarzaniem, którego doświadczają niektórzy użytkownicy
+• Kodek AV1 może nie być dostępny"
+    "Nie
+• Niektórzy użytkownicy mogą doświadczyć nowego typu problemów z odtwarzaniem
+"
     Informacja w statystykach dla nerdów
     Widoczna
     Ukryta
diff --git a/patches/src/main/resources/youtube/translations/pt-rBR/strings.xml b/patches/src/main/resources/youtube/translations/pt-rBR/strings.xml
index 1eef8b0bd..a3668074b 100644
--- a/patches/src/main/resources/youtube/translations/pt-rBR/strings.xml
+++ b/patches/src/main/resources/youtube/translations/pt-rBR/strings.xml
@@ -404,12 +404,6 @@ Limitação: O botão voltar na barra de ferramentas pode não funcionar."Remover o diálogo discricionário do visualizador
     "Remover o diálogo discricionário de visualização.
 Isso não ignora a restrição de idade, apenas aceita isso automaticamente."
-    Alterar layout
-    Original
-    Telefone
-    Telefone (Máximo 480 dp)
-    Tablet
-    Tablet (Min 600 dp)
     Alterar ação de clique do anel ao vivo
     "O canal é aberto quando o anel ao vivo é clicado."
     A transmissão ao vivo é aberta quando o anel ao vivo é clicado.
diff --git a/patches/src/main/resources/youtube/translations/ru-rRU/strings.xml b/patches/src/main/resources/youtube/translations/ru-rRU/strings.xml
index fa2574136..c1e134bcd 100644
--- a/patches/src/main/resources/youtube/translations/ru-rRU/strings.xml
+++ b/patches/src/main/resources/youtube/translations/ru-rRU/strings.xml
@@ -418,15 +418,25 @@ Shorts
     Серые разделители отображены.
     Скрыть диалог возрастного ограничения
     "Возрастные ограничения будут приниматься автоматически."
-    Изменить макет
-    Оригинал
-    Телефон
-    Телефон (Макс. 480 dip)
-    Планшет
-    Планшет (Мин. 600 dip)
     Действие нажатия на точку прямого эфира
     "Открывается канал."
     Открывается прямой эфир.
+    Макет интерфейса
+    Стандартный
+    Телефон
+    Телефон (Макс. 480 dip)
+    Планшет
+    Планшет (Мин. 600 dip)
+    Автомобильный
+    "Изменения включают:
+
+Макет для планшета
+• Сообщения сообщества скрыты.
+
+Автомобильный макет
+• Shorts открываются в обычном плеере.
+• Лента организована по темам и каналам.
+• Описание видео нельзя открыть, если «Подмена потоковых данных» отключена."
     Подмена версии приложения
     Версия приложения подменена
     Версия приложения не подменена
@@ -1477,10 +1487,10 @@ Shorts
     Заменить псевдоним канала
     Используется имя канала.
     Используется псевдоним канала.
-    Старый интерфейс проигрывателя
-    "Старый интерфейс проигрывателя включен.
-Без отступов сверху и снизу плеера."
-    Старый интерфейс проигрывателя отключен.
+    Старый интерфейс плеера
+    "Старый макет плеера используется.
+Нет полей сверху и снизу плеера."
+    Старый интерфейс плеера отключен.
     
     Управление жестами
     Управление автояркостью жестом
@@ -1946,6 +1956,14 @@ Shorts
 - устранить прерывания при воспроизведении.
 
 Максимальное разрешение видео AVC составляет 1080p. Кодек Opus недоступен. Потребление интернет трафика будет больше, чем при VP9 или AV1."
+    Пропустить шифрование ответа Onesie
+    "Шифрование ответа Onesie пропускается.
+
+• Исправлен новый тип воспроизведения, с которыми сталкиваются некоторые пользователи.
+• Кодек AV1 может быть недоступен."
+    "Шифрование ответа Onesie не пропускается.
+
+• Некоторые пользователи могут сталкиваться с новым типом проблемы воспроизведения."
     Статистике для сисадминов
     Клиент потоковых данных отображается в Статистике для сисадминов.
     Клиент потоковых данных скрыт в Статистике для сисадминов.
diff --git a/patches/src/main/resources/youtube/translations/tr-rTR/strings.xml b/patches/src/main/resources/youtube/translations/tr-rTR/strings.xml
index 09db58c12..23ebdab7c 100644
--- a/patches/src/main/resources/youtube/translations/tr-rTR/strings.xml
+++ b/patches/src/main/resources/youtube/translations/tr-rTR/strings.xml
@@ -399,12 +399,6 @@ Sınırlama: Araç çubuğundaki geri butonu çalışmayabilir."
     Video ve topluluk gönderisi arasındaki gri çubuk gizlenmiyor.
     İzleyicinin takdirine bağlı iletişim kutusunu kaldır
     "Görüntüleyicinin takdirine ilişkin iletişim kutusunu kaldırır. Bu yaş sınırlamasını atlamaz. Sadece otomatik olarak kabul ediyor."
-    Düzeni değiştir
-    Orijinal
-    Telefon
-    Telefon (En fazla 480 dp)
-    Tablet
-    Tablet (En az 600 dp)
     Canlı halka eylemini değiştir
     "Canlı halkaya tıklandığında kanal açılır.
 
diff --git a/patches/src/main/resources/youtube/translations/uk-rUA/strings.xml b/patches/src/main/resources/youtube/translations/uk-rUA/strings.xml
index 5088a7450..ccf60c3de 100644
--- a/patches/src/main/resources/youtube/translations/uk-rUA/strings.xml
+++ b/patches/src/main/resources/youtube/translations/uk-rUA/strings.xml
@@ -409,17 +409,27 @@
     Вилучати діалогове вікно
     "Вилучається діалогове вікно.
 Це не обходить вікові обмеження, а просто приймається автоматично."
-    Змінити макет
-    Оригінал
-    Телефонний
-    Телефонний (Макс 480 dp)
-    Планшетний
-    Планшетний (Мін 600 dp)
     Змінити дію натискання на кружечок наживо
     "При натисканні на кружечок наживо відкривається канал.
 
 Застереження: Коли відкрито пряму трансляцію Shorts у поточному плеєрі налаштуванням 'Відкривати Shorts у поточному плеєрі', канал не відкривається."
     При натисканні на кружечок наживо відкривається пряма трансляція.
+    Формат макету
+    Стандартний
+    Телефонний
+    Телефонний (Макс 480 dp)
+    Планшетний
+    Планшетний (Мін 600 dp)
+    Автомобільний
+    "Зміни включають:
+
+Планшетний макет
+• Публікації спільноти приховано.
+
+Автомобільний макет
+• Shorts відкриваються у поточному плеєрі.
+• Стрічка організована за темами та каналами.
+• Опис відео не відкривається, якщо вимкнена функція 'Підробити дані трансляції'."
     Підробити версію програми
     Версію підроблено
     Версію не підроблено
@@ -1915,7 +1925,8 @@
 • Вимкнення примусових авто звукових доріжок недоступне.
 • Відео для дітей можуть не відтворюватися коли вийшли з системи або в анонімному режимі."
     • Можуть бути проблеми з відтворенням (Потрібен PoToken).
-    "• Фільми та платні відео можуть не відтворюватися.
+    "• Стабілізація гучності недоступна.
+• Фільми та платні відео можуть не відтворюватися.
 • Відео для дітей можуть не відтворюватися коли вийшли з системи або в анонімному режимі."
     Примусово AVC (H.264) iOS
     Примусово увімкнено відеокодек AVC (H.264).
@@ -1923,6 +1934,14 @@
     "Увімкнення може зменшити споживання акумулятора та виправити затинання відтворення.
 
 AVC має максимальну роздільну здатність 1080p, аудіокодек Opus недоступний, а відтворення відео використовуватиме більше інтернет-даних, ніж кодек VP9 чи AV1."
+    Пропустити шифрування відповіді Onesie
+    "Шифрування відповіді Onesie пропускається.
+
+• Виправлено новий тип проблеми відтворення, з якою стикаються деякі користувачі.
+• Кодек AV1 може бути недоступний."
+    "Шифрування відповіді Onesie не пропускається.
+
+• Деякі користувачі можуть стикатися з новим типом проблеми відтворення."
     Показувати в Статистика для сисадмінів
     Клієнт, що використовується для отримання даних трансляції показується у Статистика для сисадмінів.
     Клієнт, що використовується для отримання даних трансляції приховано у Статистика для сисадмінів.
diff --git a/patches/src/main/resources/youtube/translations/vi-rVN/strings.xml b/patches/src/main/resources/youtube/translations/vi-rVN/strings.xml
index b9f2271c2..67bdd30be 100644
--- a/patches/src/main/resources/youtube/translations/vi-rVN/strings.xml
+++ b/patches/src/main/resources/youtube/translations/vi-rVN/strings.xml
@@ -409,17 +409,27 @@ Hạn chế: Nút Quay lại trên thanh công cụ có thể không hoạt đ
     Loại bỏ hộp thoại cảnh báo trước khi xem
     "Loại bỏ hộp thoại cảnh báo nội dung cần cân nhắc trước khi xem.
 Tuỳ chọn này chỉ tự động chấp nhận hộp thoại cảnh báo, chứ không thể bỏ qua giới hạn về độ tuổi."
-    Thay đổi giao diện
-    Gốc
-    Điện thoại
-    Điện thoại (Tối đa 480 dp)
-    Máy tính bảng
-    Máy tính bảng (Tối thiểu 600 dp)
     Thay đổi thao tác nhấn vào vòng phát trực tiếp
     "Mở kênh khi nhấn vào vòng phát trực tiếp.
 
-Hạn chế: Khi video ngắn phát trực tiếp được mở trong trình phát do cài đặt \"Mở video ngắn trong trình phát\", thì kênh sẽ không được mở."
+Hạn chế: Khi video ngắn phát trực tiếp được mở trong trình phát thông thường do cài đặt \"Mở video ngắn trong trình phát\", thì kênh sẽ không được mở."
     Mở video đang phát trực tiếp khi nhấn vào vòng phát trực tiếp.
+    Hình thức bố cục
+    Mặc định
+    Điện thoại
+    Điện thoại (Tối đa 480 dp)
+    Máy tính bảng
+    Máy tính bảng (Tối thiểu 600 dp)
+    Màn hình ô tô
+    "Các thay đổi bao gồm:
+
+Bố cục máy tính bảng
+• Bài đăng cộng đồng bị ẩn.
+
+Bố cục màn hình ô tô
+• Video ngắn sẽ được mở trong trình phát thông thường.
+• Bảng tin được sắp xếp theo chủ đề và kênh.
+• Không thể mở mô tả video khi cài đặt \"Giả mạo luồng dữ liệu trực tuyến\" đang tắt."
     Giả mạo phiên bản ứng dụng
     Phiên bản được giả mạo
     Phiên bản không được giả mạo
@@ -1283,8 +1293,8 @@ Chi tiết:
     Mặc định
     Dừng
     Lặp lại
-    Mở video ngắn trong trình phát
-    Đang mở video ngắn trong trình phát thay vì trình phát Shorts.
+    Mở video ngắn trong trình phát thông thường
+    Đang mở video ngắn trong trình phát thông thường thay vì trình phát Shorts.
     Đang mở video ngắn trong trình phát Shorts.
     
     Trình phát Shorts
@@ -1916,7 +1926,8 @@ Nhấn vào Tiếp tục và cho phép thay đổi lựa chọn tối ưu hoá p
 • Tắt Bản âm thanh tự động không khả dụng.
 • Video dành cho trẻ em có thể không phát được khi bạn đã đăng xuất hoặc bật chế độ ẩn danh."
     • Có thể xảy ra sự cố phát (Không khuyến khích).
-    "• Phim hoặc video trả phí có thể không phát được.
+    "• Âm lượng ổn định không khả dụng.
+• Phim hoặc video trả phí có thể không phát được.
 • Video dành cho trẻ em có thể không phát được khi bạn đã đăng xuất hoặc bật chế độ ẩn danh."
     Bắt buộc iOS sử dụng AVC (H.264)
     Codec video luôn là AVC (H.264).
@@ -1927,6 +1938,14 @@ Hạn chế:
 • AVC có độ phân giải tối đa là 1080p.
 • Codec âm thanh OPUS không khả dụng.
 • Dùng nhiều dữ liệu di động hơn với VP9 hoặc AV1."
+    Bỏ qua mã hóa phản hồi Onesie
+    "Bỏ qua mã hóa phản hồi Onesie.
+
+• Khắc phục sự cố phát mới mà một số người dùng đang gặp phải.
+• Codec AV1 có thể không khả dụng."
+    "Không bỏ qua mã hoá phản hồi Onesie.
+
+• Một số người dùng có thể gặp phải sự cố phát mới."
     Hiển thị trong Thống kê chi tiết
     Ứng dụng khách sử dụng để nạp luồng dữ liệu trực tuyến được hiển thị trong Thống kê chi tiết.
     Ứng dụng khách sử dụng để nạp luồng dữ liệu trực tuyến đã ẩn trong Thống kê chi tiết.
diff --git a/patches/src/main/resources/youtube/translations/zh-rCN/strings.xml b/patches/src/main/resources/youtube/translations/zh-rCN/strings.xml
index 8d891cde6..76505ebc0 100644
--- a/patches/src/main/resources/youtube/translations/zh-rCN/strings.xml
+++ b/patches/src/main/resources/youtube/translations/zh-rCN/strings.xml
@@ -387,12 +387,6 @@
     移除查看器的自由裁量对话框
     "移除查看器的自由裁量对话框
 这不会绕过年龄限制它只会自动同意"
-    调整布局
-    原版
-    手机
-    手机 (最大 480 dip)
-    平板
-    平板 (最小 600 dip)
     伪装应用版本
     客户端版本已伪装
     客户端版本未伪装
diff --git a/patches/src/main/resources/youtube/translations/zh-rTW/strings.xml b/patches/src/main/resources/youtube/translations/zh-rTW/strings.xml
index 22b51a325..4c6d7ebc5 100644
--- a/patches/src/main/resources/youtube/translations/zh-rTW/strings.xml
+++ b/patches/src/main/resources/youtube/translations/zh-rTW/strings.xml
@@ -405,12 +405,6 @@
     移除查看器的自由裁量對話框
     "移除觀眾酌情觀看對話方塊。
 這項選項不會繞過年齡限制,它只會自動接受。"
-    變更佈局
-    原始
-    手機
-    手機 (最大 480 dip)
-    平板電腦
-    平板電腦 (最少 600 dip)
     變更點擊直播狀態頻道圖示的動作
     "點擊直播按鈕 頻道將會開啟。"
     點擊直播按鈕 直播將會開啟。