mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-04-29 22:24:31 +02:00
Merge branch 'dev' into revanced-extended
This commit is contained in:
commit
26bf2f4b82
257
README.md
257
README.md
@ -11,72 +11,73 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
|
|||||||
|
|
||||||
| 💊 Patch | 📜 Description | 🏹 Target Version |
|
| 💊 Patch | 📜 Description | 🏹 Target Version |
|
||||||
|:--------:|:--------------:|:-----------------:|
|
|:--------:|:--------------:|:-----------------:|
|
||||||
| `Alternative thumbnails` | Adds options to replace video thumbnails using the DeArrow API or image captures from the video. | 18.29.38 ~ 19.44.39 |
|
| `Alternative thumbnails` | Adds options to replace video thumbnails using the DeArrow API or image captures from the video. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Ambient mode control` | Adds options to disable Ambient mode and to bypass Ambient mode restrictions. | 18.29.38 ~ 19.44.39 |
|
| `Ambient mode control` | Adds options to disable Ambient mode and to bypass Ambient mode restrictions. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Bypass URL redirects` | Adds an option to bypass URL redirects and open the original URL directly. | 18.29.38 ~ 19.44.39 |
|
| `Bypass URL redirects` | Adds an option to bypass URL redirects and open the original URL directly. | 19.05.36 ~ 19.47.53 |
|
||||||
| `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 |
|
| `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. | 19.05.36 ~ 19.47.53 |
|
||||||
| `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 form factor` | Adds an option to change the UI appearance to a phone, tablet, or automotive device. | 19.05.36 ~ 19.47.53 |
|
||||||
| `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 live ring click action` | Adds an option to open the channel instead of the live stream when clicking on the live ring. | 19.05.36 ~ 19.47.53 |
|
||||||
| `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 player flyout menu toggles` | Adds an option to use text toggles instead of switch toggles within the additional settings menu. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 18.29.38 ~ 19.44.39 |
|
| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 18.29.38 ~ 19.44.39 |
|
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Custom Shorts action buttons` | Changes, at compile time, the icon of the action buttons of the Shorts player. | 18.29.38 ~ 19.44.39 |
|
| `Custom Shorts action buttons` | Changes, at compile time, the icon of the action buttons of the Shorts player. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Custom branding icon for YouTube` | Changes the YouTube app icon to the icon specified in patch options. | 18.29.38 ~ 19.44.39 |
|
| `Custom branding icon for YouTube` | Changes the YouTube app icon to the icon specified in patch options. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Custom branding name for YouTube` | Changes the YouTube app name to the name specified in patch options. | 18.29.38 ~ 19.44.39 |
|
| `Custom branding name for YouTube` | Changes the YouTube app name to the name specified in patch options. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Custom double tap length` | Adds Double-tap to seek values that are specified in patch options. | 18.29.38 ~ 19.44.39 |
|
| `Custom double tap length` | Adds Double-tap to seek values that are specified in patch options. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Custom header for YouTube` | Applies a custom header in the top left corner within the app. | 18.29.38 ~ 19.44.39 |
|
| `Custom header for YouTube` | Applies a custom header in the top left corner within the app. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Description components` | Adds options to hide and disable description components. | 18.29.38 ~ 19.44.39 |
|
| `Description components` | Adds options to hide and disable description components. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 18.29.38 ~ 19.44.39 |
|
| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Disable forced auto audio tracks` | Adds an option to disable audio tracks from being automatically enabled. | 18.29.38 ~ 19.44.39 |
|
| `Disable forced auto audio tracks` | Adds an option to disable audio tracks from being automatically enabled. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 18.29.38 ~ 19.44.39 |
|
| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Disable haptic feedback` | Adds options to disable haptic feedback when swiping in the video player. | 18.29.38 ~ 19.44.39 |
|
| `Disable haptic feedback` | Adds options to disable haptic feedback when swiping in the video player. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Disable resuming Miniplayer on startup` | Adds an option to disable the Miniplayer 'Continue watching' from resuming on app startup. | 18.29.38 ~ 19.44.39 |
|
| `Disable layout updates` | Adds an option to disable layout updates by server. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Disable resuming Shorts on startup` | Adds an option to disable the Shorts player from resuming on app startup when Shorts were last being watched. | 18.29.38 ~ 19.44.39 |
|
| `Disable resuming Miniplayer on startup` | Adds an option to disable the Miniplayer 'Continue watching' from resuming on app startup. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Disable splash animation` | Adds an option to disable the splash animation on app startup. | 18.29.38 ~ 19.44.39 |
|
| `Disable resuming Shorts on startup` | Adds an option to disable the Shorts player from resuming on app startup when Shorts were last being watched. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 18.29.38 ~ 19.44.39 |
|
| `Disable splash animation` | Adds an option to disable the splash animation on app startup. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Enable debug logging` | Adds an option to enable debug logging. | 18.29.38 ~ 19.44.39 |
|
| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Enable gradient loading screen` | Adds an option to enable the gradient loading screen. | 18.29.38 ~ 19.44.39 |
|
| `Enable debug logging` | Adds an option to enable debug logging. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Force hide player buttons background` | Removes, at compile time, the dark background surrounding the video player controls. | 18.29.38 ~ 19.44.39 |
|
| `Enable gradient loading screen` | Adds an option to enable the gradient loading screen. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Fullscreen components` | Adds options to hide or change components related to fullscreen. | 18.29.38 ~ 19.44.39 |
|
| `Force hide player buttons background` | Removes, at compile time, the dark background surrounding the video player controls. | 19.05.36 ~ 19.47.53 |
|
||||||
| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 18.29.38 ~ 19.44.39 |
|
| `Fullscreen components` | Adds options to hide or change components related to fullscreen. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hide Shorts dimming` | Removes, at compile time, the dimming effect at the top and bottom of Shorts videos. | 18.29.38 ~ 19.44.39 |
|
| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hide accessibility controls dialog` | Removes, at compile time, accessibility controls dialog 'Turn on accessibility controls for the video player?'. | 18.29.38 ~ 19.44.39 |
|
| `Hide Shorts dimming` | Removes, at compile time, the dimming effect at the top and bottom of Shorts videos. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hide action buttons` | Adds options to hide action buttons under videos. | 18.29.38 ~ 19.44.39 |
|
| `Hide accessibility controls dialog` | Removes, at compile time, accessibility controls dialog 'Turn on accessibility controls for the video player?'. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hide ads` | Adds options to hide ads. | 18.29.38 ~ 19.44.39 |
|
| `Hide action buttons` | Adds options to hide action buttons under videos. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hide comments components` | Adds options to hide components related to comments. | 18.29.38 ~ 19.44.39 |
|
| `Hide ads` | Adds options to hide ads. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hide feed components` | Adds options to hide components related to feeds. | 18.29.38 ~ 19.44.39 |
|
| `Hide comments components` | Adds options to hide components related to comments. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hide feed flyout menu` | Adds the ability to hide feed flyout menu components using a custom filter. | 18.29.38 ~ 19.44.39 |
|
| `Hide feed components` | Adds options to hide components related to feeds. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hide layout components` | Adds options to hide general layout components. | 18.29.38 ~ 19.44.39 |
|
| `Hide feed flyout menu` | Adds the ability to hide feed flyout menu components using a custom filter. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hide player buttons` | Adds options to hide buttons in the video player. | 18.29.38 ~ 19.44.39 |
|
| `Hide layout components` | Adds options to hide general layout components. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hide player flyout menu` | Adds options to hide player flyout menu components. | 18.29.38 ~ 19.44.39 |
|
| `Hide player buttons` | Adds options to hide buttons in the video player. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hide shortcuts` | Remove, at compile time, the app shortcuts that appears when the app icon is long pressed. | 18.29.38 ~ 19.44.39 |
|
| `Hide player flyout menu` | Adds options to hide player flyout menu components. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hook YouTube Music actions` | Adds support for opening music in RVX Music using the in-app YouTube Music button. | 18.29.38 ~ 19.44.39 |
|
| `Hide shortcuts` | Remove, at compile time, the app shortcuts that appears when the app icon is long pressed. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Hook download actions` | Adds support to download videos with an external downloader app using the in-app download button. | 18.29.38 ~ 19.44.39 |
|
| `Hook YouTube Music actions` | Adds support for opening music in RVX Music using the in-app YouTube Music button. | 19.05.36 ~ 19.47.53 |
|
||||||
| `MaterialYou` | Applies the MaterialYou theme for Android 12+ devices. | 18.29.38 ~ 19.44.39 |
|
| `Hook download actions` | Adds support to download videos with an external downloader app using the in-app download button. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Miniplayer` | Adds options to change the in-app minimized player, and if patching target 19.16+ adds options to use modern miniplayers. | 18.29.38 ~ 19.44.39 |
|
| `MaterialYou` | Applies the MaterialYou theme for Android 12+ devices. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 18.29.38 ~ 19.44.39 |
|
| `Miniplayer` | Adds options to change the in-app minimized player, and if patching target 19.16+ adds options to use modern miniplayers. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Open links externally` | Adds an option to always open links in your browser instead of the in-app browser. | 18.29.38 ~ 19.44.39 |
|
| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Overlay buttons` | Adds options to display useful overlay buttons in the video player. | 18.29.38 ~ 19.44.39 |
|
| `Open links externally` | Adds an option to always open links in your browser instead of the in-app browser. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Player components` | Adds options to hide or change components related to the video player. | 18.29.38 ~ 19.44.39 |
|
| `Overlay buttons` | Adds options to display useful overlay buttons in the video player. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Remove background playback restrictions` | Removes restrictions on background playback, including for music and kids videos. | 18.29.38 ~ 19.44.39 |
|
| `Player components` | Adds options to hide or change components related to the video player. | 19.05.36 ~ 19.47.53 |
|
||||||
| `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. | 18.29.38 ~ 19.44.39 |
|
| `Remove background playback restrictions` | Removes restrictions on background playback, including for music and kids videos. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Return YouTube Dislike` | Adds an option to show the dislike count of videos using the Return YouTube Dislike API. | 18.29.38 ~ 19.44.39 |
|
| `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. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 18.29.38 ~ 19.44.39 |
|
| `Return YouTube Dislike` | Adds an option to show the dislike count of videos using the Return YouTube Dislike API. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 18.29.38 ~ 19.44.39 |
|
| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Seekbar components` | Adds options to hide or change components related to the seekbar. | 18.29.38 ~ 19.44.39 |
|
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Settings for YouTube` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 18.29.38 ~ 19.44.39 |
|
| `Seekbar components` | Adds options to hide or change components related to the seekbar. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Shorts components` | Adds options to hide or change components related to YouTube Shorts. | 18.29.38 ~ 19.44.39 |
|
| `Settings for YouTube` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Snack bar components` | Adds options to hide or change components related to the snack bar. | 18.29.38 ~ 19.44.39 |
|
| `Shorts components` | Adds options to hide or change components related to YouTube Shorts. | 19.05.36 ~ 19.47.53 |
|
||||||
| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content. | 18.29.38 ~ 19.44.39 |
|
| `Snack bar components` | Adds options to hide or change components related to the snack bar. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Spoof app version` | Adds options to spoof the YouTube client version. This can be used to restore old UI elements and features. | 18.29.38 ~ 19.44.39 |
|
| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Spoof streaming data` | Adds options to spoof the streaming data to allow playback. | 18.29.38 ~ 19.44.39 |
|
| `Spoof app version` | Adds options to spoof the YouTube client version. This can be used to restore old UI elements and features. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Swipe controls` | Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player. | 18.29.38 ~ 19.44.39 |
|
| `Spoof streaming data` | Adds options to spoof the streaming data to allow playback. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Theme` | Changes the app's themes to the values specified in patch options. | 18.29.38 ~ 19.44.39 |
|
| `Swipe controls` | Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Toolbar components` | Adds options to hide or change components located on the toolbar, such as the search bar, header, and toolbar buttons. | 18.29.38 ~ 19.44.39 |
|
| `Theme` | Changes the app's themes to the values specified in patch options. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Translations for YouTube` | Add translations or remove string resources. | 18.29.38 ~ 19.44.39 |
|
| `Toolbar components` | Adds options to hide or change components located on the toolbar, such as the search bar, header, and toolbar buttons. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 18.29.38 ~ 19.44.39 |
|
| `Translations for YouTube` | Add translations or remove string resources. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Visual preferences icons for YouTube` | Adds icons to specific preferences in the settings. | 18.29.38 ~ 19.44.39 |
|
| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 19.05.36 ~ 19.47.53 |
|
||||||
| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 18.29.38 ~ 19.44.39 |
|
| `Visual preferences icons for YouTube` | Adds icons to specific preferences in the settings. | 19.05.36 ~ 19.47.53 |
|
||||||
|
| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 19.05.36 ~ 19.47.53 |
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### [📦 `com.google.android.apps.youtube.music`](https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.music)
|
### [📦 `com.google.android.apps.youtube.music`](https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.music)
|
||||||
@ -84,49 +85,48 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
|
|||||||
|
|
||||||
| 💊 Patch | 📜 Description | 🏹 Target Version |
|
| 💊 Patch | 📜 Description | 🏹 Target Version |
|
||||||
|:--------:|:--------------:|:-----------------:|
|
|:--------:|:--------------:|:-----------------:|
|
||||||
| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 8.10.51 |
|
| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 8.12.53 |
|
||||||
| `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 |
|
| `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.12.53 |
|
||||||
| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 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.12.53 |
|
||||||
| `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 share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 6.20.51 ~ 8.12.53 |
|
||||||
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 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.12.53 |
|
||||||
| `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 icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 8.12.53 |
|
||||||
| `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 branding name for YouTube Music` | Changes the YouTube Music app name to the name specified in patch options. | 6.20.51 ~ 8.12.53 |
|
||||||
| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 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.12.53 |
|
||||||
| `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 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.12.53 |
|
||||||
| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.10.51 |
|
| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.12.53 |
|
||||||
| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 8.10.51 |
|
| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 8.12.53 |
|
||||||
| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 6.20.51 ~ 8.10.51 |
|
| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 6.20.51 ~ 8.12.53 |
|
||||||
| `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 dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 8.12.53 |
|
||||||
| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 8.10.51 |
|
| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 8.12.53 |
|
||||||
| `Disable music video in album` | Adds option to redirect music videos from albums for non-premium users. | 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.12.53 |
|
||||||
| `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 OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 6.20.51 ~ 8.12.53 |
|
||||||
| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 8.10.51 |
|
| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 8.12.53 |
|
||||||
| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 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.12.53 |
|
||||||
| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 8.10.51 |
|
| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 8.12.53 |
|
||||||
| `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 |
|
| `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.12.53 |
|
||||||
| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.10.51 |
|
| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.12.53 |
|
||||||
| `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 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.12.53 |
|
||||||
| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.10.51 |
|
| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.12.53 |
|
||||||
| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 8.10.51 |
|
| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 8.12.53 |
|
||||||
| `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 overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 8.12.53 |
|
||||||
| `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 |
|
| `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 8.12.53 |
|
||||||
| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 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.12.53 |
|
||||||
| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.10.51 |
|
| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.12.53 |
|
||||||
| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 8.10.51 |
|
| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 8.12.53 |
|
||||||
| `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 |
|
| `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.12.53 |
|
||||||
| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 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.12.53 |
|
||||||
| `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 Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 8.12.53 |
|
||||||
| `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 |
|
| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 8.12.53 |
|
||||||
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 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.12.53 |
|
||||||
| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 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.12.53 |
|
||||||
| `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 |
|
| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 8.12.53 |
|
||||||
| `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 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 ~ 8.10.52 |
|
||||||
| `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.12.53 |
|
||||||
| `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.12.53 |
|
||||||
| `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.12.53 |
|
||||||
| `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.12.53 |
|
||||||
| `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.12.53 |
|
||||||
| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 6.20.51 ~ 8.10.51 |
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### [📦 `com.reddit.frontpage`](https://play.google.com/store/apps/details?id=com.reddit.frontpage)
|
### [📦 `com.reddit.frontpage`](https://play.google.com/store/apps/details?id=com.reddit.frontpage)
|
||||||
@ -134,19 +134,19 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
|
|||||||
|
|
||||||
| 💊 Patch | 📜 Description | 🏹 Target Version |
|
| 💊 Patch | 📜 Description | 🏹 Target Version |
|
||||||
|:--------:|:--------------:|:-----------------:|
|
|:--------:|:--------------:|:-----------------:|
|
||||||
| `Change package name` | Changes the package name for Reddit to the name specified in patch options. | 2024.17.0 ~ 2025.05.1 |
|
| `Change package name` | Changes the package name for Reddit to the name specified in patch options. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Custom branding name for Reddit` | Changes the Reddit app name to the name specified in patch options. | 2024.17.0 ~ 2025.05.1 |
|
| `Custom branding name for Reddit` | Changes the Reddit app name to the name specified in patch options. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2024.17.0 ~ 2025.05.1 |
|
| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2024.17.0 ~ 2025.05.1 |
|
| `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Hide ads` | Adds options to hide ads. | 2024.17.0 ~ 2025.05.1 |
|
| `Hide ads` | Adds options to hide ads. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | 2024.17.0 ~ 2025.05.1 |
|
| `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Hide recommended communities shelf` | Adds an option to hide the recommended communities shelves in subreddits. | 2024.17.0 ~ 2025.05.1 |
|
| `Hide recommended communities shelf` | Adds an option to hide the recommended communities shelves in subreddits. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Open links directly` | Adds an option to skip over redirection URLs in external links. | 2024.17.0 ~ 2025.05.1 |
|
| `Open links directly` | Adds an option to skip over redirection URLs in external links. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Open links externally` | Adds an option to always open links in your browser instead of in the in-app-browser. | 2024.17.0 ~ 2025.05.1 |
|
| `Open links externally` | Adds an option to always open links in your browser instead of in the in-app-browser. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Premium icon` | Unlocks premium app icons. | 2024.17.0 ~ 2025.05.1 |
|
| `Premium icon` | Unlocks premium app icons. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Remove subreddit dialog` | Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically. | 2024.17.0 ~ 2025.05.1 |
|
| `Remove subreddit dialog` | Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 2024.17.0 ~ 2025.05.1 |
|
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 2024.17.0 ~ 2025.12.0 |
|
||||||
| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2024.17.0 ~ 2025.05.1 |
|
| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2024.17.0 ~ 2025.12.0 |
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|
||||||
@ -165,13 +165,11 @@ Example:
|
|||||||
"use":true,
|
"use":true,
|
||||||
"compatiblePackages": {
|
"compatiblePackages": {
|
||||||
"com.google.android.youtube": [
|
"com.google.android.youtube": [
|
||||||
"18.29.38",
|
|
||||||
"18.33.40",
|
|
||||||
"18.38.44",
|
|
||||||
"18.48.39",
|
|
||||||
"19.05.36",
|
"19.05.36",
|
||||||
"19.16.39",
|
"19.16.39",
|
||||||
"19.44.39"
|
"19.43.41",
|
||||||
|
"19.44.39",
|
||||||
|
"19.47.53"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"options": []
|
"options": []
|
||||||
@ -189,7 +187,7 @@ Example:
|
|||||||
"7.16.53",
|
"7.16.53",
|
||||||
"7.25.53",
|
"7.25.53",
|
||||||
"8.05.51",
|
"8.05.51",
|
||||||
"8.10.51"
|
"8.12.53"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"options": []
|
"options": []
|
||||||
@ -201,7 +199,8 @@ Example:
|
|||||||
"compatiblePackages": {
|
"compatiblePackages": {
|
||||||
"com.reddit.frontpage": [
|
"com.reddit.frontpage": [
|
||||||
"2024.17.0",
|
"2024.17.0",
|
||||||
"2025.05.1"
|
"2025.05.1",
|
||||||
|
"2025.12.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"options": []
|
"options": []
|
||||||
|
@ -26,6 +26,7 @@ android {
|
|||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(libs.annotation)
|
compileOnly(libs.annotation)
|
||||||
compileOnly(libs.preference)
|
compileOnly(libs.preference)
|
||||||
|
implementation(libs.collections4)
|
||||||
implementation(libs.lang3)
|
implementation(libs.lang3)
|
||||||
|
|
||||||
compileOnly(project(":extensions:shared:stub"))
|
compileOnly(project(":extensions:shared:stub"))
|
||||||
|
@ -108,13 +108,13 @@ public class FlyoutPatch {
|
|||||||
if (REPLACE_FLYOUT_MENU_DISMISS_QUEUE.get() &&
|
if (REPLACE_FLYOUT_MENU_DISMISS_QUEUE.get() &&
|
||||||
textView.getParent() instanceof ViewGroup clickAbleArea) {
|
textView.getParent() instanceof ViewGroup clickAbleArea) {
|
||||||
runOnMainThreadDelayed(() -> {
|
runOnMainThreadDelayed(() -> {
|
||||||
textView.setText(str("revanced_replace_flyout_menu_dismiss_queue_watch_on_youtube_label"));
|
textView.setText(str("revanced_replace_flyout_menu_dismiss_queue_watch_on_youtube_label"));
|
||||||
imageView.setImageResource(getIdentifier("yt_outline_youtube_logo_icon_vd_theme_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
|
imageView.setImageResource(getIdentifier("yt_outline_youtube_logo_icon_vd_theme_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
|
||||||
clickAbleArea.setOnClickListener(view -> {
|
clickAbleArea.setOnClickListener(view -> {
|
||||||
clickView(touchOutSideViewRef.get());
|
clickView(touchOutSideViewRef.get());
|
||||||
VideoUtils.openInYouTube();
|
VideoUtils.openInYouTube();
|
||||||
});
|
});
|
||||||
}, 0L
|
}, 0L
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,14 +126,14 @@ public class FlyoutPatch {
|
|||||||
textView.getParent() instanceof ViewGroup clickAbleArea
|
textView.getParent() instanceof ViewGroup clickAbleArea
|
||||||
) {
|
) {
|
||||||
runOnMainThreadDelayed(() -> {
|
runOnMainThreadDelayed(() -> {
|
||||||
textView.setText(str("playback_rate_title"));
|
textView.setText(str("playback_rate_title"));
|
||||||
imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
|
imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
|
||||||
imageView.setColorFilter(cf);
|
imageView.setColorFilter(cf);
|
||||||
clickAbleArea.setOnClickListener(view -> {
|
clickAbleArea.setOnClickListener(view -> {
|
||||||
clickView(touchOutSideViewRef.get());
|
clickView(touchOutSideViewRef.get());
|
||||||
VideoUtils.showPlaybackSpeedFlyoutMenu();
|
VideoUtils.showPlaybackSpeedFlyoutMenu();
|
||||||
});
|
});
|
||||||
}, 0L
|
}, 0L
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,13 +9,11 @@ import android.view.Window;
|
|||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
|
||||||
import app.revanced.extension.music.settings.Settings;
|
import app.revanced.extension.music.settings.Settings;
|
||||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||||
|
|
||||||
/**
|
|
||||||
* @noinspection ALL
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class GeneralPatch {
|
public class GeneralPatch {
|
||||||
|
|
||||||
@ -79,6 +77,13 @@ public class GeneralPatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void hideSearchButton(View view) {
|
||||||
|
if (Settings.HIDE_SEARCH_BUTTON.get()) {
|
||||||
|
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0);
|
||||||
|
view.setLayoutParams(layoutParams);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean hideSoundSearchButton() {
|
public static boolean hideSoundSearchButton() {
|
||||||
return Settings.HIDE_SOUND_SEARCH_BUTTON.get();
|
return Settings.HIDE_SOUND_SEARCH_BUTTON.get();
|
||||||
}
|
}
|
||||||
@ -123,7 +128,7 @@ public class GeneralPatch {
|
|||||||
* <p>
|
* <p>
|
||||||
* The {@link AlertDialog#getButton(int)} method must be used after {@link AlertDialog#show()} is called.
|
* The {@link AlertDialog#getButton(int)} method must be used after {@link AlertDialog#show()} is called.
|
||||||
* Otherwise {@link AlertDialog#getButton(int)} method will always return null.
|
* Otherwise {@link AlertDialog#getButton(int)} method will always return null.
|
||||||
* https://stackoverflow.com/a/4604145
|
* <a href="https://stackoverflow.com/a/4604145">Reference</a>
|
||||||
* <p>
|
* <p>
|
||||||
* That's why {@link AlertDialog#show()} is absolutely necessary.
|
* That's why {@link AlertDialog#show()} is absolutely necessary.
|
||||||
* Instead, use two tricks to hide Alertdialog.
|
* Instead, use two tricks to hide Alertdialog.
|
||||||
|
@ -7,15 +7,12 @@ import androidx.annotation.NonNull;
|
|||||||
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
|
|
||||||
import app.revanced.extension.music.patches.misc.requests.PlaylistRequest;
|
import app.revanced.extension.music.patches.misc.requests.PlaylistRequest;
|
||||||
import app.revanced.extension.music.settings.Settings;
|
import app.revanced.extension.music.settings.Settings;
|
||||||
import app.revanced.extension.music.shared.VideoInformation;
|
import app.revanced.extension.music.shared.VideoInformation;
|
||||||
import app.revanced.extension.music.utils.VideoUtils;
|
import app.revanced.extension.music.utils.VideoUtils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class AlbumMusicVideoPatch {
|
public class AlbumMusicVideoPatch {
|
||||||
@ -40,7 +37,7 @@ public class AlbumMusicVideoPatch {
|
|||||||
|
|
||||||
private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK";
|
private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK";
|
||||||
|
|
||||||
private static final AtomicBoolean isVideoLaunched = new AtomicBoolean(false);
|
private static volatile boolean isVideoLaunched = false;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private static volatile String playerResponseVideoId = "";
|
private static volatile String playerResponseVideoId = "";
|
||||||
@ -100,14 +97,6 @@ public class AlbumMusicVideoPatch {
|
|||||||
if (request == null) {
|
if (request == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// This hook is always called off the main thread,
|
|
||||||
// but this can later be called for the same video id from the main thread.
|
|
||||||
// This is not a concern, since the fetch will always be finished
|
|
||||||
// and never block the main thread.
|
|
||||||
// But if debugging, then still verify this is the situation.
|
|
||||||
if (BaseSettings.ENABLE_DEBUG_LOGGING.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
|
|
||||||
Logger.printException(() -> "Error: Blocking main thread");
|
|
||||||
}
|
|
||||||
String songId = request.getStream();
|
String songId = request.getStream();
|
||||||
if (songId.isEmpty()) {
|
if (songId.isEmpty()) {
|
||||||
Logger.printDebug(() -> "Official song not found, videoId: " + videoId);
|
Logger.printDebug(() -> "Official song not found, videoId: " + videoId);
|
||||||
@ -149,17 +138,16 @@ public class AlbumMusicVideoPatch {
|
|||||||
|
|
||||||
private static void openMusic(@NonNull String songId) {
|
private static void openMusic(@NonNull String songId) {
|
||||||
try {
|
try {
|
||||||
isVideoLaunched.compareAndSet(false, true);
|
|
||||||
|
|
||||||
// The newly opened video is not a music video.
|
// The newly opened video is not a music video.
|
||||||
// To prevent fetch requests from being sent, set the video id to the newly opened video
|
// To prevent fetch requests from being sent, set the video id to the newly opened video
|
||||||
VideoUtils.runOnMainThreadDelayed(() -> {
|
VideoUtils.runOnMainThreadDelayed(() -> {
|
||||||
|
isVideoLaunched = true;
|
||||||
playerResponseVideoId = songId;
|
playerResponseVideoId = songId;
|
||||||
currentVideoId = songId;
|
currentVideoId = songId;
|
||||||
VideoUtils.openInYouTubeMusic(songId);
|
VideoUtils.openInYouTubeMusic(songId);
|
||||||
}, 1000);
|
VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched = false, 3000);
|
||||||
|
}, 1500);
|
||||||
|
|
||||||
VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 2500);
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "openMusic failure", ex);
|
Logger.printException(() -> "openMusic failure", ex);
|
||||||
}
|
}
|
||||||
@ -191,7 +179,7 @@ public class AlbumMusicVideoPatch {
|
|||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static boolean hideSnackBar() {
|
public static boolean hideSnackBar() {
|
||||||
return DISABLE_MUSIC_VIDEO_IN_ALBUM && isVideoLaunched.get();
|
return DISABLE_MUSIC_VIDEO_IN_ALBUM && isVideoLaunched;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import androidx.annotation.Nullable;
|
|||||||
|
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
@ -2,8 +2,10 @@ package app.revanced.extension.music.patches.misc.requests
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.annotation.GuardedBy
|
import androidx.annotation.GuardedBy
|
||||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLIST_PAGE
|
||||||
import app.revanced.extension.shared.requests.Requester
|
import app.revanced.extension.shared.requests.Requester
|
||||||
import app.revanced.extension.shared.settings.AppLanguage
|
import app.revanced.extension.shared.settings.AppLanguage
|
||||||
import app.revanced.extension.shared.utils.Logger
|
import app.revanced.extension.shared.utils.Logger
|
||||||
@ -136,10 +138,11 @@ class PlaylistRequest private constructor(
|
|||||||
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
|
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
|
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||||
PlayerRoutes.GET_PLAYLIST_PAGE,
|
GET_PLAYLIST_PAGE,
|
||||||
clientType
|
clientType
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For some reason, the tracks in Top Songs have the playlistId of the album:
|
* 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)
|
* [ReVanced_Extended#2835](https://github.com/inotia00/ReVanced_Extended/issues/2835)
|
||||||
@ -152,7 +155,7 @@ class PlaylistRequest private constructor(
|
|||||||
* So we can work around this by setting the language to English when sending the request.
|
* So we can work around this by setting the language to English when sending the request.
|
||||||
*/
|
*/
|
||||||
val requestBody =
|
val requestBody =
|
||||||
PlayerRoutes.createApplicationRequestBody(
|
createApplicationRequestBody(
|
||||||
clientType = clientType,
|
clientType = clientType,
|
||||||
videoId = videoId,
|
videoId = videoId,
|
||||||
playlistId = playlistId,
|
playlistId = playlistId,
|
||||||
|
@ -3,6 +3,7 @@ package app.revanced.extension.music.settings;
|
|||||||
import static java.lang.Boolean.FALSE;
|
import static java.lang.Boolean.FALSE;
|
||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
import static app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
import static app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||||
|
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
@ -109,6 +110,7 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_NOTIFICATION_BUTTON = new BooleanSetting("revanced_hide_notification_button", FALSE, true);
|
public static final BooleanSetting HIDE_NOTIFICATION_BUTTON = new BooleanSetting("revanced_hide_notification_button", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_PLAYLIST_CARD_SHELF = new BooleanSetting("revanced_hide_playlist_card_shelf", FALSE, true);
|
public static final BooleanSetting HIDE_PLAYLIST_CARD_SHELF = new BooleanSetting("revanced_hide_playlist_card_shelf", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_SAMPLE_SHELF = new BooleanSetting("revanced_hide_samples_shelf", FALSE, true);
|
public static final BooleanSetting HIDE_SAMPLE_SHELF = new BooleanSetting("revanced_hide_samples_shelf", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_search_button", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_SOUND_SEARCH_BUTTON = new BooleanSetting("revanced_hide_sound_search_button", FALSE, true);
|
public static final BooleanSetting HIDE_SOUND_SEARCH_BUTTON = new BooleanSetting("revanced_hide_sound_search_button", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_TAP_TO_UPDATE_BUTTON = new BooleanSetting("revanced_hide_tap_to_update_button", FALSE, true);
|
public static final BooleanSetting HIDE_TAP_TO_UPDATE_BUTTON = new BooleanSetting("revanced_hide_tap_to_update_button", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_VOICE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_voice_search_button", FALSE, true);
|
public static final BooleanSetting HIDE_VOICE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_voice_search_button", FALSE, true);
|
||||||
@ -240,7 +242,10 @@ public class Settings extends BaseSettings {
|
|||||||
// region Migration
|
// region Migration
|
||||||
|
|
||||||
// Old spoof versions that no longer work reliably.
|
// Old spoof versions that no longer work reliably.
|
||||||
if (SPOOF_APP_VERSION_TARGET.get().compareTo(SPOOF_APP_VERSION_TARGET.defaultValue) < 0) {
|
String spoofAppVersionTarget = SPOOF_APP_VERSION_TARGET.get();
|
||||||
|
if (spoofAppVersionTarget.compareTo(SPOOF_APP_VERSION_TARGET.defaultValue) < 0) {
|
||||||
|
Utils.showToastShort(str("revanced_spoof_app_version_target_invalid_toast", spoofAppVersionTarget));
|
||||||
|
Utils.showToastShort(str("revanced_extended_reset_to_default_toast"));
|
||||||
Logger.printInfo(() -> "Resetting spoof app version target");
|
Logger.printInfo(() -> "Resetting spoof app version target");
|
||||||
SPOOF_APP_VERSION_TARGET.resetToDefault();
|
SPOOF_APP_VERSION_TARGET.resetToDefault();
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_NEW_POST_ADS = new BooleanSetting("revanced_hide_new_post_ads", TRUE, true);
|
public static final BooleanSetting HIDE_NEW_POST_ADS = new BooleanSetting("revanced_hide_new_post_ads", TRUE, true);
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
public static final BooleanSetting DISABLE_SCREENSHOT_POPUP = new BooleanSetting("revanced_disable_screenshot_popup", TRUE);
|
public static final BooleanSetting DISABLE_SCREENSHOT_POPUP = new BooleanSetting("revanced_disable_screenshot_popup", TRUE, true);
|
||||||
public static final BooleanSetting HIDE_CHAT_BUTTON = new BooleanSetting("revanced_hide_chat_button", FALSE, true);
|
public static final BooleanSetting HIDE_CHAT_BUTTON = new BooleanSetting("revanced_hide_chat_button", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", FALSE, true);
|
public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_DISCOVER_BUTTON = new BooleanSetting("revanced_hide_discover_button", FALSE, true);
|
public static final BooleanSetting HIDE_DISCOVER_BUTTON = new BooleanSetting("revanced_hide_discover_button", FALSE, true);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.shared.patches.client
|
package app.revanced.extension.shared.innertube.client
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import app.revanced.extension.shared.patches.PatchStatus
|
||||||
import app.revanced.extension.shared.settings.BaseSettings
|
import app.revanced.extension.shared.settings.BaseSettings
|
||||||
import app.revanced.extension.shared.utils.PackageUtils
|
import app.revanced.extension.shared.utils.PackageUtils
|
||||||
import org.apache.commons.lang3.ArrayUtils
|
import org.apache.commons.lang3.ArrayUtils
|
||||||
@ -212,8 +213,15 @@ object YouTubeAppClient {
|
|||||||
return BaseSettings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get()
|
return BaseSettings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun useIOS(): Boolean {
|
||||||
|
return PatchStatus.SpoofStreamingDataIOS() && BaseSettings.SPOOF_STREAMING_DATA_TYPE_IOS.get()
|
||||||
|
}
|
||||||
|
|
||||||
fun availableClientTypes(preferredClient: ClientType): Array<ClientType> {
|
fun availableClientTypes(preferredClient: ClientType): Array<ClientType> {
|
||||||
val availableClientTypes = ClientType.CLIENT_ORDER_TO_USE_YOUTUBE
|
val availableClientTypes = if (useIOS())
|
||||||
|
ClientType.CLIENT_ORDER_TO_USE_IOS
|
||||||
|
else
|
||||||
|
ClientType.CLIENT_ORDER_TO_USE
|
||||||
|
|
||||||
if (ArrayUtils.contains(availableClientTypes, preferredClient)) {
|
if (ArrayUtils.contains(availableClientTypes, preferredClient)) {
|
||||||
val clientToUse: Array<ClientType?> = arrayOfNulls(availableClientTypes.size)
|
val clientToUse: Array<ClientType?> = arrayOfNulls(availableClientTypes.size)
|
||||||
@ -230,7 +238,7 @@ object YouTubeAppClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION", "unused")
|
||||||
enum class ClientType(
|
enum class ClientType(
|
||||||
/**
|
/**
|
||||||
* [YouTube client type](https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients)
|
* [YouTube client type](https://github.com/zerodytrash/YouTube-Internal-Clients?tab=readme-ov-file#clients)
|
||||||
@ -278,10 +286,6 @@ object YouTubeAppClient {
|
|||||||
* If true, 'Authorization' must be included.
|
* If true, 'Authorization' must be included.
|
||||||
*/
|
*/
|
||||||
val requireAuth: Boolean = false,
|
val requireAuth: Boolean = false,
|
||||||
/**
|
|
||||||
* Whether a poToken is required to get playback for more than 1 minute.
|
|
||||||
*/
|
|
||||||
val requirePoToken: Boolean = false,
|
|
||||||
/**
|
/**
|
||||||
* Client name for innertube body.
|
* Client name for innertube body.
|
||||||
*/
|
*/
|
||||||
@ -363,7 +367,7 @@ object YouTubeAppClient {
|
|||||||
else
|
else
|
||||||
"iOS TV"
|
"iOS TV"
|
||||||
),
|
),
|
||||||
IOS(
|
IOS_DEPRECATED(
|
||||||
id = 5,
|
id = 5,
|
||||||
deviceMake = DEVICE_MAKE_IOS,
|
deviceMake = DEVICE_MAKE_IOS,
|
||||||
deviceModel = DEVICE_MODEL_IOS,
|
deviceModel = DEVICE_MODEL_IOS,
|
||||||
@ -372,7 +376,6 @@ object YouTubeAppClient {
|
|||||||
userAgent = USER_AGENT_IOS,
|
userAgent = USER_AGENT_IOS,
|
||||||
clientVersion = CLIENT_VERSION_IOS,
|
clientVersion = CLIENT_VERSION_IOS,
|
||||||
supportsCookies = false,
|
supportsCookies = false,
|
||||||
requirePoToken = true,
|
|
||||||
clientName = "IOS",
|
clientName = "IOS",
|
||||||
friendlyName = if (forceAVC())
|
friendlyName = if (forceAVC())
|
||||||
"iOS Force AVC"
|
"iOS Force AVC"
|
||||||
@ -381,12 +384,20 @@ object YouTubeAppClient {
|
|||||||
);
|
);
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val CLIENT_ORDER_TO_USE_YOUTUBE: Array<ClientType> = arrayOf(
|
val CLIENT_ORDER_TO_USE: Array<ClientType> = arrayOf(
|
||||||
ANDROID_VR_NO_AUTH,
|
ANDROID_VR_NO_AUTH,
|
||||||
ANDROID_UNPLUGGED,
|
ANDROID_UNPLUGGED,
|
||||||
ANDROID_CREATOR,
|
ANDROID_CREATOR,
|
||||||
IOS_UNPLUGGED,
|
IOS_UNPLUGGED,
|
||||||
IOS,
|
ANDROID_VR,
|
||||||
|
)
|
||||||
|
|
||||||
|
val CLIENT_ORDER_TO_USE_IOS: Array<ClientType> = arrayOf(
|
||||||
|
ANDROID_VR_NO_AUTH,
|
||||||
|
ANDROID_UNPLUGGED,
|
||||||
|
ANDROID_CREATOR,
|
||||||
|
IOS_UNPLUGGED,
|
||||||
|
IOS_DEPRECATED,
|
||||||
ANDROID_VR,
|
ANDROID_VR,
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
package app.revanced.extension.shared.patches.client;
|
package app.revanced.extension.shared.innertube.client;
|
||||||
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
public class MusicAppClient {
|
public class YouTubeMusicAppClient {
|
||||||
|
|
||||||
// Response to the '/next' request is 'Please update to continue using the app':
|
// Response to the '/next' request is 'Please update to continue using the app':
|
||||||
// https://github.com/inotia00/ReVanced_Extended/issues/2743
|
// https://github.com/inotia00/ReVanced_Extended/issues/2743
|
||||||
@ -46,7 +46,7 @@ public class MusicAppClient {
|
|||||||
private static final String DEVICE_MAKE_IOS_MUSIC = "Apple";
|
private static final String DEVICE_MAKE_IOS_MUSIC = "Apple";
|
||||||
private static final String OS_NAME_IOS_MUSIC = "iOS";
|
private static final String OS_NAME_IOS_MUSIC = "iOS";
|
||||||
|
|
||||||
private MusicAppClient() {
|
private YouTubeMusicAppClient() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String androidUserAgent(String clientVersion) {
|
private static String androidUserAgent(String clientVersion) {
|
@ -1,14 +1,10 @@
|
|||||||
package app.revanced.extension.shared.patches.client
|
package app.revanced.extension.shared.innertube.client
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to fetch video information.
|
* Used to fetch video information.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
object YouTubeWebClient {
|
object YouTubeWebClient {
|
||||||
/**
|
|
||||||
* This user agent does not require a PoToken in [ClientType.MWEB]
|
|
||||||
* https://github.com/yt-dlp/yt-dlp/blob/0b6b7742c2e7f2a1fcb0b54ef3dd484bab404b3f/yt_dlp/extractor/youtube.py#L259
|
|
||||||
*/
|
|
||||||
private const val USER_AGENT_SAFARI =
|
private const val USER_AGENT_SAFARI =
|
||||||
"Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)"
|
"Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)"
|
||||||
|
|
||||||
@ -26,11 +22,11 @@ object YouTubeWebClient {
|
|||||||
* Client version.
|
* Client version.
|
||||||
*/
|
*/
|
||||||
@JvmField
|
@JvmField
|
||||||
val clientVersion: String
|
val clientVersion: String,
|
||||||
) {
|
) {
|
||||||
MWEB(
|
MWEB(
|
||||||
id = 2,
|
id = 2,
|
||||||
clientVersion = "2.20241202.07.00"
|
clientVersion = "2.20241202.07.00",
|
||||||
),
|
),
|
||||||
WEB_REMIX(
|
WEB_REMIX(
|
||||||
id = 29,
|
id = 29,
|
@ -0,0 +1,364 @@
|
|||||||
|
package app.revanced.extension.shared.innertube.requests
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||||
|
import app.revanced.extension.shared.innertube.client.YouTubeWebClient
|
||||||
|
import app.revanced.extension.shared.requests.Requester
|
||||||
|
import app.revanced.extension.shared.requests.Route.CompiledRoute
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings
|
||||||
|
import app.revanced.extension.shared.utils.Logger
|
||||||
|
import app.revanced.extension.shared.utils.StringRef.str
|
||||||
|
import app.revanced.extension.shared.utils.Utils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.json.JSONArray
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
@Suppress("deprecation")
|
||||||
|
object InnerTubeRequestBody {
|
||||||
|
|
||||||
|
private const val YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"
|
||||||
|
|
||||||
|
private const val AUTHORIZATION_HEADER = "Authorization"
|
||||||
|
private val REQUEST_HEADER_KEYS = setOf(
|
||||||
|
AUTHORIZATION_HEADER, // Available only to logged-in users.
|
||||||
|
"X-GOOG-API-FORMAT-VERSION",
|
||||||
|
"X-Goog-Visitor-Id"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TCP connection and HTTP read timeout
|
||||||
|
*/
|
||||||
|
private const val CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000 // 10 Seconds.
|
||||||
|
|
||||||
|
private val LOCALE: Locale = Utils.getContext().resources
|
||||||
|
.configuration.locale
|
||||||
|
private val LOCALE_COUNTRY: String = LOCALE.country
|
||||||
|
private val LOCALE_LANGUAGE: String = LOCALE.language
|
||||||
|
private val TIME_ZONE: TimeZone = TimeZone.getDefault()
|
||||||
|
private val TIME_ZONE_ID: String = TIME_ZONE.id
|
||||||
|
private val UTC_OFFSET_MINUTES: Int = TIME_ZONE.getOffset(Date().time) / 60000
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun createApplicationRequestBody(
|
||||||
|
clientType: YouTubeAppClient.ClientType,
|
||||||
|
videoId: String,
|
||||||
|
playlistId: String? = null,
|
||||||
|
botGuardPoToken: String = "",
|
||||||
|
visitorId: String = "",
|
||||||
|
setLocale: Boolean = false,
|
||||||
|
language: String = BaseSettings.SPOOF_STREAMING_DATA_LANGUAGE.get().language,
|
||||||
|
): ByteArray {
|
||||||
|
val innerTubeBody = JSONObject()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val client = JSONObject()
|
||||||
|
client.put("deviceMake", clientType.deviceMake)
|
||||||
|
client.put("deviceModel", clientType.deviceModel)
|
||||||
|
client.put("clientName", clientType.clientName)
|
||||||
|
client.put("clientVersion", clientType.clientVersion)
|
||||||
|
client.put("osName", clientType.osName)
|
||||||
|
client.put("osVersion", clientType.osVersion)
|
||||||
|
if (clientType.androidSdkVersion != null) {
|
||||||
|
client.put("androidSdkVersion", clientType.androidSdkVersion)
|
||||||
|
if (clientType.gmscoreVersionCode != null) {
|
||||||
|
client.put("gmscoreVersionCode", clientType.gmscoreVersionCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.put(
|
||||||
|
"hl",
|
||||||
|
if (setLocale) {
|
||||||
|
language
|
||||||
|
} else {
|
||||||
|
LOCALE_LANGUAGE
|
||||||
|
}
|
||||||
|
)
|
||||||
|
client.put("gl", LOCALE_COUNTRY)
|
||||||
|
client.put("timeZone", TIME_ZONE_ID)
|
||||||
|
client.put("utcOffsetMinutes", "$UTC_OFFSET_MINUTES")
|
||||||
|
|
||||||
|
val context = JSONObject()
|
||||||
|
context.put("client", client)
|
||||||
|
|
||||||
|
innerTubeBody.put("context", context)
|
||||||
|
innerTubeBody.put("contentCheckOk", true)
|
||||||
|
innerTubeBody.put("racyCheckOk", true)
|
||||||
|
innerTubeBody.put("videoId", videoId)
|
||||||
|
|
||||||
|
if (playlistId != null) {
|
||||||
|
innerTubeBody.put("playlistId", playlistId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StringUtils.isAnyEmpty(botGuardPoToken, visitorId)) {
|
||||||
|
val serviceIntegrityDimensions = JSONObject()
|
||||||
|
serviceIntegrityDimensions.put("poToken", botGuardPoToken)
|
||||||
|
innerTubeBody.put("serviceIntegrityDimensions", serviceIntegrityDimensions)
|
||||||
|
}
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Logger.printException({ "Failed to create application innerTubeBody" }, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun createWebInnertubeBody(
|
||||||
|
clientType: YouTubeWebClient.ClientType,
|
||||||
|
videoId: String
|
||||||
|
): ByteArray {
|
||||||
|
val innerTubeBody = JSONObject()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val client = JSONObject()
|
||||||
|
client.put("clientName", clientType.clientName)
|
||||||
|
client.put("clientVersion", clientType.clientVersion)
|
||||||
|
val context = JSONObject()
|
||||||
|
context.put("client", client)
|
||||||
|
|
||||||
|
val lockedSafetyMode = JSONObject()
|
||||||
|
lockedSafetyMode.put("lockedSafetyMode", false)
|
||||||
|
val user = JSONObject()
|
||||||
|
user.put("user", lockedSafetyMode)
|
||||||
|
|
||||||
|
innerTubeBody.put("context", context)
|
||||||
|
innerTubeBody.put("contentCheckOk", true)
|
||||||
|
innerTubeBody.put("racyCheckOk", true)
|
||||||
|
innerTubeBody.put("videoId", videoId)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Logger.printException({ "Failed to create web innerTubeBody" }, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun androidInnerTubeBody(
|
||||||
|
clientType: YouTubeAppClient.ClientType = YouTubeAppClient.ClientType.ANDROID
|
||||||
|
): JSONObject {
|
||||||
|
val innerTubeBody = JSONObject()
|
||||||
|
|
||||||
|
try {
|
||||||
|
val client = JSONObject()
|
||||||
|
client.put("deviceMake", clientType.deviceMake)
|
||||||
|
client.put("deviceModel", clientType.deviceModel)
|
||||||
|
client.put("clientName", clientType.clientName)
|
||||||
|
client.put("clientVersion", clientType.clientVersion)
|
||||||
|
client.put("osName", clientType.osName)
|
||||||
|
client.put("osVersion", clientType.osVersion)
|
||||||
|
client.put("androidSdkVersion", clientType.androidSdkVersion)
|
||||||
|
client.put("hl", LOCALE_LANGUAGE)
|
||||||
|
client.put("gl", LOCALE_COUNTRY)
|
||||||
|
client.put("timeZone", TIME_ZONE_ID)
|
||||||
|
client.put("utcOffsetMinutes", UTC_OFFSET_MINUTES.toString())
|
||||||
|
|
||||||
|
val context = JSONObject()
|
||||||
|
context.put("client", client)
|
||||||
|
|
||||||
|
innerTubeBody.put("context", context)
|
||||||
|
innerTubeBody.put("contentCheckOk", true)
|
||||||
|
innerTubeBody.put("racyCheckOk", true)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Logger.printException({ "Failed to create android innerTubeBody" }, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerTubeBody
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun createPlaylistRequestBody(
|
||||||
|
videoId: String,
|
||||||
|
): ByteArray {
|
||||||
|
val innerTubeBody = androidInnerTubeBody()
|
||||||
|
|
||||||
|
try {
|
||||||
|
innerTubeBody.put("params", "CAQ%3D")
|
||||||
|
// TODO: Implement an AlertDialog that allows changing the title of the playlist.
|
||||||
|
innerTubeBody.put("title", str("revanced_queue_manager_queue"))
|
||||||
|
|
||||||
|
val videoIds = JSONArray()
|
||||||
|
videoIds.put(0, videoId)
|
||||||
|
innerTubeBody.put("videoIds", videoIds)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Logger.printException({ "Failed to create create/playlist innerTubeBody" }, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun deletePlaylistRequestBody(
|
||||||
|
playlistId: String,
|
||||||
|
): ByteArray {
|
||||||
|
val innerTubeBody = androidInnerTubeBody()
|
||||||
|
|
||||||
|
try {
|
||||||
|
innerTubeBody.put("playlistId", playlistId)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Logger.printException({ "Failed to create delete/playlist innerTubeBody" }, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun editPlaylistRequestBody(
|
||||||
|
videoId: String,
|
||||||
|
playlistId: String,
|
||||||
|
setVideoId: String?,
|
||||||
|
): ByteArray {
|
||||||
|
val innerTubeBody = androidInnerTubeBody()
|
||||||
|
|
||||||
|
try {
|
||||||
|
innerTubeBody.put("playlistId", playlistId)
|
||||||
|
|
||||||
|
val actionsObject = JSONObject()
|
||||||
|
if (setVideoId != null && setVideoId.isNotEmpty()) {
|
||||||
|
actionsObject.put("action", "ACTION_REMOVE_VIDEO")
|
||||||
|
actionsObject.put("setVideoId", setVideoId)
|
||||||
|
} else {
|
||||||
|
actionsObject.put("action", "ACTION_ADD_VIDEO")
|
||||||
|
actionsObject.put("addedVideoId", videoId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val actionsArray = JSONArray()
|
||||||
|
actionsArray.put(0, actionsObject)
|
||||||
|
innerTubeBody.put("actions", actionsArray)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Logger.printException({ "Failed to create edit/playlist innerTubeBody" }, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getPlaylistsRequestBody(
|
||||||
|
playlistId: String,
|
||||||
|
): ByteArray {
|
||||||
|
val innerTubeBody = androidInnerTubeBody()
|
||||||
|
|
||||||
|
try {
|
||||||
|
innerTubeBody.put("playlistId", playlistId)
|
||||||
|
innerTubeBody.put("excludeWatchLater", false)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Logger.printException({ "Failed to create get/playlists innerTubeBody" }, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun savePlaylistRequestBody(
|
||||||
|
playlistId: String,
|
||||||
|
libraryId: String,
|
||||||
|
): ByteArray {
|
||||||
|
val innerTubeBody = androidInnerTubeBody()
|
||||||
|
|
||||||
|
try {
|
||||||
|
innerTubeBody.put("playlistId", playlistId)
|
||||||
|
|
||||||
|
val actionsObject = JSONObject()
|
||||||
|
actionsObject.put("action", "ACTION_ADD_PLAYLIST")
|
||||||
|
actionsObject.put("addedFullListId", libraryId)
|
||||||
|
|
||||||
|
val actionsArray = JSONArray()
|
||||||
|
actionsArray.put(0, actionsObject)
|
||||||
|
innerTubeBody.put("actions", actionsArray)
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
Logger.printException({ "Failed to create save/playlist innerTubeBody" }, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getInnerTubeResponseConnectionFromRoute(
|
||||||
|
route: CompiledRoute,
|
||||||
|
clientType: YouTubeAppClient.ClientType,
|
||||||
|
requestHeader: Map<String, String>? = null,
|
||||||
|
dataSyncId: String? = null,
|
||||||
|
connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
|
||||||
|
readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
|
||||||
|
) = getInnerTubeResponseConnectionFromRoute(
|
||||||
|
route = route,
|
||||||
|
userAgent = clientType.userAgent,
|
||||||
|
clientId = clientType.id.toString(),
|
||||||
|
clientVersion = clientType.clientVersion,
|
||||||
|
supportsCookies = clientType.supportsCookies,
|
||||||
|
requestHeader = requestHeader,
|
||||||
|
dataSyncId = dataSyncId,
|
||||||
|
connectTimeout = connectTimeout,
|
||||||
|
readTimeout = readTimeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getInnerTubeResponseConnectionFromRoute(
|
||||||
|
route: CompiledRoute,
|
||||||
|
clientType: YouTubeWebClient.ClientType,
|
||||||
|
requestHeader: Map<String, String>? = null,
|
||||||
|
dataSyncId: String? = null,
|
||||||
|
connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
|
||||||
|
readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
|
||||||
|
) = getInnerTubeResponseConnectionFromRoute(
|
||||||
|
route = route,
|
||||||
|
userAgent = clientType.userAgent,
|
||||||
|
clientId = clientType.id.toString(),
|
||||||
|
clientVersion = clientType.clientVersion,
|
||||||
|
requestHeader = requestHeader,
|
||||||
|
dataSyncId = dataSyncId,
|
||||||
|
connectTimeout = connectTimeout,
|
||||||
|
readTimeout = readTimeout,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun getInnerTubeResponseConnectionFromRoute(
|
||||||
|
route: CompiledRoute,
|
||||||
|
userAgent: String,
|
||||||
|
clientId: String,
|
||||||
|
clientVersion: String,
|
||||||
|
supportsCookies: Boolean = true,
|
||||||
|
requestHeader: Map<String, String>? = null,
|
||||||
|
dataSyncId: String? = null,
|
||||||
|
connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
|
||||||
|
readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
|
||||||
|
): HttpURLConnection {
|
||||||
|
val connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route)
|
||||||
|
|
||||||
|
connection.setRequestProperty("Content-Type", "application/json")
|
||||||
|
connection.setRequestProperty("User-Agent", userAgent)
|
||||||
|
connection.setRequestProperty("X-YouTube-Client-Name", clientId)
|
||||||
|
connection.setRequestProperty("X-YouTube-Client-Version", clientVersion)
|
||||||
|
|
||||||
|
connection.useCaches = false
|
||||||
|
connection.doOutput = true
|
||||||
|
|
||||||
|
connection.connectTimeout = connectTimeout
|
||||||
|
connection.readTimeout = readTimeout
|
||||||
|
|
||||||
|
if (requestHeader != null) {
|
||||||
|
for (key in REQUEST_HEADER_KEYS) {
|
||||||
|
var value = requestHeader[key]
|
||||||
|
if (value != null) {
|
||||||
|
if (key == AUTHORIZATION_HEADER) {
|
||||||
|
if (!supportsCookies) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connection.setRequestProperty(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to identify brand accounts
|
||||||
|
if (dataSyncId != null && dataSyncId.isNotEmpty()) {
|
||||||
|
connection.setRequestProperty("X-Goog-PageId", dataSyncId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return connection
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
package app.revanced.extension.shared.innertube.requests
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.requests.Route
|
||||||
|
import app.revanced.extension.shared.requests.Route.CompiledRoute
|
||||||
|
|
||||||
|
object InnerTubeRoutes {
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val CREATE_PLAYLIST = compileRoute(
|
||||||
|
endpoint = "playlist/create",
|
||||||
|
fields = "playlistId",
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val DELETE_PLAYLIST = compileRoute(
|
||||||
|
endpoint = "playlist/delete",
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val EDIT_PLAYLIST = compileRoute(
|
||||||
|
endpoint = "browse/edit_playlist",
|
||||||
|
fields = "status," + "playlistEditResults",
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val GET_CATEGORY = compileRoute(
|
||||||
|
endpoint = "player",
|
||||||
|
fields = "microformat.playerMicroformatRenderer.category",
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val GET_PLAYLISTS = compileRoute(
|
||||||
|
endpoint = "playlist/get_add_to_playlist",
|
||||||
|
fields = "contents.addToPlaylistRenderer.playlists.playlistAddToOptionRenderer",
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val GET_SET_VIDEO_ID = compileRoute(
|
||||||
|
endpoint = "next",
|
||||||
|
fields = "contents.singleColumnWatchNextResults." +
|
||||||
|
"playlist.playlist.contents.playlistPanelVideoRenderer." +
|
||||||
|
"playlistSetVideoId",
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val GET_PLAYLIST_PAGE = compileRoute(
|
||||||
|
endpoint = "next",
|
||||||
|
fields = "contents.singleColumnWatchNextResults.playlist.playlist",
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val GET_STREAMING_DATA = compileRoute(
|
||||||
|
endpoint = "player",
|
||||||
|
fields = "streamingData",
|
||||||
|
alt = "proto",
|
||||||
|
prettier = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val GET_VIDEO_ACTION_BUTTON = compileRoute(
|
||||||
|
endpoint = "next",
|
||||||
|
fields = "contents.singleColumnWatchNextResults." +
|
||||||
|
"results.results.contents.slimVideoMetadataSectionRenderer." +
|
||||||
|
"contents.elementRenderer.newElement.type.componentType." +
|
||||||
|
"model.videoActionBarModel.buttons.buttonViewModel"
|
||||||
|
)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
val GET_VIDEO_DETAILS = compileRoute(
|
||||||
|
endpoint = "player",
|
||||||
|
fields = "videoDetails.channelId," +
|
||||||
|
"videoDetails.isLiveContent," +
|
||||||
|
"videoDetails.isUpcoming"
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun compileRoute(
|
||||||
|
endpoint: String,
|
||||||
|
fields: String? = null,
|
||||||
|
alt: String? = null,
|
||||||
|
prettier: Boolean = false,
|
||||||
|
): CompiledRoute {
|
||||||
|
var query = Array<String>(4) { "&" }
|
||||||
|
var i = 0
|
||||||
|
query[i] = "?"
|
||||||
|
|
||||||
|
val sb = StringBuilder(endpoint)
|
||||||
|
if (prettier == false) {
|
||||||
|
sb.append(query[i++])
|
||||||
|
sb.append("prettyPrint=false")
|
||||||
|
}
|
||||||
|
if (fields != null) {
|
||||||
|
sb.append(query[i++])
|
||||||
|
sb.append("fields=")
|
||||||
|
sb.append(fields)
|
||||||
|
}
|
||||||
|
if (alt != null) {
|
||||||
|
sb.append(query[i++])
|
||||||
|
sb.append("alt=")
|
||||||
|
sb.append(alt)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Route(
|
||||||
|
Route.Method.POST,
|
||||||
|
sb.toString()
|
||||||
|
).compile()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -37,7 +37,7 @@ public class FullscreenAdsPatch {
|
|||||||
* Therefore, make sure that the dialog contains the ads at the beginning of the Method
|
* Therefore, make sure that the dialog contains the ads at the beginning of the Method
|
||||||
*
|
*
|
||||||
* @param bytes proto buffer array
|
* @param bytes proto buffer array
|
||||||
* @param type dialog type (similar to {@link Enum#ordinal()})
|
* @param type dialog type (similar to {@link Enum#ordinal()})
|
||||||
*/
|
*/
|
||||||
public static void checkDialog(byte[] bytes, int type) {
|
public static void checkDialog(byte[] bytes, int type) {
|
||||||
if (!HIDE_FULLSCREEN_ADS) {
|
if (!HIDE_FULLSCREEN_ADS) {
|
||||||
|
@ -11,4 +11,8 @@ public class PatchStatus {
|
|||||||
// Replace this with true If the Spoof streaming data patch succeeds in YouTube.
|
// Replace this with true If the Spoof streaming data patch succeeds in YouTube.
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean SpoofStreamingDataIOS() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package app.revanced.extension.shared.patches;
|
|||||||
|
|
||||||
import static java.lang.Boolean.FALSE;
|
import static java.lang.Boolean.FALSE;
|
||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan;
|
import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
@ -2,8 +2,8 @@ package app.revanced.extension.shared.patches;
|
|||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class WatchHistoryPatch {
|
public final class WatchHistoryPatch {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package app.revanced.extension.shared.patches.spoof;
|
package app.revanced.extension.shared.patches.spoof;
|
||||||
|
|
||||||
import app.revanced.extension.shared.patches.client.MusicAppClient.ClientType;
|
import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient.ClientType;
|
||||||
import app.revanced.extension.music.settings.Settings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SpoofClientPatch extends BlockRequestPatch {
|
public class SpoofClientPatch extends BlockRequestPatch {
|
||||||
private static final ClientType CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get();
|
private static final ClientType CLIENT_TYPE = BaseSettings.SPOOF_CLIENT_TYPE.get();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
|
@ -10,21 +10,21 @@ import java.util.Collections;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient.ClientType;
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient.ClientType;
|
||||||
|
import app.revanced.extension.shared.patches.PatchStatus;
|
||||||
import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
|
import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
|
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||||
private static final String PO_TOKEN =
|
|
||||||
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 =
|
private static final boolean SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION =
|
||||||
SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION.get();
|
SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION.get();
|
||||||
|
private static final boolean SPOOF_STREAMING_DATA_TYPE_IOS =
|
||||||
|
PatchStatus.SpoofStreamingDataIOS() && BaseSettings.SPOOF_STREAMING_DATA_TYPE_IOS.get();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any unreachable ip address. Used to intentionally fail requests.
|
* Any unreachable ip address. Used to intentionally fail requests.
|
||||||
@ -69,17 +69,27 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
|||||||
* Skip response encryption in OnesiePlayerRequest.
|
* Skip response encryption in OnesiePlayerRequest.
|
||||||
*/
|
*/
|
||||||
public static boolean skipResponseEncryption(boolean original) {
|
public static boolean skipResponseEncryption(boolean original) {
|
||||||
if (SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) {
|
if (!SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) {
|
||||||
return false;
|
return original;
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return original;
|
/**
|
||||||
|
* Injection point.
|
||||||
|
* Turns off a feature flag that interferes with video playback.
|
||||||
|
*/
|
||||||
|
public static boolean usePlaybackStartFeatureFlag(boolean original) {
|
||||||
|
if (!SPOOF_STREAMING_DATA) {
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static void fetchStreams(String url, Map<String, String> requestHeaders) {
|
public static void fetchStreams(String url, Map<String, String> requestHeader) {
|
||||||
if (SPOOF_STREAMING_DATA) {
|
if (SPOOF_STREAMING_DATA) {
|
||||||
String id = Utils.getVideoIdFromRequest(url);
|
String id = Utils.getVideoIdFromRequest(url);
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
@ -89,7 +99,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN);
|
StreamingDataRequest.fetchRequest(id, requestHeader);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,6 +220,18 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
|||||||
return videoFormat;
|
return videoFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String[] getEntries() {
|
||||||
|
return SPOOF_STREAMING_DATA_TYPE_IOS
|
||||||
|
? ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_ios_entries")
|
||||||
|
: ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_entries");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String[] getEntryValues() {
|
||||||
|
return SPOOF_STREAMING_DATA_TYPE_IOS
|
||||||
|
? ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_ios_entry_values")
|
||||||
|
: ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_entry_values");
|
||||||
|
}
|
||||||
|
|
||||||
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
|
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
|
||||||
@Override
|
@Override
|
||||||
public boolean isAvailable() {
|
public boolean isAvailable() {
|
||||||
|
@ -1,223 +0,0 @@
|
|||||||
package app.revanced.extension.shared.patches.spoof.requests
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient
|
|
||||||
import app.revanced.extension.shared.patches.client.YouTubeWebClient
|
|
||||||
import app.revanced.extension.shared.requests.Requester
|
|
||||||
import app.revanced.extension.shared.requests.Route
|
|
||||||
import app.revanced.extension.shared.requests.Route.CompiledRoute
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings
|
|
||||||
import app.revanced.extension.shared.utils.Logger
|
|
||||||
import app.revanced.extension.shared.utils.Utils
|
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import org.json.JSONException
|
|
||||||
import org.json.JSONObject
|
|
||||||
import java.io.IOException
|
|
||||||
import java.net.HttpURLConnection
|
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
@Suppress("deprecation")
|
|
||||||
object PlayerRoutes {
|
|
||||||
@JvmField
|
|
||||||
val GET_CATEGORY: CompiledRoute = Route(
|
|
||||||
Route.Method.POST,
|
|
||||||
"player" +
|
|
||||||
"?prettyPrint=false" +
|
|
||||||
"&fields=microformat.playerMicroformatRenderer.category"
|
|
||||||
).compile()
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val GET_PLAYLIST_PAGE: CompiledRoute = Route(
|
|
||||||
Route.Method.POST,
|
|
||||||
"next" +
|
|
||||||
"?prettyPrint=false" +
|
|
||||||
"&fields=contents.singleColumnWatchNextResults.playlist.playlist"
|
|
||||||
).compile()
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val GET_STREAMING_DATA: CompiledRoute = Route(
|
|
||||||
Route.Method.POST,
|
|
||||||
"player" +
|
|
||||||
"?fields=streamingData" +
|
|
||||||
"&alt=proto"
|
|
||||||
).compile()
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val GET_VIDEO_ACTION_BUTTON: CompiledRoute = Route(
|
|
||||||
Route.Method.POST,
|
|
||||||
"next" +
|
|
||||||
"?prettyPrint=false" +
|
|
||||||
"&fields=contents.singleColumnWatchNextResults." +
|
|
||||||
"results.results.contents.slimVideoMetadataSectionRenderer." +
|
|
||||||
"contents.elementRenderer.newElement.type.componentType." +
|
|
||||||
"model.videoActionBarModel.buttons.buttonViewModel"
|
|
||||||
).compile()
|
|
||||||
|
|
||||||
@JvmField
|
|
||||||
val GET_VIDEO_DETAILS: CompiledRoute = Route(
|
|
||||||
Route.Method.POST,
|
|
||||||
"player" +
|
|
||||||
"?prettyPrint=false" +
|
|
||||||
"&fields=videoDetails.channelId," +
|
|
||||||
"videoDetails.isLiveContent," +
|
|
||||||
"videoDetails.isUpcoming"
|
|
||||||
).compile()
|
|
||||||
|
|
||||||
private const val YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TCP connection and HTTP read timeout
|
|
||||||
*/
|
|
||||||
private const val CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000 // 10 Seconds.
|
|
||||||
|
|
||||||
private val LOCALE: Locale = Utils.getContext().resources
|
|
||||||
.configuration.locale
|
|
||||||
private val LOCALE_COUNTRY: String = LOCALE.country
|
|
||||||
private val LOCALE_LANGUAGE: String = LOCALE.language
|
|
||||||
private val TIME_ZONE: TimeZone = TimeZone.getDefault()
|
|
||||||
private val TIME_ZONE_ID: String = TIME_ZONE.id
|
|
||||||
private val UTC_OFFSET_MINUTES: Int = TIME_ZONE.getOffset(Date().time) / 60000
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun createApplicationRequestBody(
|
|
||||||
clientType: YouTubeAppClient.ClientType,
|
|
||||||
videoId: String,
|
|
||||||
playlistId: String? = null,
|
|
||||||
botGuardPoToken: String = "",
|
|
||||||
visitorId: String = "",
|
|
||||||
setLocale: Boolean = false,
|
|
||||||
language: String = BaseSettings.SPOOF_STREAMING_DATA_LANGUAGE.get().language,
|
|
||||||
): ByteArray {
|
|
||||||
val innerTubeBody = JSONObject()
|
|
||||||
|
|
||||||
try {
|
|
||||||
val client = JSONObject()
|
|
||||||
client.put("deviceMake", clientType.deviceMake)
|
|
||||||
client.put("deviceModel", clientType.deviceModel)
|
|
||||||
client.put("clientName", clientType.clientName)
|
|
||||||
client.put("clientVersion", clientType.clientVersion)
|
|
||||||
client.put("osName", clientType.osName)
|
|
||||||
client.put("osVersion", clientType.osVersion)
|
|
||||||
if (clientType.androidSdkVersion != null) {
|
|
||||||
client.put("androidSdkVersion", clientType.androidSdkVersion)
|
|
||||||
if (clientType.gmscoreVersionCode != null) {
|
|
||||||
client.put("gmscoreVersionCode", clientType.gmscoreVersionCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client.put(
|
|
||||||
"hl",
|
|
||||||
if (setLocale) {
|
|
||||||
language
|
|
||||||
} else {
|
|
||||||
LOCALE_LANGUAGE
|
|
||||||
}
|
|
||||||
)
|
|
||||||
client.put("gl", LOCALE_COUNTRY)
|
|
||||||
client.put("timeZone", TIME_ZONE_ID)
|
|
||||||
client.put("utcOffsetMinutes", "$UTC_OFFSET_MINUTES")
|
|
||||||
|
|
||||||
val context = JSONObject()
|
|
||||||
context.put("client", client)
|
|
||||||
|
|
||||||
innerTubeBody.put("context", context)
|
|
||||||
innerTubeBody.put("contentCheckOk", true)
|
|
||||||
innerTubeBody.put("racyCheckOk", true)
|
|
||||||
innerTubeBody.put("videoId", videoId)
|
|
||||||
|
|
||||||
if (playlistId != null) {
|
|
||||||
innerTubeBody.put("playlistId", playlistId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!StringUtils.isAnyEmpty(botGuardPoToken, visitorId)) {
|
|
||||||
val serviceIntegrityDimensions = JSONObject()
|
|
||||||
serviceIntegrityDimensions.put("poToken", botGuardPoToken)
|
|
||||||
innerTubeBody.put("serviceIntegrityDimensions", serviceIntegrityDimensions)
|
|
||||||
}
|
|
||||||
} catch (e: JSONException) {
|
|
||||||
Logger.printException({ "Failed to create application innerTubeBody" }, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun createWebInnertubeBody(
|
|
||||||
clientType: YouTubeWebClient.ClientType,
|
|
||||||
videoId: String
|
|
||||||
): ByteArray {
|
|
||||||
val innerTubeBody = JSONObject()
|
|
||||||
|
|
||||||
try {
|
|
||||||
val client = JSONObject()
|
|
||||||
client.put("clientName", clientType.clientName)
|
|
||||||
client.put("clientVersion", clientType.clientVersion)
|
|
||||||
val context = JSONObject()
|
|
||||||
context.put("client", client)
|
|
||||||
|
|
||||||
val lockedSafetyMode = JSONObject()
|
|
||||||
lockedSafetyMode.put("lockedSafetyMode", false)
|
|
||||||
val user = JSONObject()
|
|
||||||
user.put("user", lockedSafetyMode)
|
|
||||||
|
|
||||||
innerTubeBody.put("context", context)
|
|
||||||
innerTubeBody.put("contentCheckOk", true)
|
|
||||||
innerTubeBody.put("racyCheckOk", true)
|
|
||||||
innerTubeBody.put("videoId", videoId)
|
|
||||||
} catch (e: JSONException) {
|
|
||||||
Logger.printException({ "Failed to create web innerTubeBody" }, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getPlayerResponseConnectionFromRoute(
|
|
||||||
route: CompiledRoute,
|
|
||||||
clientType: YouTubeAppClient.ClientType
|
|
||||||
): HttpURLConnection {
|
|
||||||
return getPlayerResponseConnectionFromRoute(
|
|
||||||
route,
|
|
||||||
clientType.userAgent,
|
|
||||||
clientType.id.toString(),
|
|
||||||
clientType.clientVersion
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getPlayerResponseConnectionFromRoute(
|
|
||||||
route: CompiledRoute,
|
|
||||||
clientType: YouTubeWebClient.ClientType
|
|
||||||
): HttpURLConnection {
|
|
||||||
return getPlayerResponseConnectionFromRoute(
|
|
||||||
route,
|
|
||||||
clientType.userAgent,
|
|
||||||
clientType.id.toString(),
|
|
||||||
clientType.clientVersion,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(IOException::class)
|
|
||||||
fun getPlayerResponseConnectionFromRoute(
|
|
||||||
route: CompiledRoute,
|
|
||||||
userAgent: String,
|
|
||||||
clientId: String,
|
|
||||||
clientVersion: String
|
|
||||||
): HttpURLConnection {
|
|
||||||
val connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route)
|
|
||||||
|
|
||||||
connection.setRequestProperty("Content-Type", "application/json")
|
|
||||||
connection.setRequestProperty("User-Agent", userAgent)
|
|
||||||
connection.setRequestProperty("X-YouTube-Client-Name", clientId)
|
|
||||||
connection.setRequestProperty("X-YouTube-Client-Version", clientVersion)
|
|
||||||
|
|
||||||
connection.useCaches = false
|
|
||||||
connection.doOutput = true
|
|
||||||
|
|
||||||
connection.connectTimeout = CONNECTION_TIMEOUT_MILLISECONDS
|
|
||||||
connection.readTimeout = CONNECTION_TIMEOUT_MILLISECONDS
|
|
||||||
return connection
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,14 +1,14 @@
|
|||||||
package app.revanced.extension.shared.patches.spoof.requests
|
package app.revanced.extension.shared.patches.spoof.requests
|
||||||
|
|
||||||
import androidx.annotation.GuardedBy
|
import androidx.annotation.GuardedBy
|
||||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
|
||||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
|
||||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_STREAMING_DATA
|
||||||
import app.revanced.extension.shared.settings.BaseSettings
|
import app.revanced.extension.shared.settings.BaseSettings
|
||||||
import app.revanced.extension.shared.utils.Logger
|
import app.revanced.extension.shared.utils.Logger
|
||||||
|
import app.revanced.extension.shared.utils.StringRef.str
|
||||||
import app.revanced.extension.shared.utils.Utils
|
import app.revanced.extension.shared.utils.Utils
|
||||||
import org.apache.commons.lang3.StringUtils
|
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
@ -32,21 +32,19 @@ import java.util.concurrent.TimeoutException
|
|||||||
* did use its own client streams.
|
* did use its own client streams.
|
||||||
*/
|
*/
|
||||||
class StreamingDataRequest private constructor(
|
class StreamingDataRequest private constructor(
|
||||||
videoId: String, playerHeaders: Map<String, String>,
|
videoId: String,
|
||||||
visitorId: String, botGuardPoToken: String
|
requestHeader: Map<String, String>,
|
||||||
) {
|
) {
|
||||||
private val videoId: String
|
private val videoId: String
|
||||||
private val future: Future<ByteBuffer?>
|
private val future: Future<ByteBuffer?>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Objects.requireNonNull(playerHeaders)
|
Objects.requireNonNull(requestHeader)
|
||||||
this.videoId = videoId
|
this.videoId = videoId
|
||||||
this.future = Utils.submitOnBackgroundThread {
|
this.future = Utils.submitOnBackgroundThread {
|
||||||
fetch(
|
fetch(
|
||||||
videoId,
|
videoId,
|
||||||
playerHeaders,
|
requestHeader,
|
||||||
visitorId,
|
|
||||||
botGuardPoToken
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,33 +84,16 @@ class StreamingDataRequest private constructor(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val AUTHORIZATION_HEADER = "Authorization"
|
private const val AUTHORIZATION_HEADER = "Authorization"
|
||||||
private const val VISITOR_ID_HEADER = "X-Goog-Visitor-Id"
|
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
|
||||||
private val REQUEST_HEADER_KEYS = arrayOf(
|
|
||||||
AUTHORIZATION_HEADER, // Available only to logged-in users.
|
|
||||||
"X-GOOG-API-FORMAT-VERSION",
|
|
||||||
VISITOR_ID_HEADER
|
|
||||||
)
|
|
||||||
private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType =
|
private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType =
|
||||||
BaseSettings.SPOOF_STREAMING_DATA_TYPE.get()
|
BaseSettings.SPOOF_STREAMING_DATA_TYPE.get()
|
||||||
|
|
||||||
private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> =
|
private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> =
|
||||||
YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE)
|
YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE)
|
||||||
|
|
||||||
private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean =
|
private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean =
|
||||||
SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH
|
SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH
|
||||||
|
|
||||||
private var lastSpoofedClientType: YouTubeAppClient.ClientType? = null
|
private var lastSpoofedClientFriendlyName: String? = null
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TCP connection and HTTP read timeout.
|
|
||||||
*/
|
|
||||||
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
|
|
||||||
*/
|
|
||||||
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
|
|
||||||
|
|
||||||
@GuardedBy("itself")
|
@GuardedBy("itself")
|
||||||
val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap(
|
val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap(
|
||||||
@ -126,22 +107,24 @@ class StreamingDataRequest private constructor(
|
|||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val lastSpoofedClientName: String
|
val lastSpoofedClientName: String
|
||||||
get() = lastSpoofedClientType
|
get() {
|
||||||
?.friendlyName
|
return if (lastSpoofedClientFriendlyName != null) {
|
||||||
?: "Unknown"
|
lastSpoofedClientFriendlyName!!
|
||||||
|
} else {
|
||||||
|
"Unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun fetchRequest(
|
fun fetchRequest(
|
||||||
videoId: String, fetchHeaders: Map<String, String>,
|
videoId: String,
|
||||||
visitorId: String, botGuardPoToken: String
|
fetchHeaders: Map<String, String>,
|
||||||
) {
|
) {
|
||||||
// Always fetch, even if there is an existing request for the same video.
|
// Always fetch, even if there is an existing request for the same video.
|
||||||
cache[videoId] =
|
cache[videoId] =
|
||||||
StreamingDataRequest(
|
StreamingDataRequest(
|
||||||
videoId,
|
videoId,
|
||||||
fetchHeaders,
|
fetchHeaders
|
||||||
visitorId,
|
|
||||||
botGuardPoToken
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,71 +133,40 @@ class StreamingDataRequest private constructor(
|
|||||||
return cache[videoId]
|
return cache[videoId]
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleConnectionError(toastMessage: String, ex: Exception?) {
|
private fun handleConnectionError(
|
||||||
|
toastMessage: String,
|
||||||
|
ex: Exception?,
|
||||||
|
showToast: Boolean = false,
|
||||||
|
) {
|
||||||
|
if (showToast) Utils.showToastShort(toastMessage)
|
||||||
Logger.printInfo({ toastMessage }, ex)
|
Logger.printInfo({ toastMessage }, ex)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun send(
|
private fun send(
|
||||||
clientType: YouTubeAppClient.ClientType,
|
clientType: YouTubeAppClient.ClientType,
|
||||||
videoId: String,
|
videoId: String,
|
||||||
playerHeaders: Map<String, String>,
|
requestHeader: Map<String, String>,
|
||||||
visitorId: String,
|
|
||||||
botGuardPoToken: String
|
|
||||||
): HttpURLConnection? {
|
): HttpURLConnection? {
|
||||||
Objects.requireNonNull(clientType)
|
Objects.requireNonNull(clientType)
|
||||||
Objects.requireNonNull(videoId)
|
Objects.requireNonNull(videoId)
|
||||||
Objects.requireNonNull(playerHeaders)
|
Objects.requireNonNull(requestHeader)
|
||||||
|
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" }
|
Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val connection =
|
val connection =
|
||||||
getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType)
|
getInnerTubeResponseConnectionFromRoute(
|
||||||
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
|
GET_STREAMING_DATA,
|
||||||
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
|
clientType,
|
||||||
|
requestHeader
|
||||||
val usePoToken =
|
|
||||||
clientType.requirePoToken && !StringUtils.isAnyEmpty(botGuardPoToken, visitorId)
|
|
||||||
|
|
||||||
for (key in REQUEST_HEADER_KEYS) {
|
|
||||||
var value = playerHeaders[key]
|
|
||||||
if (value != null) {
|
|
||||||
if (key == AUTHORIZATION_HEADER) {
|
|
||||||
if (!clientType.supportsCookies) {
|
|
||||||
Logger.printDebug { "Not including request header: $key" }
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (key == VISITOR_ID_HEADER && usePoToken) {
|
|
||||||
val originalVisitorId: String = value
|
|
||||||
Logger.printDebug { "Original visitor id:\n$originalVisitorId" }
|
|
||||||
Logger.printDebug { "Replaced visitor id:\n$visitorId" }
|
|
||||||
value = visitorId
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.setRequestProperty(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val requestBody: ByteArray
|
|
||||||
if (usePoToken) {
|
|
||||||
requestBody = createApplicationRequestBody(
|
|
||||||
clientType = clientType,
|
|
||||||
videoId = videoId,
|
|
||||||
botGuardPoToken = botGuardPoToken,
|
|
||||||
visitorId = visitorId,
|
|
||||||
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
|
|
||||||
)
|
)
|
||||||
Logger.printDebug { "Set poToken (botGuardPoToken):\n$botGuardPoToken" }
|
|
||||||
} else {
|
val requestBody = createApplicationRequestBody(
|
||||||
requestBody =
|
clientType = clientType,
|
||||||
createApplicationRequestBody(
|
videoId = videoId,
|
||||||
clientType = clientType,
|
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
|
||||||
videoId = videoId,
|
)
|
||||||
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.setFixedLengthStreamingMode(requestBody.size)
|
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||||
connection.outputStream.write(requestBody)
|
connection.outputStream.write(requestBody)
|
||||||
@ -243,15 +195,15 @@ class StreamingDataRequest private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun fetch(
|
private fun fetch(
|
||||||
videoId: String, playerHeaders: Map<String, String>,
|
videoId: String,
|
||||||
visitorId: String, botGuardPoToken: String
|
requestHeader: Map<String, String>,
|
||||||
): ByteBuffer? {
|
): ByteBuffer? {
|
||||||
lastSpoofedClientType = null
|
lastSpoofedClientFriendlyName = null
|
||||||
|
|
||||||
// Retry with different client if empty response body is received.
|
// Retry with different client if empty response body is received.
|
||||||
for (clientType in CLIENT_ORDER_TO_USE) {
|
for (clientType in CLIENT_ORDER_TO_USE) {
|
||||||
if (clientType.requireAuth &&
|
if (clientType.requireAuth &&
|
||||||
playerHeaders[AUTHORIZATION_HEADER] == null
|
requestHeader[AUTHORIZATION_HEADER] == null
|
||||||
) {
|
) {
|
||||||
Logger.printDebug { "Skipped login-required client (incognito mode or not logged in)\nClient: $clientType\nVideo: $videoId" }
|
Logger.printDebug { "Skipped login-required client (incognito mode or not logged in)\nClient: $clientType\nVideo: $videoId" }
|
||||||
continue
|
continue
|
||||||
@ -259,9 +211,7 @@ class StreamingDataRequest private constructor(
|
|||||||
send(
|
send(
|
||||||
clientType,
|
clientType,
|
||||||
videoId,
|
videoId,
|
||||||
playerHeaders,
|
requestHeader,
|
||||||
visitorId,
|
|
||||||
botGuardPoToken
|
|
||||||
)?.let { connection ->
|
)?.let { connection ->
|
||||||
try {
|
try {
|
||||||
// gzip encoding doesn't response with content length (-1),
|
// gzip encoding doesn't response with content length (-1),
|
||||||
@ -271,14 +221,14 @@ class StreamingDataRequest private constructor(
|
|||||||
} else {
|
} else {
|
||||||
BufferedInputStream(connection.inputStream).use { inputStream ->
|
BufferedInputStream(connection.inputStream).use { inputStream ->
|
||||||
ByteArrayOutputStream().use { stream ->
|
ByteArrayOutputStream().use { stream ->
|
||||||
val buffer = ByteArray(2048)
|
val buffer = ByteArray(4096)
|
||||||
var bytesRead: Int
|
var bytesRead: Int
|
||||||
while ((inputStream.read(buffer)
|
while ((inputStream.read(buffer)
|
||||||
.also { bytesRead = it }) >= 0
|
.also { bytesRead = it }) >= 0
|
||||||
) {
|
) {
|
||||||
stream.write(buffer, 0, bytesRead)
|
stream.write(buffer, 0, bytesRead)
|
||||||
}
|
}
|
||||||
lastSpoofedClientType = clientType
|
lastSpoofedClientFriendlyName = clientType.friendlyName
|
||||||
return ByteBuffer.wrap(stream.toByteArray())
|
return ByteBuffer.wrap(stream.toByteArray())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -289,7 +239,12 @@ class StreamingDataRequest private constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConnectionError("Could not fetch any client streams", null)
|
handleConnectionError(str("revanced_spoof_streaming_data_failed_forbidden"), null, true)
|
||||||
|
handleConnectionError(
|
||||||
|
str("revanced_spoof_streaming_data_failed_forbidden_suggestion"),
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,10 @@ package app.revanced.extension.shared.settings;
|
|||||||
import static java.lang.Boolean.FALSE;
|
import static java.lang.Boolean.FALSE;
|
||||||
import static java.lang.Boolean.TRUE;
|
import static java.lang.Boolean.TRUE;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient;
|
||||||
|
import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient;
|
||||||
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
|
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
|
||||||
import app.revanced.extension.shared.patches.WatchHistoryPatch.WatchHistoryType;
|
import app.revanced.extension.shared.patches.WatchHistoryPatch.WatchHistoryType;
|
||||||
import app.revanced.extension.shared.patches.client.MusicAppClient;
|
|
||||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient;
|
|
||||||
import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch.AudioStreamLanguageOverrideAvailability;
|
import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch.AudioStreamLanguageOverrideAvailability;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -31,7 +31,7 @@ public class BaseSettings {
|
|||||||
* Some patches are in a shared path, so they are declared here.
|
* Some patches are in a shared path, so they are declared here.
|
||||||
*/
|
*/
|
||||||
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true);
|
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true);
|
||||||
public static final EnumSetting<MusicAppClient.ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", MusicAppClient.ClientType.IOS_MUSIC_6_21, true);
|
public static final EnumSetting<YouTubeMusicAppClient.ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", YouTubeMusicAppClient.ClientType.IOS_MUSIC_6_21, true);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These settings are used by YouTube.
|
* These settings are used by YouTube.
|
||||||
@ -43,11 +43,9 @@ public class BaseSettings {
|
|||||||
"revanced_spoof_streaming_data_ios_force_avc_user_dialog_message");
|
"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_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);
|
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
|
||||||
|
public static final BooleanSetting SPOOF_STREAMING_DATA_TYPE_IOS = new BooleanSetting("revanced_spoof_streaming_data_type_ios", FALSE, true, "revanced_spoof_streaming_data_type_ios_user_dialog_message");
|
||||||
// Client type must be last spoof setting due to cyclic references.
|
// Client type must be last spoof setting due to cyclic references.
|
||||||
public static final EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_UNPLUGGED, true);
|
public static final EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_VR, true);
|
||||||
|
|
||||||
public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true);
|
|
||||||
public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* These settings are used by YouTube and YouTube Music.
|
* These settings are used by YouTube and YouTube Music.
|
||||||
|
@ -22,16 +22,16 @@ import android.widget.ListView;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
|
import app.revanced.extension.shared.utils.StringRef;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that if a preference changes,
|
* Indicates that if a preference changes,
|
||||||
* to apply the change from the Setting to the UI component.
|
* to apply the change from the Setting to the UI component.
|
||||||
@ -39,7 +39,11 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
public static boolean settingImportInProgress;
|
public static boolean settingImportInProgress;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Confirm and restart dialog button text and title.
|
* Prevents recursive calls during preference <-> UI syncing from showing extra dialogs.
|
||||||
|
*/
|
||||||
|
private static boolean updatingPreference;
|
||||||
|
|
||||||
|
/**
|
||||||
* Set by subclasses if Strings cannot be added as a resource.
|
* Set by subclasses if Strings cannot be added as a resource.
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
@ -52,7 +56,14 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
|
|
||||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||||
try {
|
try {
|
||||||
Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
|
if (updatingPreference) {
|
||||||
|
Logger.printDebug(() -> "Ignoring preference change as sync is in progress");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (str == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Setting<?> setting = Setting.getSettingFromPath(str);
|
||||||
if (setting == null) {
|
if (setting == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -73,10 +84,13 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatingPreference = true;
|
||||||
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
|
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
|
||||||
|
// Updating here can can cause a recursive call back into this same method.
|
||||||
updatePreference(pref, setting, true, settingImportInProgress);
|
updatePreference(pref, setting, true, settingImportInProgress);
|
||||||
// Update any other preference availability that may now be different.
|
// Update any other preference availability that may now be different.
|
||||||
updateUIAvailability();
|
updateUIAvailability();
|
||||||
|
updatingPreference = false;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
|
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
|
||||||
}
|
}
|
||||||
@ -103,36 +117,39 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
Utils.verifyOnMainThread();
|
Utils.verifyOnMainThread();
|
||||||
|
|
||||||
final var context = getActivity();
|
final var context = getActivity();
|
||||||
showingUserDialogMessage = true;
|
final StringRef userDialogMessage = setting.userDialogMessage;
|
||||||
assert setting.userDialogMessage != null;
|
if (context != null && userDialogMessage != null) {
|
||||||
new AlertDialog.Builder(context)
|
showingUserDialogMessage = true;
|
||||||
.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.
|
new AlertDialog.Builder(context)
|
||||||
updateUIAvailability();
|
.setTitle(android.R.string.dialog_alert_title)
|
||||||
|
.setMessage(userDialogMessage.toString())
|
||||||
|
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
||||||
|
// User confirmed, save to the Setting.
|
||||||
|
updatePreference(pref, setting, true, false);
|
||||||
|
|
||||||
if (setting.rebootApp) {
|
// Update availability of other preferences that may be changed.
|
||||||
showRestartDialog(context);
|
updateUIAvailability();
|
||||||
}
|
|
||||||
})
|
if (setting.rebootApp) {
|
||||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
showRestartDialog(context);
|
||||||
// Restore whatever the setting was before the change.
|
}
|
||||||
updatePreference(pref, setting, true, true);
|
})
|
||||||
})
|
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||||
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
|
// Restore whatever the setting was before the change.
|
||||||
.setCancelable(false)
|
updatePreference(pref, setting, true, true);
|
||||||
.show();
|
})
|
||||||
|
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
|
||||||
|
.setCancelable(false)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates all Preferences values and their availability using the current values in {@link Setting}.
|
* Updates all Preferences values and their availability using the current values in {@link Setting}.
|
||||||
*/
|
*/
|
||||||
protected void updateUIToSettingValues() {
|
protected void updateUIToSettingValues() {
|
||||||
updatePreferenceScreen(getPreferenceScreen(), true,true);
|
updatePreferenceScreen(getPreferenceScreen(), true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -146,14 +163,16 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
* @return If the preference is currently set to the default value of the Setting.
|
* @return If the preference is currently set to the default value of the Setting.
|
||||||
*/
|
*/
|
||||||
protected boolean prefIsSetToDefault(Preference pref, Setting<?> setting) {
|
protected boolean prefIsSetToDefault(Preference pref, Setting<?> setting) {
|
||||||
|
Object defaultValue = setting.defaultValue;
|
||||||
if (pref instanceof SwitchPreference switchPref) {
|
if (pref instanceof SwitchPreference switchPref) {
|
||||||
return switchPref.isChecked() == (Boolean) setting.defaultValue;
|
return switchPref.isChecked() == (Boolean) defaultValue;
|
||||||
}
|
}
|
||||||
|
String defaultValueString = defaultValue.toString();
|
||||||
if (pref instanceof EditTextPreference editPreference) {
|
if (pref instanceof EditTextPreference editPreference) {
|
||||||
return editPreference.getText().equals(setting.defaultValue.toString());
|
return editPreference.getText().equals(defaultValueString);
|
||||||
}
|
}
|
||||||
if (pref instanceof ListPreference listPref) {
|
if (pref instanceof ListPreference listPref) {
|
||||||
return listPref.getValue().equals(setting.defaultValue.toString());
|
return listPref.getValue().equals(defaultValueString);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalStateException("Must override method to handle "
|
throw new IllegalStateException("Must override method to handle "
|
||||||
@ -227,7 +246,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
/**
|
/**
|
||||||
* Updates a UI Preference with the {@link Setting} that backs it.
|
* 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.
|
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
|
||||||
* If false, then apply {@link Setting} <- Preference.
|
* If false, then apply {@link Setting} <- Preference.
|
||||||
*/
|
*/
|
||||||
@ -258,18 +277,19 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
|||||||
listPreference.setSummary(objectStringValue);
|
listPreference.setSummary(objectStringValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showRestartDialog(@NonNull final Context context) {
|
public static void showRestartDialog(@NonNull Context context) {
|
||||||
if (restartDialogMessage == null) {
|
if (restartDialogMessage == null) {
|
||||||
restartDialogMessage = str("revanced_extended_restart_message");
|
restartDialogMessage = str("revanced_extended_restart_message");
|
||||||
}
|
}
|
||||||
|
|
||||||
showRestartDialog(context, restartDialogMessage);
|
showRestartDialog(context, restartDialogMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showRestartDialog(@NonNull final Context context, String message) {
|
public static void showRestartDialog(@NonNull Context context, String message) {
|
||||||
showRestartDialog(context, message, 0);
|
showRestartDialog(context, message, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showRestartDialog(@NonNull final Context context, String message, long delay) {
|
public static void showRestartDialog(@NonNull Context context, String message, long delay) {
|
||||||
Utils.verifyOnMainThread();
|
Utils.verifyOnMainThread();
|
||||||
|
|
||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context)
|
||||||
|
@ -10,6 +10,8 @@ import android.util.AttributeSet;
|
|||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
@ -19,6 +21,12 @@ import app.revanced.extension.shared.utils.Utils;
|
|||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class ResettableEditTextPreference extends EditTextPreference {
|
public class ResettableEditTextPreference extends EditTextPreference {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setting to reset.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private Setting<?> setting;
|
||||||
|
|
||||||
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
}
|
}
|
||||||
@ -35,6 +43,10 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
|||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setSetting(@Nullable Setting<?> setting) {
|
||||||
|
this.setting = setting;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||||
Utils.setEditTextDialogTheme(builder);
|
Utils.setEditTextDialogTheme(builder);
|
||||||
@ -44,7 +56,12 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
|||||||
if (title != null) {
|
if (title != null) {
|
||||||
builder.setTitle(getTitle());
|
builder.setTitle(getTitle());
|
||||||
}
|
}
|
||||||
final Setting<?> setting = Setting.getSettingFromPath(getKey());
|
if (setting == null) {
|
||||||
|
String key = getKey();
|
||||||
|
if (key != null) {
|
||||||
|
setting = Setting.getSettingFromPath(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (setting != null) {
|
if (setting != null) {
|
||||||
builder.setNeutralButton(str("revanced_extended_settings_reset"), null);
|
builder.setNeutralButton(str("revanced_extended_settings_reset"), null);
|
||||||
}
|
}
|
||||||
@ -65,8 +82,7 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
|||||||
}
|
}
|
||||||
button.setOnClickListener(v -> {
|
button.setOnClickListener(v -> {
|
||||||
try {
|
try {
|
||||||
Setting<?> setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey()));
|
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
|
||||||
String defaultStringValue = setting.defaultValue.toString();
|
|
||||||
EditText editText = getEditText();
|
EditText editText = getEditText();
|
||||||
editText.setText(defaultStringValue);
|
editText.setText(defaultStringValue);
|
||||||
editText.setSelection(defaultStringValue.length()); // move cursor to end of text
|
editText.setSelection(defaultStringValue.length()); // move cursor to end of text
|
||||||
|
@ -29,10 +29,20 @@ public class PackageUtils extends Utils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static Integer getTargetSDKVersion(@NonNull String packageName) {
|
||||||
|
ApplicationInfo applicationInfo = getApplicationInfo(packageName);
|
||||||
|
if (applicationInfo != null) {
|
||||||
|
return applicationInfo.targetSdkVersion;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isPackageEnabled(@NonNull String packageName) {
|
public static boolean isPackageEnabled(@NonNull String packageName) {
|
||||||
try {
|
ApplicationInfo applicationInfo = getApplicationInfo(packageName);
|
||||||
return getContext().getPackageManager().getApplicationInfo(packageName, 0).enabled;
|
if (applicationInfo != null) {
|
||||||
} catch (PackageManager.NameNotFoundException ignored) {
|
return applicationInfo.enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -47,6 +57,16 @@ public class PackageUtils extends Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// utils
|
// utils
|
||||||
|
@Nullable
|
||||||
|
private static ApplicationInfo getApplicationInfo(@NonNull String packageName) {
|
||||||
|
try {
|
||||||
|
return getContext().getPackageManager().getApplicationInfo(packageName, 0);
|
||||||
|
} catch (PackageManager.NameNotFoundException e) {
|
||||||
|
Logger.printException(() -> "Failed to get application Info!" + e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private static PackageInfo getPackageInfo() {
|
private static PackageInfo getPackageInfo() {
|
||||||
try {
|
try {
|
||||||
|
@ -60,6 +60,7 @@ public class Utils {
|
|||||||
private static WeakReference<Activity> activityRef = new WeakReference<>(null);
|
private static WeakReference<Activity> activityRef = new WeakReference<>(null);
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
private static volatile Context context;
|
private static volatile Context context;
|
||||||
|
private static Locale contextLocale;
|
||||||
|
|
||||||
protected Utils() {
|
protected Utils() {
|
||||||
} // utility class
|
} // utility class
|
||||||
@ -308,34 +309,51 @@ public class Utils {
|
|||||||
* @return Context with locale applied.
|
* @return Context with locale applied.
|
||||||
*/
|
*/
|
||||||
public static Context getLocalizedContext(Context mContext) {
|
public static Context getLocalizedContext(Context mContext) {
|
||||||
Activity mActivity = activityRef.get();
|
try {
|
||||||
if (mActivity == null) {
|
Activity mActivity = activityRef.get();
|
||||||
return mContext;
|
if (mActivity != null && mContext != null) {
|
||||||
}
|
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
||||||
if (mContext == null) {
|
|
||||||
return null;
|
// Locale of Application.
|
||||||
|
Locale applicationLocale = language == AppLanguage.DEFAULT
|
||||||
|
? mActivity.getResources().getConfiguration().locale
|
||||||
|
: language.getLocale();
|
||||||
|
|
||||||
|
// Locale of Context.
|
||||||
|
Locale contextLocale = mContext.getResources().getConfiguration().locale;
|
||||||
|
|
||||||
|
// If they are different, overrides the Locale of the Context and resource.
|
||||||
|
if (applicationLocale != contextLocale) {
|
||||||
|
Utils.contextLocale = contextLocale;
|
||||||
|
|
||||||
|
// If they are different, overrides the Locale of the Context and resource.
|
||||||
|
Locale.setDefault(applicationLocale);
|
||||||
|
Configuration configuration = new Configuration(mContext.getResources().getConfiguration());
|
||||||
|
configuration.setLocale(applicationLocale);
|
||||||
|
return mContext.createConfigurationContext(configuration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "getLocalizedContext failed", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
return mContext;
|
||||||
|
}
|
||||||
|
|
||||||
// Locale of Application.
|
public static void resetLocalizedContext() {
|
||||||
Locale applicationLocale = language == AppLanguage.DEFAULT
|
try {
|
||||||
? mActivity.getResources().getConfiguration().locale
|
if (contextLocale != null) {
|
||||||
: language.getLocale();
|
Locale.setDefault(contextLocale);
|
||||||
|
Context mContext = getContext();
|
||||||
// Locale of Context.
|
if (mContext != null) {
|
||||||
Locale contextLocale = mContext.getResources().getConfiguration().locale;
|
Configuration config = mContext.getResources().getConfiguration();
|
||||||
|
config.setLocale(contextLocale);
|
||||||
// If they are identical, no need to override them.
|
setContext(mContext.createConfigurationContext(config));
|
||||||
if (applicationLocale == contextLocale) {
|
}
|
||||||
return mContext;
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "resetLocalizedContext failed", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If they are different, overrides the Locale of the Context and resource.
|
|
||||||
Locale.setDefault(applicationLocale);
|
|
||||||
Configuration configuration = new Configuration(mContext.getResources().getConfiguration());
|
|
||||||
configuration.setLocale(applicationLocale);
|
|
||||||
return mContext.createConfigurationContext(configuration);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setActivity(Activity mainActivity) {
|
public static void setActivity(Activity mainActivity) {
|
||||||
@ -353,14 +371,6 @@ public class Utils {
|
|||||||
// Must initially set context to check the app language.
|
// Must initially set context to check the app language.
|
||||||
context = appContext;
|
context = appContext;
|
||||||
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
|
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
|
||||||
|
|
||||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
|
||||||
if (language != AppLanguage.DEFAULT) {
|
|
||||||
// Create a new context with the desired language.
|
|
||||||
Configuration config = appContext.getResources().getConfiguration();
|
|
||||||
config.setLocale(language.getLocale());
|
|
||||||
context = appContext.createConfigurationContext(config);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setClipboard(@NonNull String text) {
|
public static void setClipboard(@NonNull String text) {
|
||||||
@ -538,14 +548,6 @@ public class Utils {
|
|||||||
return Build.VERSION.SDK_INT >= sdk;
|
return Build.VERSION.SDK_INT >= sdk;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int dpToPx(float dp) {
|
|
||||||
if (context == null) {
|
|
||||||
return (int) dp;
|
|
||||||
} else {
|
|
||||||
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int dpToPx(int dp) {
|
public static int dpToPx(int dp) {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
return dp;
|
return dp;
|
||||||
@ -608,10 +610,10 @@ public class Utils {
|
|||||||
* <br>
|
* <br>
|
||||||
* Be aware the on start action can be called multiple times for some situations,
|
* 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.
|
* such as the user switching apps without dismissing the dialog then switching back to this app.
|
||||||
*<br>
|
* <br>
|
||||||
* This method is only useful during app startup and multiple patches may show their own dialog,
|
* 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.
|
* and the most important dialog can be called last (using a delay) so it's always on top.
|
||||||
*<br>
|
* <br>
|
||||||
* For all other situations it's better to not use this method and
|
* For all other situations it's better to not use this method and
|
||||||
* call {@link AlertDialog#show()} on the dialog.
|
* call {@link AlertDialog#show()} on the dialog.
|
||||||
*/
|
*/
|
||||||
|
@ -6,7 +6,6 @@ import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
|
|||||||
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
|
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
|
||||||
import app.revanced.extension.shared.patches.components.Filter;
|
import app.revanced.extension.shared.patches.components.Filter;
|
||||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
|
@ -6,7 +6,6 @@ import app.revanced.extension.shared.settings.Setting.Availability
|
|||||||
import app.revanced.extension.shared.utils.Logger
|
import app.revanced.extension.shared.utils.Logger
|
||||||
import app.revanced.extension.youtube.settings.Settings
|
import app.revanced.extension.youtube.settings.Settings
|
||||||
import org.apache.commons.lang3.StringUtils
|
import org.apache.commons.lang3.StringUtils
|
||||||
import kotlin.Boolean
|
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
object ChangeStartPagePatch {
|
object ChangeStartPagePatch {
|
||||||
@ -44,7 +43,7 @@ object ChangeStartPagePatch {
|
|||||||
}
|
}
|
||||||
appLaunched = true
|
appLaunched = true
|
||||||
|
|
||||||
Logger.printDebug{ "Changing browseId to $browseId" }
|
Logger.printDebug { "Changing browseId to $browseId" }
|
||||||
return browseId
|
return browseId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,18 +1,34 @@
|
|||||||
package app.revanced.extension.youtube.patches.general;
|
package app.revanced.extension.youtube.patches.general;
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import static app.revanced.extension.youtube.utils.VideoUtils.launchPlaylistExternalDownloader;
|
||||||
|
import static app.revanced.extension.youtube.utils.VideoUtils.launchVideoExternalDownloader;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
|
import app.revanced.extension.youtube.patches.utils.PlaylistPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public final class DownloadActionsPatch extends VideoUtils {
|
public final class DownloadActionsPatch {
|
||||||
|
|
||||||
private static final BooleanSetting overrideVideoDownloadButton =
|
private static final boolean OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON =
|
||||||
Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON;
|
Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON.get();
|
||||||
|
|
||||||
private static final BooleanSetting overridePlaylistDownloadButton =
|
private static final boolean OVERRIDE_VIDEO_DOWNLOAD_BUTTON =
|
||||||
Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON;
|
Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON.get();
|
||||||
|
|
||||||
|
private static final boolean OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER =
|
||||||
|
OVERRIDE_VIDEO_DOWNLOAD_BUTTON && Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER.get();
|
||||||
|
|
||||||
|
private static final String ELEMENTS_SENDER_VIEW =
|
||||||
|
"com.google.android.libraries.youtube.rendering.elements.sender_view";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
@ -23,17 +39,21 @@ public final class DownloadActionsPatch extends VideoUtils {
|
|||||||
* <p>
|
* <p>
|
||||||
* Appears to always be called from the main thread.
|
* Appears to always be called from the main thread.
|
||||||
*/
|
*/
|
||||||
public static boolean inAppVideoDownloadButtonOnClick(String videoId) {
|
public static boolean inAppVideoDownloadButtonOnClick(@Nullable Map<Object, Object> map, Object offlineVideoEndpointOuterClass,
|
||||||
|
@Nullable String videoId) {
|
||||||
try {
|
try {
|
||||||
if (!overrideVideoDownloadButton.get()) {
|
if (OVERRIDE_VIDEO_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(videoId)) {
|
||||||
return false;
|
if (OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER) {
|
||||||
}
|
if (map != null && map.get(ELEMENTS_SENDER_VIEW) instanceof View view) {
|
||||||
if (videoId == null || videoId.isEmpty()) {
|
PlaylistPatch.setContext(view.getContext());
|
||||||
return false;
|
}
|
||||||
}
|
PlaylistPatch.prepareDialogBuilder(videoId);
|
||||||
launchVideoExternalDownloader(videoId);
|
} else {
|
||||||
|
launchVideoExternalDownloader(videoId);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "inAppVideoDownloadButtonOnClick failure", ex);
|
Logger.printException(() -> "inAppVideoDownloadButtonOnClick failure", ex);
|
||||||
}
|
}
|
||||||
@ -49,15 +69,10 @@ public final class DownloadActionsPatch extends VideoUtils {
|
|||||||
*/
|
*/
|
||||||
public static String inAppPlaylistDownloadButtonOnClick(String playlistId) {
|
public static String inAppPlaylistDownloadButtonOnClick(String playlistId) {
|
||||||
try {
|
try {
|
||||||
if (!overridePlaylistDownloadButton.get()) {
|
if (OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(playlistId)) {
|
||||||
return playlistId;
|
launchPlaylistExternalDownloader(playlistId);
|
||||||
|
return "";
|
||||||
}
|
}
|
||||||
if (playlistId == null || playlistId.isEmpty()) {
|
|
||||||
return playlistId;
|
|
||||||
}
|
|
||||||
launchPlaylistExternalDownloader(playlistId);
|
|
||||||
|
|
||||||
return "";
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "inAppPlaylistDownloadButtonOnClick failure", ex);
|
Logger.printException(() -> "inAppPlaylistDownloadButtonOnClick failure", ex);
|
||||||
}
|
}
|
||||||
@ -73,15 +88,10 @@ public final class DownloadActionsPatch extends VideoUtils {
|
|||||||
*/
|
*/
|
||||||
public static boolean inAppPlaylistDownloadMenuOnClick(String playlistId) {
|
public static boolean inAppPlaylistDownloadMenuOnClick(String playlistId) {
|
||||||
try {
|
try {
|
||||||
if (!overridePlaylistDownloadButton.get()) {
|
if (OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(playlistId)) {
|
||||||
return false;
|
launchPlaylistExternalDownloader(playlistId);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
if (playlistId == null || playlistId.isEmpty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
launchPlaylistExternalDownloader(playlistId);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "inAppPlaylistDownloadMenuOnClick failure", ex);
|
Logger.printException(() -> "inAppPlaylistDownloadMenuOnClick failure", ex);
|
||||||
}
|
}
|
||||||
@ -92,7 +102,7 @@ public final class DownloadActionsPatch extends VideoUtils {
|
|||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static boolean overridePlaylistDownloadButtonVisibility() {
|
public static boolean overridePlaylistDownloadButtonVisibility() {
|
||||||
return overridePlaylistDownloadButton.get();
|
return OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,7 @@ import java.util.EnumMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
@ -105,6 +106,34 @@ public class GeneralPatch {
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
// region [Disable layout updates] patch
|
||||||
|
|
||||||
|
private static final String[] REQUEST_HEADER_KEYS = {
|
||||||
|
"X-Youtube-Cold-Config-Data",
|
||||||
|
"X-Youtube-Cold-Hash-Data",
|
||||||
|
"X-Youtube-Hot-Config-Data",
|
||||||
|
"X-Youtube-Hot-Hash-Data"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final boolean DISABLE_LAYOUT_UPDATES =
|
||||||
|
Settings.DISABLE_LAYOUT_UPDATES.get();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param key Keys to be added to the header of CronetBuilder.
|
||||||
|
* @param value Values to be added to the header of CronetBuilder.
|
||||||
|
* @return Empty value if setting is enabled.
|
||||||
|
*/
|
||||||
|
public static String disableLayoutUpdates(String key, String value) {
|
||||||
|
if (DISABLE_LAYOUT_UPDATES && StringUtils.equalsAny(key, REQUEST_HEADER_KEYS)) {
|
||||||
|
Logger.printDebug(() -> "Blocking: " + key);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
// region [Disable splash animation] patch
|
// region [Disable splash animation] patch
|
||||||
|
|
||||||
public static boolean disableSplashAnimation(boolean original) {
|
public static boolean disableSplashAnimation(boolean original) {
|
||||||
@ -234,6 +263,17 @@ public class GeneralPatch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getLibraryDrawableId(int original) {
|
||||||
|
if (ExtendedUtils.IS_19_26_OR_GREATER &&
|
||||||
|
!ExtendedUtils.isSpoofingToLessThan("19.27.00")) {
|
||||||
|
int libraryCairoId = ResourceUtils.getDrawableIdentifier("yt_outline_library_cairo_black_24");
|
||||||
|
if (libraryCairoId != 0) {
|
||||||
|
return libraryCairoId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean switchCreateWithNotificationButton(boolean original) {
|
public static boolean switchCreateWithNotificationButton(boolean original) {
|
||||||
return Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get() || original;
|
return Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get() || original;
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,8 @@ public final class OpenChannelOfLiveAvatarPatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*
|
*
|
||||||
* @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor
|
* @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor
|
||||||
* @param newlyLoadedVideoId id of the current video
|
* @param newlyLoadedVideoId id of the current video
|
||||||
*/
|
*/
|
||||||
public static void fetchChannelId(@NonNull Map<Object, Object> playbackStartDescriptorMap, String newlyLoadedVideoId) {
|
public static void fetchChannelId(@NonNull Map<Object, Object> playbackStartDescriptorMap, String newlyLoadedVideoId) {
|
||||||
try {
|
try {
|
||||||
|
@ -2,8 +2,10 @@ package app.revanced.extension.youtube.patches.general.requests
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.annotation.GuardedBy
|
import androidx.annotation.GuardedBy
|
||||||
import app.revanced.extension.shared.patches.client.YouTubeWebClient
|
import app.revanced.extension.shared.innertube.client.YouTubeWebClient
|
||||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createWebInnertubeBody
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_VIDEO_DETAILS
|
||||||
import app.revanced.extension.shared.requests.Requester
|
import app.revanced.extension.shared.requests.Requester
|
||||||
import app.revanced.extension.shared.utils.Logger
|
import app.revanced.extension.shared.utils.Logger
|
||||||
import app.revanced.extension.shared.utils.Utils
|
import app.revanced.extension.shared.utils.Utils
|
||||||
@ -86,12 +88,11 @@ class VideoDetailsRequest private constructor(
|
|||||||
Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" }
|
Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
|
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||||
PlayerRoutes.GET_VIDEO_DETAILS,
|
GET_VIDEO_DETAILS,
|
||||||
clientType
|
clientType
|
||||||
)
|
)
|
||||||
val requestBody =
|
val requestBody = createWebInnertubeBody(clientType, videoId)
|
||||||
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
|
|
||||||
|
|
||||||
connection.setFixedLengthStreamingMode(requestBody.size)
|
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||||
connection.outputStream.write(requestBody)
|
connection.outputStream.write(requestBody)
|
||||||
|
@ -15,7 +15,16 @@ public class BackgroundPlaybackPatch {
|
|||||||
*/
|
*/
|
||||||
public static boolean isBackgroundPlaybackAllowed(boolean original) {
|
public static boolean isBackgroundPlaybackAllowed(boolean original) {
|
||||||
if (original) return true;
|
if (original) return true;
|
||||||
return ShortsPlayerState.getCurrent().isClosed();
|
return ShortsPlayerState.getCurrent().isClosed() &&
|
||||||
|
// 1. Shorts background playback is enabled.
|
||||||
|
// 2. Autoplay in feed is turned on.
|
||||||
|
// 3. Play Shorts from feed.
|
||||||
|
// 4. Media controls appear in status bar.
|
||||||
|
// (For unpatched YouTube with Premium accounts, media controls do not appear in the status bar)
|
||||||
|
//
|
||||||
|
// This is just a visual bug and does not affect Shorts background play in any way.
|
||||||
|
// To fix this, just check PlayerType.
|
||||||
|
PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -6,7 +6,9 @@ import android.view.ViewGroup;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
|
import app.revanced.extension.youtube.patches.utils.PlaylistPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@ -19,7 +21,14 @@ public class ExternalDownload extends BottomControlButton {
|
|||||||
bottomControlsViewGroup,
|
bottomControlsViewGroup,
|
||||||
"external_download_button",
|
"external_download_button",
|
||||||
Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER,
|
Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER,
|
||||||
view -> VideoUtils.launchVideoExternalDownloader(),
|
view -> {
|
||||||
|
if (Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER_QUEUE_MANAGER.get()) {
|
||||||
|
PlaylistPatch.setContext(view.getContext());
|
||||||
|
PlaylistPatch.prepareDialogBuilder(VideoInformation.getVideoId());
|
||||||
|
} else {
|
||||||
|
VideoUtils.launchVideoExternalDownloader();
|
||||||
|
}
|
||||||
|
},
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.patches.player;
|
package app.revanced.extension.youtube.patches.player;
|
||||||
|
|
||||||
|
import static app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton.REMIX;
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import org.apache.commons.lang3.ArrayUtils;
|
import org.apache.commons.lang3.ArrayUtils;
|
||||||
@ -8,8 +10,6 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton.*;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
@ -106,8 +106,8 @@ public class ActionButtonsPatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*
|
*
|
||||||
* @param list Type list of litho components
|
* @param list Type list of litho components
|
||||||
* @param identifier Identifier of litho components
|
* @param identifier Identifier of litho components
|
||||||
*/
|
*/
|
||||||
public static List<Object> hideActionButtonByIndex(@Nullable List<Object> list, @Nullable String identifier) {
|
public static List<Object> hideActionButtonByIndex(@Nullable List<Object> list, @Nullable String identifier) {
|
||||||
try {
|
try {
|
||||||
|
@ -24,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.IntegerSetting;
|
import app.revanced.extension.shared.settings.IntegerSetting;
|
||||||
import app.revanced.extension.shared.settings.StringSetting;
|
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
@ -34,7 +33,6 @@ import app.revanced.extension.youtube.settings.Settings;
|
|||||||
import app.revanced.extension.youtube.shared.EngagementPanel;
|
import app.revanced.extension.youtube.shared.EngagementPanel;
|
||||||
import app.revanced.extension.youtube.shared.PlayerType;
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
import app.revanced.extension.youtube.shared.RootView;
|
import app.revanced.extension.youtube.shared.RootView;
|
||||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
|
||||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||||
|
|
||||||
@ -441,7 +439,7 @@ public class PlayerPatch {
|
|||||||
if (isLiveChatOrPlaylistPanel) {
|
if (isLiveChatOrPlaylistPanel) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return isAutoPopupPanel && ShortsPlayerState.getCurrent().isClosed();
|
return isAutoPopupPanel && !RootView.isShortsActive();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -471,8 +469,8 @@ public class PlayerPatch {
|
|||||||
* Used in YouTube 20.05.46+.
|
* Used in YouTube 20.05.46+.
|
||||||
*/
|
*/
|
||||||
public static void disableAutoPlayerPopupPanels(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
public static void disableAutoPlayerPopupPanels(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||||
if (Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get() && newVideoStarted.compareAndSet(false, true)) {
|
if (Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get() && newVideoStarted.compareAndSet(false, true)) {
|
||||||
Utils.runOnMainThreadDelayed(() -> newVideoStarted.compareAndSet(true, false), 3000L);
|
Utils.runOnMainThreadDelayed(() -> newVideoStarted.compareAndSet(true, false), 3000L);
|
||||||
}
|
}
|
||||||
@ -518,6 +516,12 @@ public class PlayerPatch {
|
|||||||
return SPEED_OVERLAY_VALUE;
|
return SPEED_OVERLAY_VALUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static float speedOverlayRelativeValue(float original) {
|
||||||
|
return SPEED_OVERLAY_VALUE != 2.0f
|
||||||
|
? 0f
|
||||||
|
: original;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean hideChannelWatermark(boolean original) {
|
public static boolean hideChannelWatermark(boolean original) {
|
||||||
return !Settings.HIDE_CHANNEL_WATERMARK.get() && original;
|
return !Settings.HIDE_CHANNEL_WATERMARK.get() && original;
|
||||||
}
|
}
|
||||||
@ -540,6 +544,10 @@ public class PlayerPatch {
|
|||||||
return Settings.HIDE_FILMSTRIP_OVERLAY.get();
|
return Settings.HIDE_FILMSTRIP_OVERLAY.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean hideFilmstripOverlay(boolean original) {
|
||||||
|
return !Settings.HIDE_FILMSTRIP_OVERLAY.get() && original;
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean hideInfoCard(boolean original) {
|
public static boolean hideInfoCard(boolean original) {
|
||||||
return !Settings.HIDE_INFO_CARDS.get() && original;
|
return !Settings.HIDE_INFO_CARDS.get() && original;
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ public class SeekbarColorPatch {
|
|||||||
/**
|
/**
|
||||||
* Empty seekbar gradient, if hide seekbar in feed is enabled.
|
* Empty seekbar gradient, if hide seekbar in feed is enabled.
|
||||||
*/
|
*/
|
||||||
private static final int[] HIDDEN_SEEKBAR_GRADIENT_COLORS = { 0x0, 0x0 };
|
private static final int[] HIDDEN_SEEKBAR_GRADIENT_COLORS = {0x0, 0x0};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default YouTube seekbar color brightness.
|
* Default YouTube seekbar color brightness.
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package app.revanced.extension.youtube.patches.player.requests
|
package app.revanced.extension.youtube.patches.player.requests
|
||||||
|
|
||||||
import androidx.annotation.GuardedBy
|
import androidx.annotation.GuardedBy
|
||||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_VIDEO_ACTION_BUTTON
|
||||||
import app.revanced.extension.shared.requests.Requester
|
import app.revanced.extension.shared.requests.Requester
|
||||||
import app.revanced.extension.shared.utils.Logger
|
import app.revanced.extension.shared.utils.Logger
|
||||||
import app.revanced.extension.shared.utils.Utils
|
import app.revanced.extension.shared.utils.Utils
|
||||||
@ -20,10 +22,10 @@ import java.util.concurrent.TimeoutException
|
|||||||
|
|
||||||
class ActionButtonRequest private constructor(
|
class ActionButtonRequest private constructor(
|
||||||
private val videoId: String,
|
private val videoId: String,
|
||||||
private val playerHeaders: Map<String, String>,
|
private val requestHeader: Map<String, String>,
|
||||||
) {
|
) {
|
||||||
private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread {
|
private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread {
|
||||||
fetch(videoId, playerHeaders)
|
fetch(videoId, requestHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
val array: Array<ActionButton>
|
val array: Array<ActionButton>
|
||||||
@ -52,14 +54,6 @@ class ActionButtonRequest private constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
|
||||||
* TCP connection and HTTP read timeout.
|
|
||||||
*/
|
|
||||||
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
|
|
||||||
*/
|
|
||||||
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
|
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
|
||||||
|
|
||||||
@GuardedBy("itself")
|
@GuardedBy("itself")
|
||||||
@ -73,11 +67,11 @@ class ActionButtonRequest private constructor(
|
|||||||
})
|
})
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map<String, String>) {
|
fun fetchRequestIfNeeded(videoId: String, requestHeader: Map<String, String>) {
|
||||||
Objects.requireNonNull(videoId)
|
Objects.requireNonNull(videoId)
|
||||||
synchronized(cache) {
|
synchronized(cache) {
|
||||||
if (!cache.containsKey(videoId)) {
|
if (!cache.containsKey(videoId)) {
|
||||||
cache[videoId] = ActionButtonRequest(videoId, playerHeaders)
|
cache[videoId] = ActionButtonRequest(videoId, requestHeader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -93,43 +87,28 @@ class ActionButtonRequest private constructor(
|
|||||||
Logger.printInfo({ toastMessage }, ex)
|
Logger.printInfo({ toastMessage }, ex)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val REQUEST_HEADER_KEYS = arrayOf(
|
private fun sendRequest(videoId: String, requestHeader: Map<String, String>): JSONObject? {
|
||||||
"Authorization", // Available only to logged-in users.
|
|
||||||
"X-GOOG-API-FORMAT-VERSION",
|
|
||||||
"X-Goog-Visitor-Id"
|
|
||||||
)
|
|
||||||
|
|
||||||
private fun sendRequest(videoId: String, playerHeaders: Map<String, String>): JSONObject? {
|
|
||||||
Objects.requireNonNull(videoId)
|
Objects.requireNonNull(videoId)
|
||||||
|
|
||||||
val startTime = System.currentTimeMillis()
|
val startTime = System.currentTimeMillis()
|
||||||
// '/next' request does not require PoToken.
|
// '/next' endpoint does not require PoToken.
|
||||||
val clientType = YouTubeAppClient.ClientType.ANDROID
|
val clientType = YouTubeAppClient.ClientType.ANDROID
|
||||||
val clientTypeName = clientType.name
|
val clientTypeName = clientType.name
|
||||||
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
|
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
|
|
||||||
PlayerRoutes.GET_VIDEO_ACTION_BUTTON,
|
|
||||||
clientType
|
|
||||||
)
|
|
||||||
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
|
|
||||||
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
|
|
||||||
|
|
||||||
// Since [THANKS] button and [CLIP] button are shown only with the logged in,
|
// Since [THANKS] button and [CLIP] button are shown only with the logged in,
|
||||||
// Set the [Authorization] field to property to get the correct action buttons.
|
// Set the [Authorization] field to property to get the correct action buttons.
|
||||||
for (key in REQUEST_HEADER_KEYS) {
|
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||||
var value = playerHeaders[key]
|
GET_VIDEO_ACTION_BUTTON,
|
||||||
if (value != null) {
|
clientType,
|
||||||
connection.setRequestProperty(key, value)
|
requestHeader,
|
||||||
}
|
)
|
||||||
}
|
|
||||||
|
|
||||||
val requestBody =
|
val requestBody = createApplicationRequestBody(
|
||||||
PlayerRoutes.createApplicationRequestBody(
|
clientType = clientType,
|
||||||
clientType = clientType,
|
videoId = videoId
|
||||||
videoId = videoId
|
)
|
||||||
)
|
|
||||||
|
|
||||||
connection.setFixedLengthStreamingMode(requestBody.size)
|
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||||
connection.outputStream.write(requestBody)
|
connection.outputStream.write(requestBody)
|
||||||
@ -214,8 +193,11 @@ class ActionButtonRequest private constructor(
|
|||||||
return emptyArray()
|
return emptyArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetch(videoId: String, playerHeaders: Map<String, String>): Array<ActionButton> {
|
private fun fetch(
|
||||||
val json = sendRequest(videoId, playerHeaders)
|
videoId: String,
|
||||||
|
requestHeader: Map<String, String>
|
||||||
|
): Array<ActionButton> {
|
||||||
|
val json = sendRequest(videoId, requestHeader)
|
||||||
if (json != null) {
|
if (json != null) {
|
||||||
return parseResponse(json)
|
return parseResponse(json)
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,15 @@
|
|||||||
package app.revanced.extension.youtube.patches.shorts;
|
package app.revanced.extension.youtube.patches.shorts;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.utils.ResourceUtils.getString;
|
import static app.revanced.extension.shared.utils.ResourceUtils.getString;
|
||||||
import static app.revanced.extension.shared.utils.Utils.dpToPx;
|
|
||||||
import static app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter.isShortsFlyoutMenuVisible;
|
import static app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter.isShortsFlyoutMenuVisible;
|
||||||
|
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
|
||||||
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
|
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.ColorFilter;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.graphics.PorterDuffColorFilter;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.GradientDrawable;
|
|
||||||
import android.graphics.drawable.StateListDrawable;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.Window;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.ScrollView;
|
import android.widget.ScrollView;
|
||||||
@ -41,8 +31,7 @@ import app.revanced.extension.shared.utils.ResourceUtils;
|
|||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
import app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter;
|
import app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||||
import app.revanced.extension.youtube.utils.ThemeUtils;
|
|
||||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@ -66,7 +55,7 @@ public final class CustomActionsPatch {
|
|||||||
if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) {
|
if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ShortsPlayerState.getCurrent().isClosed()) {
|
if (!isShortsActive()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isMoreButton(enumString)) {
|
if (!isMoreButton(enumString)) {
|
||||||
@ -90,105 +79,28 @@ public final class CustomActionsPatch {
|
|||||||
}), 0);
|
}), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void showMoreButtonDialog(Context context) {
|
private static void showMoreButtonDialog(Context mContext) {
|
||||||
ScrollView scrollView = new ScrollView(context);
|
ScrollView mScrollView = new ScrollView(mContext);
|
||||||
LinearLayout container = new LinearLayout(context);
|
LinearLayout mLinearLayout = new LinearLayout(mContext);
|
||||||
|
mLinearLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
mLinearLayout.setPadding(0, 0, 0, 0);
|
||||||
|
|
||||||
container.setOrientation(LinearLayout.VERTICAL);
|
Map<LinearLayout, Runnable> actionsMap = new LinkedHashMap<>(arrSize);
|
||||||
container.setPadding(0, 0, 0, 0);
|
|
||||||
|
|
||||||
Map<LinearLayout, Runnable> toolbarMap = new LinkedHashMap<>(arrSize);
|
|
||||||
|
|
||||||
for (CustomAction customAction : CustomAction.values()) {
|
for (CustomAction customAction : CustomAction.values()) {
|
||||||
if (customAction.settings.get()) {
|
if (customAction.settings.get()) {
|
||||||
String title = customAction.getLabel();
|
String title = customAction.getLabel();
|
||||||
int iconId = customAction.getDrawableId();
|
int iconId = customAction.getDrawableId();
|
||||||
Runnable action = customAction.getOnClickAction();
|
Runnable action = customAction.getOnClickAction();
|
||||||
LinearLayout itemLayout = createItemLayout(context, title, iconId);
|
LinearLayout itemLayout = ExtendedUtils.createItemLayout(mContext, title, iconId);
|
||||||
toolbarMap.putIfAbsent(itemLayout, action);
|
actionsMap.putIfAbsent(itemLayout, action);
|
||||||
container.addView(itemLayout);
|
mLinearLayout.addView(itemLayout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollView.addView(container);
|
mScrollView.addView(mLinearLayout);
|
||||||
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
ExtendedUtils.showBottomSheetDialog(mContext, mScrollView, actionsMap);
|
||||||
builder.setView(scrollView);
|
|
||||||
|
|
||||||
AlertDialog dialog = builder.create();
|
|
||||||
dialog.show();
|
|
||||||
|
|
||||||
toolbarMap.forEach((view, action) ->
|
|
||||||
view.setOnClickListener(v -> {
|
|
||||||
action.run();
|
|
||||||
dialog.dismiss();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
toolbarMap.clear();
|
|
||||||
|
|
||||||
Window window = dialog.getWindow();
|
|
||||||
if (window == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// round corners
|
|
||||||
GradientDrawable dialogBackground = new GradientDrawable();
|
|
||||||
dialogBackground.setCornerRadius(32);
|
|
||||||
window.setBackgroundDrawable(dialogBackground);
|
|
||||||
|
|
||||||
// fit screen width
|
|
||||||
int dialogWidth = (int) (context.getResources().getDisplayMetrics().widthPixels * 0.95);
|
|
||||||
window.setLayout(dialogWidth, ViewGroup.LayoutParams.WRAP_CONTENT);
|
|
||||||
|
|
||||||
// move dialog to bottom
|
|
||||||
WindowManager.LayoutParams layoutParams = window.getAttributes();
|
|
||||||
layoutParams.gravity = Gravity.BOTTOM;
|
|
||||||
|
|
||||||
// adjust the vertical offset
|
|
||||||
layoutParams.y = dpToPx(5);
|
|
||||||
|
|
||||||
window.setAttributes(layoutParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static LinearLayout createItemLayout(Context context, String title, int iconId) {
|
|
||||||
// Item Layout
|
|
||||||
LinearLayout itemLayout = new LinearLayout(context);
|
|
||||||
itemLayout.setOrientation(LinearLayout.HORIZONTAL);
|
|
||||||
itemLayout.setPadding(dpToPx(16), dpToPx(12), dpToPx(16), dpToPx(12));
|
|
||||||
itemLayout.setGravity(Gravity.CENTER_VERTICAL);
|
|
||||||
itemLayout.setClickable(true);
|
|
||||||
itemLayout.setFocusable(true);
|
|
||||||
|
|
||||||
// Create a StateListDrawable for the background
|
|
||||||
StateListDrawable background = new StateListDrawable();
|
|
||||||
ColorDrawable pressedDrawable = new ColorDrawable(ThemeUtils.getPressedElementColor());
|
|
||||||
ColorDrawable defaultDrawable = new ColorDrawable(ThemeUtils.getBackgroundColor());
|
|
||||||
background.addState(new int[]{android.R.attr.state_pressed}, pressedDrawable);
|
|
||||||
background.addState(new int[]{}, defaultDrawable);
|
|
||||||
itemLayout.setBackground(background);
|
|
||||||
|
|
||||||
// Icon
|
|
||||||
ColorFilter cf = new PorterDuffColorFilter(ThemeUtils.getForegroundColor(), PorterDuff.Mode.SRC_ATOP);
|
|
||||||
ImageView iconView = new ImageView(context);
|
|
||||||
iconView.setImageResource(iconId);
|
|
||||||
iconView.setColorFilter(cf);
|
|
||||||
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(dpToPx(24), dpToPx(24));
|
|
||||||
iconParams.setMarginEnd(dpToPx(16));
|
|
||||||
iconView.setLayoutParams(iconParams);
|
|
||||||
itemLayout.addView(iconView);
|
|
||||||
|
|
||||||
// Text container
|
|
||||||
LinearLayout textContainer = new LinearLayout(context);
|
|
||||||
textContainer.setOrientation(LinearLayout.VERTICAL);
|
|
||||||
TextView titleView = new TextView(context);
|
|
||||||
titleView.setText(title);
|
|
||||||
titleView.setTextSize(16);
|
|
||||||
titleView.setTextColor(ThemeUtils.getForegroundColor());
|
|
||||||
textContainer.addView(titleView);
|
|
||||||
|
|
||||||
itemLayout.addView(textContainer);
|
|
||||||
|
|
||||||
return itemLayout;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean isMoreButton(String enumString) {
|
private static boolean isMoreButton(String enumString) {
|
||||||
@ -206,7 +118,7 @@ public final class CustomActionsPatch {
|
|||||||
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
|
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ShortsPlayerState.getCurrent().isClosed()) {
|
if (!isShortsActive()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (bottomSheetMenuObject == null) {
|
if (bottomSheetMenuObject == null) {
|
||||||
@ -224,7 +136,7 @@ public final class CustomActionsPatch {
|
|||||||
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
|
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ShortsPlayerState.getCurrent().isClosed()) {
|
if (!isShortsActive()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (CustomAction customAction : CustomAction.values()) {
|
for (CustomAction customAction : CustomAction.values()) {
|
||||||
@ -243,6 +155,34 @@ public final class CustomActionsPatch {
|
|||||||
Logger.printInfo(() -> customAction.name() + bottomSheetMenuClass + bottomSheetMenuList + bottomSheetMenuObject);
|
Logger.printInfo(() -> customAction.name() + bottomSheetMenuClass + bottomSheetMenuList + bottomSheetMenuObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean onBottomSheetMenuItemClick(View view) {
|
||||||
|
try {
|
||||||
|
if (view instanceof ViewGroup viewGroup) {
|
||||||
|
TextView textView = Utils.getChildView(viewGroup, v -> v instanceof TextView);
|
||||||
|
if (textView != null) {
|
||||||
|
String menuTitle = textView.getText().toString();
|
||||||
|
for (CustomAction customAction : CustomAction.values()) {
|
||||||
|
if (customAction.getLabel().equals(menuTitle)) {
|
||||||
|
View.OnLongClickListener onLongClick = customAction.getOnLongClickListener();
|
||||||
|
if (onLongClick != null) {
|
||||||
|
view.setOnLongClickListener(onLongClick);
|
||||||
|
}
|
||||||
|
customAction.getOnClickAction().run();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onBottomSheetMenuItemClick failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@ -252,7 +192,7 @@ public final class CustomActionsPatch {
|
|||||||
}
|
}
|
||||||
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
||||||
try {
|
try {
|
||||||
if (ShortsPlayerState.getCurrent().isClosed()) {
|
if (!isShortsActive()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
contextRef = new WeakReference<>(recyclerView.getContext());
|
contextRef = new WeakReference<>(recyclerView.getContext());
|
||||||
@ -267,8 +207,9 @@ public final class CustomActionsPatch {
|
|||||||
if (recyclerView.getChildAt(childCount - i - 1) instanceof ViewGroup parentViewGroup) {
|
if (recyclerView.getChildAt(childCount - i - 1) instanceof ViewGroup parentViewGroup) {
|
||||||
childCount = recyclerView.getChildCount();
|
childCount = recyclerView.getChildCount();
|
||||||
if (childCount > 3 && parentViewGroup.getChildAt(1) instanceof TextView textView) {
|
if (childCount > 3 && parentViewGroup.getChildAt(1) instanceof TextView textView) {
|
||||||
|
String menuTitle = textView.getText().toString();
|
||||||
for (CustomAction customAction : CustomAction.values()) {
|
for (CustomAction customAction : CustomAction.values()) {
|
||||||
if (customAction.getLabel().equals(textView.getText().toString())) {
|
if (customAction.getLabel().equals(menuTitle)) {
|
||||||
View.OnClickListener onClick = customAction.getOnClickListener();
|
View.OnClickListener onClick = customAction.getOnClickListener();
|
||||||
View.OnLongClickListener onLongClick = customAction.getOnLongClickListener();
|
View.OnLongClickListener onLongClick = customAction.getOnLongClickListener();
|
||||||
recyclerViewRef = new WeakReference<>(recyclerView);
|
recyclerViewRef = new WeakReference<>(recyclerView);
|
||||||
@ -384,6 +325,11 @@ public final class CustomActionsPatch {
|
|||||||
true
|
true
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
SPEED_DIALOG(
|
||||||
|
Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG,
|
||||||
|
"yt_outline_play_arrow_half_circle_black_24",
|
||||||
|
() -> VideoUtils.showPlaybackSpeedDialog(contextRef.get())
|
||||||
|
),
|
||||||
REPEAT_STATE(
|
REPEAT_STATE(
|
||||||
Settings.SHORTS_CUSTOM_ACTIONS_REPEAT_STATE,
|
Settings.SHORTS_CUSTOM_ACTIONS_REPEAT_STATE,
|
||||||
"yt_outline_arrow_repeat_1_black_24",
|
"yt_outline_arrow_repeat_1_black_24",
|
||||||
|
@ -2,6 +2,8 @@ package app.revanced.extension.youtube.patches.shorts;
|
|||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
@ -29,10 +31,15 @@ public class ShortsRepeatStatePatch {
|
|||||||
|
|
||||||
static void setYTEnumValue(Enum<?> ytBehavior) {
|
static void setYTEnumValue(Enum<?> ytBehavior) {
|
||||||
for (ShortsLoopBehavior rvBehavior : values()) {
|
for (ShortsLoopBehavior rvBehavior : values()) {
|
||||||
if (ytBehavior.name().endsWith(rvBehavior.name())) {
|
String ytName = ytBehavior.name();
|
||||||
rvBehavior.ytEnumValue = ytBehavior;
|
if (ytName.endsWith(rvBehavior.name())) {
|
||||||
|
if (rvBehavior.ytEnumValue != null) {
|
||||||
Logger.printDebug(() -> rvBehavior + " set to YT enum: " + ytBehavior.name());
|
Logger.printException(() -> "Conflicting behavior names: " + rvBehavior
|
||||||
|
+ " ytBehavior: " + ytName);
|
||||||
|
} else {
|
||||||
|
rvBehavior.ytEnumValue = ytBehavior;
|
||||||
|
Logger.printDebug(() -> rvBehavior + " set to YT enum: " + ytName);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,25 +84,39 @@ public class ShortsRepeatStatePatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static Enum<?> changeShortsRepeatBehavior(Enum<?> original) {
|
@Nullable
|
||||||
|
public static Enum<?> changeShortsRepeatBehavior(@Nullable Enum<?> original) {
|
||||||
try {
|
try {
|
||||||
final ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER &&
|
ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER &&
|
||||||
isAppInBackgroundPiPMode()
|
isAppInBackgroundPiPMode()
|
||||||
? Settings.CHANGE_SHORTS_BACKGROUND_REPEAT_STATE.get()
|
? Settings.CHANGE_SHORTS_BACKGROUND_REPEAT_STATE.get()
|
||||||
: Settings.CHANGE_SHORTS_REPEAT_STATE.get();
|
: Settings.CHANGE_SHORTS_REPEAT_STATE.get();
|
||||||
|
Enum<?> overrideBehavior = behavior.ytEnumValue;
|
||||||
|
|
||||||
if (behavior != ShortsLoopBehavior.UNKNOWN && behavior.ytEnumValue != null) {
|
if (behavior != ShortsLoopBehavior.UNKNOWN && overrideBehavior != null) {
|
||||||
Logger.printDebug(() -> behavior.ytEnumValue == original
|
Logger.printDebug(() -> {
|
||||||
? "Changing Shorts repeat behavior from: " + original.name() + " to: " + behavior.ytEnumValue
|
String name = original == null ? "unknown (null)" : original.name();
|
||||||
: "Behavior setting is same as original. Using original: " + original.name()
|
return overrideBehavior == original
|
||||||
);
|
? "Behavior setting is same as original. Using original: " + name
|
||||||
|
: "Changing Shorts repeat behavior from: " + name + " to: " + overrideBehavior.name();
|
||||||
|
});
|
||||||
|
|
||||||
return behavior.ytEnumValue;
|
// For some reason, in YouTube 20.09+, 'UNKNOWN' functions as 'Pause'.
|
||||||
|
return ExtendedUtils.IS_20_09_OR_GREATER && behavior == ShortsLoopBehavior.END_SCREEN
|
||||||
|
? ShortsLoopBehavior.UNKNOWN.ytEnumValue
|
||||||
|
: overrideBehavior;
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "changeShortsRepeatState failure", ex);
|
Logger.printException(() -> "changeShortsRepeatBehavior failure", ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
return original;
|
return original;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean isAutoPlay(@Nullable Enum<?> original) {
|
||||||
|
return ShortsLoopBehavior.SINGLE_PLAY.ytEnumValue == original;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import android.view.View;
|
|||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
@ -59,4 +60,20 @@ public class SwipeControlsPatch {
|
|||||||
return engagementOverlayView != null && engagementOverlayView.getVisibility() == View.VISIBLE;
|
return engagementOverlayView != null && engagementOverlayView.getVisibility() == View.VISIBLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static final class SwipeOverlayTextSizeAvailability implements Setting.Availability {
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return (Settings.ENABLE_SWIPE_BRIGHTNESS.get() || Settings.ENABLE_SWIPE_VOLUME.get()) &&
|
||||||
|
!Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class SwipeOverlayModernUIAvailability implements Setting.Availability {
|
||||||
|
@Override
|
||||||
|
public boolean isAvailable() {
|
||||||
|
return (Settings.ENABLE_SWIPE_BRIGHTNESS.get() || Settings.ENABLE_SWIPE_VOLUME.get()) &&
|
||||||
|
Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
package app.revanced.extension.youtube.patches.utils;
|
package app.revanced.extension.youtube.patches.utils;
|
||||||
|
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
|
import app.revanced.extension.youtube.shared.EngagementPanel;
|
||||||
import app.revanced.extension.youtube.shared.PlayerType;
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
|
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class PlaybackSpeedWhilePlayingPatch {
|
public class PlaybackSpeedWhilePlayingPatch {
|
||||||
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
||||||
|
|
||||||
public static boolean playbackSpeedChanged(float playbackSpeed) {
|
public static boolean playbackSpeedChanged(float playbackSpeed) {
|
||||||
PlayerType playerType = PlayerType.getCurrent();
|
if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED) {
|
||||||
if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED &&
|
if (PlayerType.getCurrent().isMaximizedOrFullscreenOrPiP()
|
||||||
playerType.isMaximizedOrFullscreenOrPiP()) {
|
// Since RVX has a default playback speed setting for Shorts,
|
||||||
|
// Playback speed reset should also be prevented in Shorts.
|
||||||
|
|| ShortsPlayerState.getCurrent().isOpen() && EngagementPanel.isOpen()) {
|
||||||
|
Logger.printDebug(() -> "Ignore changing playback speed, as it is invalid request");
|
||||||
|
|
||||||
Logger.printDebug(() -> "Ignore changing playback speed, as it is invalid request: " + playerType.name());
|
return true;
|
||||||
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
@ -0,0 +1,508 @@
|
|||||||
|
package app.revanced.extension.youtube.patches.utils;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||||
|
|
||||||
|
import org.apache.commons.collections4.BidiMap;
|
||||||
|
import org.apache.commons.collections4.bidimap.DualHashBidiMap;
|
||||||
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
|
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||||
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
|
import app.revanced.extension.youtube.patches.utils.requests.CreatePlaylistRequest;
|
||||||
|
import app.revanced.extension.youtube.patches.utils.requests.DeletePlaylistRequest;
|
||||||
|
import app.revanced.extension.youtube.patches.utils.requests.EditPlaylistRequest;
|
||||||
|
import app.revanced.extension.youtube.patches.utils.requests.GetPlaylistsRequest;
|
||||||
|
import app.revanced.extension.youtube.patches.utils.requests.SavePlaylistRequest;
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||||
|
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||||
|
import kotlin.Pair;
|
||||||
|
|
||||||
|
// TODO: Implement sync queue and clean up code.
|
||||||
|
@SuppressWarnings({"unused", "StaticFieldLeak"})
|
||||||
|
public class PlaylistPatch extends VideoUtils {
|
||||||
|
private static final String AUTHORIZATION_HEADER = "Authorization";
|
||||||
|
private static final String[] REQUEST_HEADER_KEYS = {
|
||||||
|
AUTHORIZATION_HEADER,
|
||||||
|
"X-GOOG-API-FORMAT-VERSION",
|
||||||
|
"X-Goog-Visitor-Id"
|
||||||
|
};
|
||||||
|
private static final boolean QUEUE_MANAGER =
|
||||||
|
Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER_QUEUE_MANAGER.get()
|
||||||
|
|| Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER.get();
|
||||||
|
|
||||||
|
private static Context mContext;
|
||||||
|
private static volatile String authorization = "";
|
||||||
|
public static volatile String dataSyncId = "";
|
||||||
|
public static volatile boolean isIncognito = false;
|
||||||
|
private static volatile Map<String, String> requestHeader;
|
||||||
|
private static volatile String playlistId = "";
|
||||||
|
private static volatile String videoId = "";
|
||||||
|
|
||||||
|
private static String checkFailedAuth;
|
||||||
|
private static String checkFailedPlaylistId;
|
||||||
|
private static String checkFailedQueue;
|
||||||
|
private static String checkFailedVideoId;
|
||||||
|
private static String checkFailedGeneric;
|
||||||
|
|
||||||
|
private static String fetchFailedAdd;
|
||||||
|
private static String fetchFailedCreate;
|
||||||
|
private static String fetchFailedDelete;
|
||||||
|
private static String fetchFailedRemove;
|
||||||
|
private static String fetchFailedSave;
|
||||||
|
|
||||||
|
private static String fetchSucceededAdd;
|
||||||
|
private static String fetchSucceededCreate;
|
||||||
|
private static String fetchSucceededDelete;
|
||||||
|
private static String fetchSucceededRemove;
|
||||||
|
private static String fetchSucceededSave;
|
||||||
|
|
||||||
|
static {
|
||||||
|
Context mContext = Utils.getContext();
|
||||||
|
if (mContext != null && mContext.getResources() != null) {
|
||||||
|
checkFailedAuth = str("revanced_queue_manager_check_failed_auth");
|
||||||
|
checkFailedPlaylistId = str("revanced_queue_manager_check_failed_playlist_id");
|
||||||
|
checkFailedQueue = str("revanced_queue_manager_check_failed_queue");
|
||||||
|
checkFailedVideoId = str("revanced_queue_manager_check_failed_video_id");
|
||||||
|
checkFailedGeneric = str("revanced_queue_manager_check_failed_generic");
|
||||||
|
|
||||||
|
fetchFailedAdd = str("revanced_queue_manager_fetch_failed_add");
|
||||||
|
fetchFailedCreate = str("revanced_queue_manager_fetch_failed_create");
|
||||||
|
fetchFailedDelete = str("revanced_queue_manager_fetch_failed_delete");
|
||||||
|
fetchFailedRemove = str("revanced_queue_manager_fetch_failed_remove");
|
||||||
|
fetchFailedSave = str("revanced_queue_manager_fetch_failed_save");
|
||||||
|
|
||||||
|
fetchSucceededAdd = str("revanced_queue_manager_fetch_succeeded_add");
|
||||||
|
fetchSucceededCreate = str("revanced_queue_manager_fetch_succeeded_create");
|
||||||
|
fetchSucceededDelete = str("revanced_queue_manager_fetch_succeeded_delete");
|
||||||
|
fetchSucceededRemove = str("revanced_queue_manager_fetch_succeeded_remove");
|
||||||
|
fetchSucceededSave = str("revanced_queue_manager_fetch_succeeded_save");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GuardedBy("itself")
|
||||||
|
private static final BidiMap<String, String> lastVideoIds = new DualHashBidiMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean onKeyLongPress(int keyCode) {
|
||||||
|
if (!QUEUE_MANAGER || keyCode != KeyEvent.KEYCODE_BACK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (mContext == null) {
|
||||||
|
handleCheckError(checkFailedQueue);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
prepareDialogBuilder("");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static void removeFromQueue(@Nullable String setVideoId) {
|
||||||
|
if (StringUtils.isNotEmpty(setVideoId)) {
|
||||||
|
synchronized (lastVideoIds) {
|
||||||
|
String videoId = lastVideoIds.inverseBidiMap().get(setVideoId);
|
||||||
|
if (videoId != null) {
|
||||||
|
lastVideoIds.remove(videoId, setVideoId);
|
||||||
|
EditPlaylistRequest.clearVideoId(videoId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static void setPivotBar(PivotBar view) {
|
||||||
|
if (QUEUE_MANAGER) {
|
||||||
|
mContext = view.getContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static void setRequestHeaders(String url, Map<String, String> requestHeaders) {
|
||||||
|
if (QUEUE_MANAGER) {
|
||||||
|
try {
|
||||||
|
// Save requestHeaders whenever an account is switched.
|
||||||
|
String auth = requestHeaders.get(AUTHORIZATION_HEADER);
|
||||||
|
if (auth == null || authorization.equals(auth)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (String key : REQUEST_HEADER_KEYS) {
|
||||||
|
if (requestHeaders.get(key) == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
authorization = auth;
|
||||||
|
requestHeader = requestHeaders;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "setRequestHeaders failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked by extension.
|
||||||
|
*/
|
||||||
|
public static void setContext(Context context) {
|
||||||
|
mContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invoked by extension.
|
||||||
|
*/
|
||||||
|
public static void prepareDialogBuilder(@NonNull String currentVideoId) {
|
||||||
|
if (authorization.isEmpty() || (dataSyncId.isEmpty() && isIncognito)) {
|
||||||
|
handleCheckError(checkFailedAuth);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentVideoId.isEmpty()) {
|
||||||
|
buildBottomSheetDialog(QueueManager.noVideoIdQueueEntries);
|
||||||
|
} else {
|
||||||
|
videoId = currentVideoId;
|
||||||
|
synchronized (lastVideoIds) {
|
||||||
|
QueueManager[] customActionsEntries = playlistId.isEmpty() || lastVideoIds.get(currentVideoId) == null
|
||||||
|
? QueueManager.addToQueueEntries
|
||||||
|
: QueueManager.removeFromQueueEntries;
|
||||||
|
|
||||||
|
buildBottomSheetDialog(customActionsEntries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void buildBottomSheetDialog(QueueManager[] queueManagerEntries) {
|
||||||
|
ScrollView mScrollView = new ScrollView(mContext);
|
||||||
|
LinearLayout mLinearLayout = new LinearLayout(mContext);
|
||||||
|
mLinearLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
mLinearLayout.setPadding(0, 0, 0, 0);
|
||||||
|
|
||||||
|
Map<LinearLayout, Runnable> actionsMap = new LinkedHashMap<>(queueManagerEntries.length);
|
||||||
|
|
||||||
|
for (QueueManager queueManager : queueManagerEntries) {
|
||||||
|
String title = queueManager.label;
|
||||||
|
int iconId = queueManager.drawableId;
|
||||||
|
Runnable action = queueManager.onClickAction;
|
||||||
|
LinearLayout itemLayout = ExtendedUtils.createItemLayout(mContext, title, iconId);
|
||||||
|
actionsMap.putIfAbsent(itemLayout, action);
|
||||||
|
mLinearLayout.addView(itemLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
mScrollView.addView(mLinearLayout);
|
||||||
|
|
||||||
|
ExtendedUtils.showBottomSheetDialog(mContext, mScrollView, actionsMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void fetchQueue(boolean remove, boolean openPlaylist, boolean openVideo) {
|
||||||
|
try {
|
||||||
|
String currentPlaylistId = playlistId;
|
||||||
|
String currentVideoId = videoId;
|
||||||
|
synchronized (lastVideoIds) {
|
||||||
|
if (currentPlaylistId.isEmpty()) { // Queue is empty, create new playlist.
|
||||||
|
CreatePlaylistRequest.fetchRequestIfNeeded(currentVideoId, requestHeader, dataSyncId);
|
||||||
|
runOnMainThreadDelayed(() -> {
|
||||||
|
CreatePlaylistRequest request = CreatePlaylistRequest.getRequestForVideoId(currentVideoId);
|
||||||
|
if (request != null) {
|
||||||
|
Pair<String, String> playlistIds = request.getPlaylistId();
|
||||||
|
if (playlistIds != null) {
|
||||||
|
String createdPlaylistId = playlistIds.getFirst();
|
||||||
|
String setVideoId = playlistIds.getSecond();
|
||||||
|
if (createdPlaylistId != null && setVideoId != null) {
|
||||||
|
playlistId = createdPlaylistId;
|
||||||
|
lastVideoIds.putIfAbsent(currentVideoId, setVideoId);
|
||||||
|
showToast(fetchSucceededCreate);
|
||||||
|
Logger.printDebug(() -> "Queue successfully created, playlistId: " + createdPlaylistId + ", setVideoId: " + setVideoId);
|
||||||
|
if (openPlaylist) {
|
||||||
|
openQueue(currentVideoId, openVideo);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast(fetchFailedCreate);
|
||||||
|
}, 1000);
|
||||||
|
} else { // Queue is not empty, add or remove video.
|
||||||
|
String setVideoId = lastVideoIds.get(currentVideoId);
|
||||||
|
EditPlaylistRequest.fetchRequestIfNeeded(currentVideoId, currentPlaylistId, setVideoId, requestHeader, dataSyncId);
|
||||||
|
|
||||||
|
runOnMainThreadDelayed(() -> {
|
||||||
|
EditPlaylistRequest request = EditPlaylistRequest.getRequestForVideoId(currentVideoId);
|
||||||
|
if (request != null) {
|
||||||
|
String fetchedSetVideoId = request.getResult();
|
||||||
|
Logger.printDebug(() -> "fetchedSetVideoId: " + fetchedSetVideoId);
|
||||||
|
if (remove) { // Remove from queue.
|
||||||
|
if (StringUtils.isEmpty(fetchedSetVideoId)) {
|
||||||
|
lastVideoIds.remove(currentVideoId, setVideoId);
|
||||||
|
showToast(fetchSucceededRemove);
|
||||||
|
if (openPlaylist) {
|
||||||
|
openQueue(currentVideoId, openVideo);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast(fetchFailedRemove);
|
||||||
|
} else { // Add to queue.
|
||||||
|
if (StringUtils.isNotEmpty(fetchedSetVideoId)) {
|
||||||
|
lastVideoIds.putIfAbsent(currentVideoId, fetchedSetVideoId);
|
||||||
|
showToast(fetchSucceededAdd);
|
||||||
|
Logger.printDebug(() -> "Video successfully added, setVideoId: " + fetchedSetVideoId);
|
||||||
|
if (openPlaylist) {
|
||||||
|
openQueue(currentVideoId, openVideo);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast(fetchFailedAdd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "fetchQueue failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveToPlaylist() {
|
||||||
|
String currentPlaylistId = playlistId;
|
||||||
|
if (currentPlaylistId.isEmpty()) {
|
||||||
|
handleCheckError(checkFailedQueue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
GetPlaylistsRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader, dataSyncId);
|
||||||
|
runOnMainThreadDelayed(() -> {
|
||||||
|
GetPlaylistsRequest request = GetPlaylistsRequest.getRequestForPlaylistId(currentPlaylistId);
|
||||||
|
if (request != null) {
|
||||||
|
Pair<String, String>[] playlists = request.getPlaylists();
|
||||||
|
if (playlists != null) {
|
||||||
|
ScrollView mScrollView = new ScrollView(mContext);
|
||||||
|
LinearLayout mLinearLayout = new LinearLayout(mContext);
|
||||||
|
mLinearLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
mLinearLayout.setPadding(0, 0, 0, 0);
|
||||||
|
|
||||||
|
Map<LinearLayout, Runnable> actionsMap = new LinkedHashMap<>(playlists.length);
|
||||||
|
|
||||||
|
int libraryIconId = QueueManager.SAVE_QUEUE.drawableId;
|
||||||
|
|
||||||
|
for (Pair<String, String> playlist : playlists) {
|
||||||
|
String playlistId = playlist.getFirst();
|
||||||
|
String title = playlist.getSecond();
|
||||||
|
Runnable action = () -> saveToPlaylist(playlistId, title);
|
||||||
|
LinearLayout itemLayout = ExtendedUtils.createItemLayout(mContext, title, libraryIconId);
|
||||||
|
actionsMap.putIfAbsent(itemLayout, action);
|
||||||
|
mLinearLayout.addView(itemLayout);
|
||||||
|
}
|
||||||
|
|
||||||
|
mScrollView.addView(mLinearLayout);
|
||||||
|
|
||||||
|
ExtendedUtils.showBottomSheetDialog(mContext, mScrollView, actionsMap);
|
||||||
|
GetPlaylistsRequest.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "saveToPlaylist failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void saveToPlaylist(@Nullable String libraryId, @Nullable String libraryTitle) {
|
||||||
|
try {
|
||||||
|
if (StringUtils.isEmpty(libraryId)) {
|
||||||
|
handleCheckError(checkFailedPlaylistId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
SavePlaylistRequest.fetchRequestIfNeeded(playlistId, libraryId, requestHeader, dataSyncId);
|
||||||
|
|
||||||
|
runOnMainThreadDelayed(() -> {
|
||||||
|
SavePlaylistRequest request = SavePlaylistRequest.getRequestForLibraryId(libraryId);
|
||||||
|
if (request != null) {
|
||||||
|
Boolean result = request.getResult();
|
||||||
|
if (BooleanUtils.isTrue(result)) {
|
||||||
|
showToast(String.format(fetchSucceededSave, libraryTitle));
|
||||||
|
SavePlaylistRequest.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast(fetchFailedSave);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "saveToPlaylist failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void removeQueue() {
|
||||||
|
String currentPlaylistId = playlistId;
|
||||||
|
if (currentPlaylistId.isEmpty()) {
|
||||||
|
handleCheckError(checkFailedQueue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
DeletePlaylistRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader, dataSyncId);
|
||||||
|
runOnMainThreadDelayed(() -> {
|
||||||
|
DeletePlaylistRequest request = DeletePlaylistRequest.getRequestForPlaylistId(currentPlaylistId);
|
||||||
|
if (request != null) {
|
||||||
|
Boolean result = request.getResult();
|
||||||
|
if (BooleanUtils.isTrue(result)) {
|
||||||
|
playlistId = "";
|
||||||
|
synchronized (lastVideoIds) {
|
||||||
|
lastVideoIds.clear();
|
||||||
|
}
|
||||||
|
CreatePlaylistRequest.clear();
|
||||||
|
DeletePlaylistRequest.clear();
|
||||||
|
EditPlaylistRequest.clear();
|
||||||
|
GetPlaylistsRequest.clear();
|
||||||
|
SavePlaylistRequest.clear();
|
||||||
|
showToast(fetchSucceededDelete);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showToast(fetchFailedDelete);
|
||||||
|
}, 1000);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "removeQueue failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void downloadVideo() {
|
||||||
|
String currentVideoId = videoId;
|
||||||
|
launchVideoExternalDownloader(currentVideoId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void openQueue() {
|
||||||
|
openQueue("", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void openQueue(String currentVideoId, boolean openVideo) {
|
||||||
|
String currentPlaylistId = playlistId;
|
||||||
|
if (currentPlaylistId.isEmpty()) {
|
||||||
|
handleCheckError(checkFailedQueue);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (openVideo) {
|
||||||
|
if (StringUtils.isEmpty(currentVideoId)) {
|
||||||
|
handleCheckError(checkFailedVideoId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Open a video from a playlist
|
||||||
|
openPlaylist(currentPlaylistId, currentVideoId);
|
||||||
|
} else {
|
||||||
|
// Open a playlist
|
||||||
|
openPlaylist(currentPlaylistId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void handleCheckError(String reason) {
|
||||||
|
showToast(String.format(checkFailedGeneric, reason));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void showToast(String reason) {
|
||||||
|
Utils.showToastShort(reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum QueueManager {
|
||||||
|
ADD_TO_QUEUE(
|
||||||
|
"revanced_queue_manager_add_to_queue",
|
||||||
|
"yt_outline_list_add_black_24",
|
||||||
|
() -> fetchQueue(false, false, false)
|
||||||
|
),
|
||||||
|
ADD_TO_QUEUE_AND_OPEN_QUEUE(
|
||||||
|
"revanced_queue_manager_add_to_queue_and_open_queue",
|
||||||
|
"yt_outline_list_add_black_24",
|
||||||
|
() -> fetchQueue(false, true, false)
|
||||||
|
),
|
||||||
|
ADD_TO_QUEUE_AND_PLAY_VIDEO(
|
||||||
|
"revanced_queue_manager_add_to_queue_and_play_video",
|
||||||
|
"yt_outline_list_play_arrow_black_24",
|
||||||
|
() -> fetchQueue(false, true, true)
|
||||||
|
),
|
||||||
|
REMOVE_FROM_QUEUE(
|
||||||
|
"revanced_queue_manager_remove_from_queue",
|
||||||
|
"yt_outline_trash_can_black_24",
|
||||||
|
() -> fetchQueue(true, false, false)
|
||||||
|
),
|
||||||
|
REMOVE_FROM_QUEUE_AND_OPEN_QUEUE(
|
||||||
|
"revanced_queue_manager_remove_from_queue_and_open_queue",
|
||||||
|
"yt_outline_trash_can_black_24",
|
||||||
|
() -> fetchQueue(true, true, false)
|
||||||
|
),
|
||||||
|
OPEN_QUEUE(
|
||||||
|
"revanced_queue_manager_open_queue",
|
||||||
|
"yt_outline_list_view_black_24",
|
||||||
|
PlaylistPatch::openQueue
|
||||||
|
),
|
||||||
|
// For some reason, the 'playlist/delete' endpoint is unavailable.
|
||||||
|
REMOVE_QUEUE(
|
||||||
|
"revanced_queue_manager_remove_queue",
|
||||||
|
"yt_outline_slash_circle_left_black_24",
|
||||||
|
PlaylistPatch::removeQueue
|
||||||
|
),
|
||||||
|
SAVE_QUEUE(
|
||||||
|
"revanced_queue_manager_save_queue",
|
||||||
|
"yt_outline_bookmark_black_24",
|
||||||
|
PlaylistPatch::saveToPlaylist
|
||||||
|
),
|
||||||
|
EXTERNAL_DOWNLOADER(
|
||||||
|
"revanced_queue_manager_external_downloader",
|
||||||
|
"yt_outline_download_black_24",
|
||||||
|
PlaylistPatch::downloadVideo
|
||||||
|
);
|
||||||
|
|
||||||
|
public final int drawableId;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final String label;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Runnable onClickAction;
|
||||||
|
|
||||||
|
QueueManager(@NonNull String label, @NonNull String icon, @NonNull Runnable onClickAction) {
|
||||||
|
this.drawableId = ResourceUtils.getDrawableIdentifier(icon);
|
||||||
|
this.label = ResourceUtils.getString(label);
|
||||||
|
this.onClickAction = onClickAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final QueueManager[] addToQueueEntries = {
|
||||||
|
ADD_TO_QUEUE,
|
||||||
|
ADD_TO_QUEUE_AND_OPEN_QUEUE,
|
||||||
|
ADD_TO_QUEUE_AND_PLAY_VIDEO,
|
||||||
|
OPEN_QUEUE,
|
||||||
|
//REMOVE_QUEUE,
|
||||||
|
EXTERNAL_DOWNLOADER,
|
||||||
|
SAVE_QUEUE,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final QueueManager[] removeFromQueueEntries = {
|
||||||
|
REMOVE_FROM_QUEUE,
|
||||||
|
REMOVE_FROM_QUEUE_AND_OPEN_QUEUE,
|
||||||
|
OPEN_QUEUE,
|
||||||
|
//REMOVE_QUEUE,
|
||||||
|
EXTERNAL_DOWNLOADER,
|
||||||
|
SAVE_QUEUE,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final QueueManager[] noVideoIdQueueEntries = {
|
||||||
|
OPEN_QUEUE,
|
||||||
|
//REMOVE_QUEUE,
|
||||||
|
SAVE_QUEUE,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,281 @@
|
|||||||
|
package app.revanced.extension.youtube.patches.utils.requests
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy
|
||||||
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createPlaylistRequestBody
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.CREATE_PLAYLIST
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_SET_VIDEO_ID
|
||||||
|
import app.revanced.extension.shared.requests.Requester
|
||||||
|
import app.revanced.extension.shared.utils.Logger
|
||||||
|
import app.revanced.extension.shared.utils.Utils
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.Objects
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
|
import java.util.concurrent.Future
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
class CreatePlaylistRequest private constructor(
|
||||||
|
private val videoId: String,
|
||||||
|
private val requestHeader: Map<String, String>,
|
||||||
|
private val dataSyncId: String,
|
||||||
|
) {
|
||||||
|
private val future: Future<Pair<String, String>> = Utils.submitOnBackgroundThread {
|
||||||
|
fetch(
|
||||||
|
videoId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val playlistId: Pair<String, String>?
|
||||||
|
get() {
|
||||||
|
try {
|
||||||
|
return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS]
|
||||||
|
} catch (ex: TimeoutException) {
|
||||||
|
Logger.printInfo(
|
||||||
|
{ "getPlaylistId timed out" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
} catch (ex: InterruptedException) {
|
||||||
|
Logger.printException(
|
||||||
|
{ "getPlaylistId interrupted" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
Thread.currentThread().interrupt() // Restore interrupt status flag.
|
||||||
|
} catch (ex: ExecutionException) {
|
||||||
|
Logger.printException(
|
||||||
|
{ "getPlaylistId failure" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
|
||||||
|
|
||||||
|
@GuardedBy("itself")
|
||||||
|
val cache: MutableMap<String, CreatePlaylistRequest> = Collections.synchronizedMap(
|
||||||
|
object : LinkedHashMap<String, CreatePlaylistRequest>(100) {
|
||||||
|
private val CACHE_LIMIT = 50
|
||||||
|
|
||||||
|
override fun removeEldestEntry(eldest: Map.Entry<String, CreatePlaylistRequest>): Boolean {
|
||||||
|
return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun clear() {
|
||||||
|
synchronized(cache) {
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun fetchRequestIfNeeded(
|
||||||
|
videoId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(videoId)
|
||||||
|
synchronized(cache) {
|
||||||
|
if (!cache.containsKey(videoId)) {
|
||||||
|
cache[videoId] = CreatePlaylistRequest(
|
||||||
|
videoId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getRequestForVideoId(videoId: String): CreatePlaylistRequest? {
|
||||||
|
synchronized(cache) {
|
||||||
|
return cache[videoId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleConnectionError(toastMessage: String, ex: Exception?) {
|
||||||
|
Logger.printInfo({ toastMessage }, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendCreatePlaylistRequest(
|
||||||
|
videoId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
): JSONObject? {
|
||||||
|
Objects.requireNonNull(videoId)
|
||||||
|
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
// 'playlist/create' endpoint does not require PoToken.
|
||||||
|
val clientType = YouTubeAppClient.ClientType.ANDROID
|
||||||
|
val clientTypeName = clientType.name
|
||||||
|
Logger.printDebug { "Fetching create playlist request for: $videoId, using client: $clientTypeName" }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||||
|
CREATE_PLAYLIST,
|
||||||
|
clientType,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
|
||||||
|
val requestBody = createPlaylistRequestBody(videoId = videoId)
|
||||||
|
|
||||||
|
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||||
|
connection.outputStream.write(requestBody)
|
||||||
|
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
if (responseCode == 200) return Requester.parseJSONObject(connection)
|
||||||
|
|
||||||
|
handleConnectionError(
|
||||||
|
(clientTypeName + " not available with response code: "
|
||||||
|
+ responseCode + " message: " + connection.responseMessage),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} catch (ex: SocketTimeoutException) {
|
||||||
|
handleConnectionError("Connection timeout", ex)
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
handleConnectionError("Network error", ex)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.printException({ "sendCreatePlaylistRequest failed" }, ex)
|
||||||
|
} finally {
|
||||||
|
Logger.printDebug { "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendSetVideoIdRequest(
|
||||||
|
videoId: String,
|
||||||
|
playlistId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
): JSONObject? {
|
||||||
|
Objects.requireNonNull(playlistId)
|
||||||
|
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
// 'playlist/create' endpoint does not require PoToken.
|
||||||
|
val clientType = YouTubeAppClient.ClientType.ANDROID
|
||||||
|
val clientTypeName = clientType.name
|
||||||
|
Logger.printDebug { "Fetching set video id request for: $playlistId, using client: $clientTypeName" }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||||
|
GET_SET_VIDEO_ID,
|
||||||
|
clientType,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
|
||||||
|
val requestBody = createApplicationRequestBody(
|
||||||
|
clientType = clientType,
|
||||||
|
videoId = videoId,
|
||||||
|
playlistId = playlistId
|
||||||
|
)
|
||||||
|
|
||||||
|
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||||
|
connection.outputStream.write(requestBody)
|
||||||
|
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
if (responseCode == 200) return Requester.parseJSONObject(connection)
|
||||||
|
|
||||||
|
handleConnectionError(
|
||||||
|
(clientTypeName + " not available with response code: "
|
||||||
|
+ responseCode + " message: " + connection.responseMessage),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} catch (ex: SocketTimeoutException) {
|
||||||
|
handleConnectionError("Connection timeout", ex)
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
handleConnectionError("Network error", ex)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.printException({ "sendSetVideoIdRequest failed" }, ex)
|
||||||
|
} finally {
|
||||||
|
Logger.printDebug { "playlist: " + playlistId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseCreatePlaylistResponse(json: JSONObject): String? {
|
||||||
|
try {
|
||||||
|
return json.getString("playlistId")
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
val jsonForMessage = json.toString()
|
||||||
|
Logger.printException(
|
||||||
|
{ "Fetch failed while processing response data for response: $jsonForMessage" },
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseSetVideoIdResponse(json: JSONObject): String? {
|
||||||
|
try {
|
||||||
|
val secondaryContentsJsonObject =
|
||||||
|
json.getJSONObject("contents")
|
||||||
|
.getJSONObject("singleColumnWatchNextResults")
|
||||||
|
.getJSONObject("playlist")
|
||||||
|
.getJSONObject("playlist")
|
||||||
|
.getJSONArray("contents")
|
||||||
|
.get(0)
|
||||||
|
|
||||||
|
if (secondaryContentsJsonObject is JSONObject) {
|
||||||
|
return secondaryContentsJsonObject
|
||||||
|
.getJSONObject("playlistPanelVideoRenderer")
|
||||||
|
.getString("playlistSetVideoId")
|
||||||
|
}
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
val jsonForMessage = json.toString()
|
||||||
|
Logger.printException(
|
||||||
|
{ "Fetch failed while processing response data for response: $jsonForMessage" },
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetch(
|
||||||
|
videoId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
): Pair<String, String>? {
|
||||||
|
val createPlaylistJson = sendCreatePlaylistRequest(
|
||||||
|
videoId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId
|
||||||
|
)
|
||||||
|
if (createPlaylistJson != null) {
|
||||||
|
val playlistId = parseCreatePlaylistResponse(createPlaylistJson)
|
||||||
|
if (playlistId != null) {
|
||||||
|
val setVideoIdJson = sendSetVideoIdRequest(
|
||||||
|
videoId,
|
||||||
|
playlistId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId
|
||||||
|
)
|
||||||
|
if (setVideoIdJson != null) {
|
||||||
|
val setVideoId = parseSetVideoIdResponse(setVideoIdJson)
|
||||||
|
if (setVideoId != null) {
|
||||||
|
return Pair(playlistId, setVideoId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,187 @@
|
|||||||
|
package app.revanced.extension.youtube.patches.utils.requests
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy
|
||||||
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.deletePlaylistRequestBody
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.DELETE_PLAYLIST
|
||||||
|
import app.revanced.extension.shared.requests.Requester
|
||||||
|
import app.revanced.extension.shared.utils.Logger
|
||||||
|
import app.revanced.extension.shared.utils.Utils
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.Objects
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
|
import java.util.concurrent.Future
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
class DeletePlaylistRequest private constructor(
|
||||||
|
private val playlistId: String,
|
||||||
|
private val requestHeader: Map<String, String>,
|
||||||
|
private val dataSyncId: String,
|
||||||
|
) {
|
||||||
|
private val future: Future<Boolean> = Utils.submitOnBackgroundThread {
|
||||||
|
fetch(
|
||||||
|
playlistId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result: Boolean?
|
||||||
|
get() {
|
||||||
|
try {
|
||||||
|
return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS]
|
||||||
|
} catch (ex: TimeoutException) {
|
||||||
|
Logger.printInfo(
|
||||||
|
{ "getResult timed out" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
} catch (ex: InterruptedException) {
|
||||||
|
Logger.printException(
|
||||||
|
{ "getResult interrupted" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
Thread.currentThread().interrupt() // Restore interrupt status flag.
|
||||||
|
} catch (ex: ExecutionException) {
|
||||||
|
Logger.printException(
|
||||||
|
{ "getResult failure" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
|
||||||
|
|
||||||
|
@GuardedBy("itself")
|
||||||
|
val cache: MutableMap<String, DeletePlaylistRequest> = Collections.synchronizedMap(
|
||||||
|
object : LinkedHashMap<String, DeletePlaylistRequest>(100) {
|
||||||
|
private val CACHE_LIMIT = 50
|
||||||
|
|
||||||
|
override fun removeEldestEntry(eldest: Map.Entry<String, DeletePlaylistRequest>): Boolean {
|
||||||
|
return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun clear() {
|
||||||
|
synchronized(cache) {
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun fetchRequestIfNeeded(
|
||||||
|
playlistId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(playlistId)
|
||||||
|
synchronized(cache) {
|
||||||
|
if (!cache.containsKey(playlistId)) {
|
||||||
|
cache[playlistId] = DeletePlaylistRequest(
|
||||||
|
playlistId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getRequestForPlaylistId(playlistId: String): DeletePlaylistRequest? {
|
||||||
|
synchronized(cache) {
|
||||||
|
return cache[playlistId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleConnectionError(toastMessage: String, ex: Exception?) {
|
||||||
|
Logger.printInfo({ toastMessage }, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendRequest(
|
||||||
|
playlistId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
): JSONObject? {
|
||||||
|
Objects.requireNonNull(playlistId)
|
||||||
|
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
// 'playlist/delete' endpoint does not require PoToken.
|
||||||
|
val clientType = YouTubeAppClient.ClientType.ANDROID
|
||||||
|
val clientTypeName = clientType.name
|
||||||
|
Logger.printDebug { "Fetching delete playlist request, playlistId: $playlistId, using client: $clientTypeName" }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||||
|
DELETE_PLAYLIST,
|
||||||
|
clientType,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId
|
||||||
|
)
|
||||||
|
|
||||||
|
val requestBody = deletePlaylistRequestBody(playlistId)
|
||||||
|
|
||||||
|
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||||
|
connection.outputStream.write(requestBody)
|
||||||
|
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
if (responseCode == 200) return Requester.parseJSONObject(connection)
|
||||||
|
|
||||||
|
handleConnectionError(
|
||||||
|
(clientTypeName + " not available with response code: "
|
||||||
|
+ responseCode + " message: " + connection.responseMessage),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} catch (ex: SocketTimeoutException) {
|
||||||
|
handleConnectionError("Connection timeout", ex)
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
handleConnectionError("Network error", ex)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.printException({ "sendRequest failed" }, ex)
|
||||||
|
} finally {
|
||||||
|
Logger.printDebug { "playlist: " + playlistId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseResponse(json: JSONObject): Boolean? {
|
||||||
|
try {
|
||||||
|
return json.has("command")
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
val jsonForMessage = json.toString()
|
||||||
|
Logger.printException(
|
||||||
|
{ "Fetch failed while processing response data for response: $jsonForMessage" },
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetch(
|
||||||
|
playlistId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
): Boolean? {
|
||||||
|
val json = sendRequest(
|
||||||
|
playlistId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
if (json != null) {
|
||||||
|
return parseResponse(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,225 @@
|
|||||||
|
package app.revanced.extension.youtube.patches.utils.requests
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy
|
||||||
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.editPlaylistRequestBody
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.EDIT_PLAYLIST
|
||||||
|
import app.revanced.extension.shared.requests.Requester
|
||||||
|
import app.revanced.extension.shared.utils.Logger
|
||||||
|
import app.revanced.extension.shared.utils.Utils
|
||||||
|
import org.apache.commons.lang3.StringUtils
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.Objects
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
|
import java.util.concurrent.Future
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
class EditPlaylistRequest private constructor(
|
||||||
|
private val videoId: String,
|
||||||
|
private val playlistId: String,
|
||||||
|
private val setVideoId: String?,
|
||||||
|
private val requestHeader: Map<String, String>,
|
||||||
|
private val dataSyncId: String,
|
||||||
|
) {
|
||||||
|
private val future: Future<String> = Utils.submitOnBackgroundThread {
|
||||||
|
fetch(
|
||||||
|
videoId,
|
||||||
|
playlistId,
|
||||||
|
setVideoId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result: String?
|
||||||
|
get() {
|
||||||
|
try {
|
||||||
|
return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS]
|
||||||
|
} catch (ex: TimeoutException) {
|
||||||
|
Logger.printInfo(
|
||||||
|
{ "getResult timed out" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
} catch (ex: InterruptedException) {
|
||||||
|
Logger.printException(
|
||||||
|
{ "getResult interrupted" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
Thread.currentThread().interrupt() // Restore interrupt status flag.
|
||||||
|
} catch (ex: ExecutionException) {
|
||||||
|
Logger.printException(
|
||||||
|
{ "getResult failure" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
|
||||||
|
|
||||||
|
@GuardedBy("itself")
|
||||||
|
val cache: MutableMap<String, EditPlaylistRequest> = Collections.synchronizedMap(
|
||||||
|
object : LinkedHashMap<String, EditPlaylistRequest>(100) {
|
||||||
|
private val CACHE_LIMIT = 50
|
||||||
|
|
||||||
|
override fun removeEldestEntry(eldest: Map.Entry<String, EditPlaylistRequest>): Boolean {
|
||||||
|
return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun clear() {
|
||||||
|
synchronized(cache) {
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun clearVideoId(videoId: String) {
|
||||||
|
synchronized(cache) {
|
||||||
|
cache.remove(videoId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun fetchRequestIfNeeded(
|
||||||
|
videoId: String,
|
||||||
|
playlistId: String,
|
||||||
|
setVideoId: String?,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(videoId)
|
||||||
|
synchronized(cache) {
|
||||||
|
if (!cache.containsKey(videoId)) {
|
||||||
|
cache[videoId] = EditPlaylistRequest(
|
||||||
|
videoId,
|
||||||
|
playlistId,
|
||||||
|
setVideoId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getRequestForVideoId(videoId: String): EditPlaylistRequest? {
|
||||||
|
synchronized(cache) {
|
||||||
|
return cache[videoId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleConnectionError(toastMessage: String, ex: Exception?) {
|
||||||
|
Logger.printInfo({ toastMessage }, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendRequest(
|
||||||
|
videoId: String,
|
||||||
|
playlistId: String,
|
||||||
|
setVideoId: String?,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
): JSONObject? {
|
||||||
|
Objects.requireNonNull(videoId)
|
||||||
|
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
// 'browse/edit_playlist' endpoint does not require PoToken.
|
||||||
|
val clientType = YouTubeAppClient.ClientType.ANDROID
|
||||||
|
val clientTypeName = clientType.name
|
||||||
|
Logger.printDebug { "Fetching edit playlist request, videoId: $videoId, playlistId: $playlistId, setVideoId: $setVideoId, using client: $clientTypeName" }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||||
|
EDIT_PLAYLIST,
|
||||||
|
clientType,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId
|
||||||
|
)
|
||||||
|
|
||||||
|
val requestBody = editPlaylistRequestBody(
|
||||||
|
videoId = videoId,
|
||||||
|
playlistId = playlistId,
|
||||||
|
setVideoId = setVideoId
|
||||||
|
)
|
||||||
|
|
||||||
|
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||||
|
connection.outputStream.write(requestBody)
|
||||||
|
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
if (responseCode == 200) return Requester.parseJSONObject(connection)
|
||||||
|
|
||||||
|
handleConnectionError(
|
||||||
|
(clientTypeName + " not available with response code: "
|
||||||
|
+ responseCode + " message: " + connection.responseMessage),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} catch (ex: SocketTimeoutException) {
|
||||||
|
handleConnectionError("Connection timeout", ex)
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
handleConnectionError("Network error", ex)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.printException({ "sendRequest failed" }, ex)
|
||||||
|
} finally {
|
||||||
|
Logger.printDebug { "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseResponse(json: JSONObject, remove: Boolean): String? {
|
||||||
|
try {
|
||||||
|
if (json.getString("status") == "STATUS_SUCCEEDED") {
|
||||||
|
if (remove) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
val playlistEditResultsJSONObject =
|
||||||
|
json.getJSONArray("playlistEditResults").get(0)
|
||||||
|
|
||||||
|
if (playlistEditResultsJSONObject is JSONObject) {
|
||||||
|
return playlistEditResultsJSONObject
|
||||||
|
.getJSONObject("playlistEditVideoAddedResultData")
|
||||||
|
.getString("setVideoId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
val jsonForMessage = json.toString()
|
||||||
|
Logger.printException(
|
||||||
|
{ "Fetch failed while processing response data for response: $jsonForMessage" },
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetch(
|
||||||
|
videoId: String,
|
||||||
|
playlistId: String,
|
||||||
|
setVideoId: String?,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
): String? {
|
||||||
|
val json = sendRequest(
|
||||||
|
videoId,
|
||||||
|
playlistId,
|
||||||
|
setVideoId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
if (json != null) {
|
||||||
|
return parseResponse(json, StringUtils.isNotEmpty(setVideoId))
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,222 @@
|
|||||||
|
package app.revanced.extension.youtube.patches.utils.requests
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy
|
||||||
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getPlaylistsRequestBody
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLISTS
|
||||||
|
import app.revanced.extension.shared.requests.Requester
|
||||||
|
import app.revanced.extension.shared.utils.Logger
|
||||||
|
import app.revanced.extension.shared.utils.Utils
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.Objects
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
|
import java.util.concurrent.Future
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
class GetPlaylistsRequest private constructor(
|
||||||
|
private val playlistId: String,
|
||||||
|
private val requestHeader: Map<String, String>,
|
||||||
|
private val dataSyncId: String,
|
||||||
|
) {
|
||||||
|
private val future: Future<Array<Pair<String, String>>> = Utils.submitOnBackgroundThread {
|
||||||
|
fetch(
|
||||||
|
playlistId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val playlists: Array<Pair<String, String>>?
|
||||||
|
get() {
|
||||||
|
try {
|
||||||
|
return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS]
|
||||||
|
} catch (ex: TimeoutException) {
|
||||||
|
Logger.printInfo(
|
||||||
|
{ "getPlaylists timed out" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
} catch (ex: InterruptedException) {
|
||||||
|
Logger.printException(
|
||||||
|
{ "getPlaylists interrupted" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
Thread.currentThread().interrupt() // Restore interrupt status flag.
|
||||||
|
} catch (ex: ExecutionException) {
|
||||||
|
Logger.printException(
|
||||||
|
{ "getPlaylists failure" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
|
||||||
|
|
||||||
|
@GuardedBy("itself")
|
||||||
|
val cache: MutableMap<String, GetPlaylistsRequest> = Collections.synchronizedMap(
|
||||||
|
object : LinkedHashMap<String, GetPlaylistsRequest>(100) {
|
||||||
|
private val CACHE_LIMIT = 50
|
||||||
|
|
||||||
|
override fun removeEldestEntry(eldest: Map.Entry<String, GetPlaylistsRequest>): Boolean {
|
||||||
|
return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun clear() {
|
||||||
|
synchronized(cache) {
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun fetchRequestIfNeeded(
|
||||||
|
playlistId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(playlistId)
|
||||||
|
synchronized(cache) {
|
||||||
|
if (!cache.containsKey(playlistId)) {
|
||||||
|
cache[playlistId] = GetPlaylistsRequest(
|
||||||
|
playlistId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getRequestForPlaylistId(playlistId: String): GetPlaylistsRequest? {
|
||||||
|
synchronized(cache) {
|
||||||
|
return cache[playlistId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleConnectionError(toastMessage: String, ex: Exception?) {
|
||||||
|
Logger.printInfo({ toastMessage }, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendRequest(
|
||||||
|
playlistId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
): JSONObject? {
|
||||||
|
Objects.requireNonNull(playlistId)
|
||||||
|
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
// 'playlist/get_add_to_playlist' endpoint does not require PoToken.
|
||||||
|
val clientType = YouTubeAppClient.ClientType.ANDROID
|
||||||
|
val clientTypeName = clientType.name
|
||||||
|
Logger.printDebug { "Fetching get playlists request, playlistId: $playlistId, using client: $clientTypeName" }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||||
|
GET_PLAYLISTS,
|
||||||
|
clientType,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId
|
||||||
|
)
|
||||||
|
|
||||||
|
val requestBody = getPlaylistsRequestBody(playlistId)
|
||||||
|
|
||||||
|
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||||
|
connection.outputStream.write(requestBody)
|
||||||
|
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
if (responseCode == 200) return Requester.parseJSONObject(connection)
|
||||||
|
|
||||||
|
handleConnectionError(
|
||||||
|
(clientTypeName + " not available with response code: "
|
||||||
|
+ responseCode + " message: " + connection.responseMessage),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} catch (ex: SocketTimeoutException) {
|
||||||
|
handleConnectionError("Connection timeout", ex)
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
handleConnectionError("Network error", ex)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.printException({ "sendRequest failed" }, ex)
|
||||||
|
} finally {
|
||||||
|
Logger.printDebug { "playlist: " + playlistId + " took: " + (System.currentTimeMillis() - startTime) + "ms" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseResponse(json: JSONObject): Array<Pair<String, String>>? {
|
||||||
|
try {
|
||||||
|
val addToPlaylistRendererJsonObject =
|
||||||
|
json.getJSONArray("contents").get(0)
|
||||||
|
|
||||||
|
if (addToPlaylistRendererJsonObject is JSONObject) {
|
||||||
|
val playlistsJsonArray =
|
||||||
|
addToPlaylistRendererJsonObject
|
||||||
|
.getJSONObject("addToPlaylistRenderer")
|
||||||
|
.getJSONArray("playlists")
|
||||||
|
|
||||||
|
val playlistsLength = playlistsJsonArray.length()
|
||||||
|
val playlists: Array<Pair<String, String>?> =
|
||||||
|
arrayOfNulls(playlistsLength)
|
||||||
|
|
||||||
|
for (i in 0..playlistsLength - 1) {
|
||||||
|
val elementsJsonObject =
|
||||||
|
playlistsJsonArray.get(i)
|
||||||
|
|
||||||
|
if (elementsJsonObject is JSONObject) {
|
||||||
|
val playlistAddToOptionRendererJSONObject =
|
||||||
|
elementsJsonObject
|
||||||
|
.getJSONObject("playlistAddToOptionRenderer")
|
||||||
|
|
||||||
|
val playlistId = playlistAddToOptionRendererJSONObject
|
||||||
|
.getString("playlistId")
|
||||||
|
val playlistTitle =
|
||||||
|
(playlistAddToOptionRendererJSONObject
|
||||||
|
.getJSONObject("title")
|
||||||
|
.getJSONArray("runs")
|
||||||
|
.get(0) as JSONObject)
|
||||||
|
.getString("text")
|
||||||
|
|
||||||
|
playlists[i] = Pair(playlistId, playlistTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalPlaylists = playlists.filterNotNull().toTypedArray()
|
||||||
|
if (finalPlaylists.isNotEmpty()) {
|
||||||
|
return finalPlaylists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
val jsonForMessage = json.toString()
|
||||||
|
Logger.printException(
|
||||||
|
{ "Fetch failed while processing response data for response: $jsonForMessage" },
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetch(
|
||||||
|
playlistId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
): Array<Pair<String, String>>? {
|
||||||
|
val json = sendRequest(playlistId, requestHeader, dataSyncId)
|
||||||
|
if (json != null) {
|
||||||
|
return parseResponse(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,193 @@
|
|||||||
|
package app.revanced.extension.youtube.patches.utils.requests
|
||||||
|
|
||||||
|
import androidx.annotation.GuardedBy
|
||||||
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.savePlaylistRequestBody
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.EDIT_PLAYLIST
|
||||||
|
import app.revanced.extension.shared.requests.Requester
|
||||||
|
import app.revanced.extension.shared.utils.Logger
|
||||||
|
import app.revanced.extension.shared.utils.Utils
|
||||||
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
|
import java.io.IOException
|
||||||
|
import java.net.SocketTimeoutException
|
||||||
|
import java.util.Collections
|
||||||
|
import java.util.Objects
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
|
import java.util.concurrent.Future
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import java.util.concurrent.TimeoutException
|
||||||
|
|
||||||
|
class SavePlaylistRequest private constructor(
|
||||||
|
private val playlistId: String,
|
||||||
|
private val libraryId: String,
|
||||||
|
private val requestHeader: Map<String, String>,
|
||||||
|
private val dataSyncId: String,
|
||||||
|
) {
|
||||||
|
private val future: Future<Boolean> = Utils.submitOnBackgroundThread {
|
||||||
|
fetch(
|
||||||
|
playlistId,
|
||||||
|
libraryId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result: Boolean?
|
||||||
|
get() {
|
||||||
|
try {
|
||||||
|
return future[MAX_MILLISECONDS_TO_WAIT_FOR_FETCH.toLong(), TimeUnit.MILLISECONDS]
|
||||||
|
} catch (ex: TimeoutException) {
|
||||||
|
Logger.printInfo(
|
||||||
|
{ "getResult timed out" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
} catch (ex: InterruptedException) {
|
||||||
|
Logger.printException(
|
||||||
|
{ "getResult interrupted" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
Thread.currentThread().interrupt() // Restore interrupt status flag.
|
||||||
|
} catch (ex: ExecutionException) {
|
||||||
|
Logger.printException(
|
||||||
|
{ "getResult failure" },
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
|
||||||
|
|
||||||
|
@GuardedBy("itself")
|
||||||
|
val cache: MutableMap<String, SavePlaylistRequest> = Collections.synchronizedMap(
|
||||||
|
object : LinkedHashMap<String, SavePlaylistRequest>(100) {
|
||||||
|
private val CACHE_LIMIT = 50
|
||||||
|
|
||||||
|
override fun removeEldestEntry(eldest: Map.Entry<String, SavePlaylistRequest>): Boolean {
|
||||||
|
return size > CACHE_LIMIT // Evict the oldest entry if over the cache limit.
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun clear() {
|
||||||
|
synchronized(cache) {
|
||||||
|
cache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun fetchRequestIfNeeded(
|
||||||
|
playlistId: String,
|
||||||
|
libraryId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
) {
|
||||||
|
Objects.requireNonNull(playlistId)
|
||||||
|
synchronized(cache) {
|
||||||
|
cache[libraryId] = SavePlaylistRequest(
|
||||||
|
playlistId,
|
||||||
|
libraryId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getRequestForLibraryId(libraryId: String): SavePlaylistRequest? {
|
||||||
|
synchronized(cache) {
|
||||||
|
return cache[libraryId]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleConnectionError(toastMessage: String, ex: Exception?) {
|
||||||
|
Logger.printInfo({ toastMessage }, ex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendRequest(
|
||||||
|
playlistId: String,
|
||||||
|
libraryId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
): JSONObject? {
|
||||||
|
Objects.requireNonNull(playlistId)
|
||||||
|
Objects.requireNonNull(libraryId)
|
||||||
|
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
// 'browse/edit_playlist' endpoint does not require PoToken.
|
||||||
|
val clientType = YouTubeAppClient.ClientType.ANDROID
|
||||||
|
val clientTypeName = clientType.name
|
||||||
|
Logger.printDebug { "Fetching edit playlist request, playlistId: $playlistId, libraryId: $libraryId, using client: $clientTypeName" }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||||
|
EDIT_PLAYLIST,
|
||||||
|
clientType,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId
|
||||||
|
)
|
||||||
|
|
||||||
|
val requestBody = savePlaylistRequestBody(libraryId, playlistId)
|
||||||
|
|
||||||
|
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||||
|
connection.outputStream.write(requestBody)
|
||||||
|
|
||||||
|
val responseCode = connection.responseCode
|
||||||
|
if (responseCode == 200) return Requester.parseJSONObject(connection)
|
||||||
|
|
||||||
|
handleConnectionError(
|
||||||
|
(clientTypeName + " not available with response code: "
|
||||||
|
+ responseCode + " message: " + connection.responseMessage),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} catch (ex: SocketTimeoutException) {
|
||||||
|
handleConnectionError("Connection timeout", ex)
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
handleConnectionError("Network error", ex)
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
Logger.printException({ "sendRequest failed" }, ex)
|
||||||
|
} finally {
|
||||||
|
Logger.printDebug { "playlistId: $playlistId libraryId: $libraryId took: ${(System.currentTimeMillis() - startTime)}ms" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseResponse(json: JSONObject): Boolean? {
|
||||||
|
try {
|
||||||
|
return json.getString("status") == "STATUS_SUCCEEDED"
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
val jsonForMessage = json.toString()
|
||||||
|
Logger.printException(
|
||||||
|
{ "Fetch failed while processing response data for response: $jsonForMessage" },
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fetch(
|
||||||
|
playlistId: String,
|
||||||
|
libraryId: String,
|
||||||
|
requestHeader: Map<String, String>,
|
||||||
|
dataSyncId: String,
|
||||||
|
): Boolean? {
|
||||||
|
val json = sendRequest(
|
||||||
|
playlistId,
|
||||||
|
libraryId,
|
||||||
|
requestHeader,
|
||||||
|
dataSyncId,
|
||||||
|
)
|
||||||
|
if (json != null) {
|
||||||
|
return parseResponse(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,10 @@
|
|||||||
package app.revanced.extension.youtube.patches.video;
|
package app.revanced.extension.youtube.patches.video;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class AV1CodecPatch {
|
public class AV1CodecPatch {
|
||||||
private static final int LITERAL_VALUE_AV01 = 1635135811;
|
|
||||||
private static final int LITERAL_VALUE_DOLBY_VISION = 1685485123;
|
|
||||||
private static final String VP9_CODEC = "video/x-vnd.on2.vp9";
|
private static final String VP9_CODEC = "video/x-vnd.on2.vp9";
|
||||||
private static long lastTimeResponse = 0;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace the SW AV01 codec to VP9 codec.
|
* Replace the SW AV01 codec to VP9 codec.
|
||||||
@ -22,32 +15,4 @@ public class AV1CodecPatch {
|
|||||||
public static String replaceCodec(String original) {
|
public static String replaceCodec(String original) {
|
||||||
return Settings.REPLACE_AV1_CODEC.get() ? VP9_CODEC : original;
|
return Settings.REPLACE_AV1_CODEC.get() ? VP9_CODEC : original;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace the SW AV01 codec request with a Dolby Vision codec request.
|
|
||||||
* This request is invalid, so it falls back to codecs other than AV01.
|
|
||||||
* <p>
|
|
||||||
* Limitation: Fallback process causes about 15-20 seconds of buffering.
|
|
||||||
*
|
|
||||||
* @param literalValue literal value of the codec
|
|
||||||
*/
|
|
||||||
public static int rejectResponse(int literalValue) {
|
|
||||||
if (!Settings.REJECT_AV1_CODEC.get())
|
|
||||||
return literalValue;
|
|
||||||
|
|
||||||
Logger.printDebug(() -> "Response: " + literalValue);
|
|
||||||
|
|
||||||
if (literalValue != LITERAL_VALUE_AV01)
|
|
||||||
return literalValue;
|
|
||||||
|
|
||||||
final long currentTime = System.currentTimeMillis();
|
|
||||||
|
|
||||||
// Ignore the invoke within 20 seconds.
|
|
||||||
if (currentTime - lastTimeResponse > 20000) {
|
|
||||||
lastTimeResponse = currentTime;
|
|
||||||
Utils.showToastShort(str("revanced_reject_av1_codec_toast"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return LITERAL_VALUE_DOLBY_VISION;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -75,30 +75,30 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
return isCustomPlaybackSpeedEnabled() ? 0 : original;
|
return isCustomPlaybackSpeedEnabled() ? 0 : original;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] getListEntries() {
|
public static String[] getEntries() {
|
||||||
return isCustomPlaybackSpeedEnabled()
|
return isCustomPlaybackSpeedEnabled()
|
||||||
? customSpeedEntries
|
? customSpeedEntries
|
||||||
: defaultSpeedEntries;
|
: defaultSpeedEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] getListEntryValues() {
|
public static String[] getEntryValues() {
|
||||||
return isCustomPlaybackSpeedEnabled()
|
return isCustomPlaybackSpeedEnabled()
|
||||||
? customSpeedEntryValues
|
? customSpeedEntryValues
|
||||||
: defaultSpeedEntryValues;
|
: defaultSpeedEntryValues;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] getTrimmedListEntries() {
|
public static String[] getTrimmedEntries() {
|
||||||
if (playbackSpeedEntries == null) {
|
if (playbackSpeedEntries == null) {
|
||||||
final String[] playbackSpeedWithAutoEntries = getListEntries();
|
final String[] playbackSpeedWithAutoEntries = getEntries();
|
||||||
playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length);
|
playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return playbackSpeedEntries;
|
return playbackSpeedEntries;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String[] getTrimmedListEntryValues() {
|
public static String[] getTrimmedEntryValues() {
|
||||||
if (playbackSpeedEntryValues == null) {
|
if (playbackSpeedEntryValues == null) {
|
||||||
final String[] playbackSpeedWithAutoEntryValues = getListEntryValues();
|
final String[] playbackSpeedWithAutoEntryValues = getEntryValues();
|
||||||
playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length);
|
playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
package app.revanced.extension.youtube.patches.video;
|
package app.revanced.extension.youtube.patches.video;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||||
|
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.BooleanUtils;
|
import org.apache.commons.lang3.BooleanUtils;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
|
import app.revanced.extension.shared.settings.FloatSetting;
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
||||||
@ -17,25 +19,57 @@ import app.revanced.extension.youtube.whitelist.Whitelist;
|
|||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class PlaybackSpeedPatch {
|
public class PlaybackSpeedPatch {
|
||||||
|
private static final FloatSetting DEFAULT_PLAYBACK_SPEED =
|
||||||
|
Settings.DEFAULT_PLAYBACK_SPEED;
|
||||||
|
private static final FloatSetting DEFAULT_PLAYBACK_SPEED_SHORTS =
|
||||||
|
Settings.DEFAULT_PLAYBACK_SPEED_SHORTS;
|
||||||
|
|
||||||
private static final boolean DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC =
|
private static final boolean DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC =
|
||||||
Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get();
|
Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get();
|
||||||
private static final long TOAST_DELAY_MILLISECONDS = 750;
|
private static final long TOAST_DELAY_MILLISECONDS = 750;
|
||||||
private static long lastTimeSpeedChanged;
|
private static long lastTimeSpeedChanged;
|
||||||
|
private static float lastSelectedPlaybackSpeed = 1.0f;
|
||||||
|
|
||||||
|
private static volatile String channelId = "";
|
||||||
|
private static volatile String videoId = "";
|
||||||
private static boolean isLiveStream;
|
private static boolean isLiveStream;
|
||||||
|
|
||||||
|
private static volatile String channelIdShorts = "";
|
||||||
|
private static volatile String videoIdShorts = "";
|
||||||
|
private static boolean isLiveStreamShorts;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||||
isLiveStream = newlyLoadedLiveStreamValue;
|
if (isShortsActive()) {
|
||||||
Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId);
|
channelIdShorts = newlyLoadedChannelId;
|
||||||
|
videoIdShorts = newlyLoadedVideoId;
|
||||||
|
isLiveStreamShorts = newlyLoadedLiveStreamValue;
|
||||||
|
|
||||||
final float defaultPlaybackSpeed = getDefaultPlaybackSpeed(newlyLoadedChannelId, newlyLoadedVideoId);
|
Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId);
|
||||||
Logger.printDebug(() -> "overridePlaybackSpeed: " + defaultPlaybackSpeed);
|
} else {
|
||||||
|
channelId = newlyLoadedChannelId;
|
||||||
|
videoId = newlyLoadedVideoId;
|
||||||
|
isLiveStream = newlyLoadedLiveStreamValue;
|
||||||
|
|
||||||
VideoInformation.overridePlaybackSpeed(defaultPlaybackSpeed);
|
Logger.printDebug(() -> "newShortsVideoStarted: " + newlyLoadedVideoId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static void newShortsVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||||
|
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||||
|
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||||
|
channelIdShorts = newlyLoadedChannelId;
|
||||||
|
videoIdShorts = newlyLoadedVideoId;
|
||||||
|
isLiveStreamShorts = newlyLoadedLiveStreamValue;
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "newShortsVideoStarted: " + newlyLoadedVideoId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,17 +99,34 @@ public class PlaybackSpeedPatch {
|
|||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
public static float getPlaybackSpeedInShorts(final float playbackSpeed) {
|
public static float getPlaybackSpeed(float playbackSpeed) {
|
||||||
if (VideoInformation.lastPlayerResponseIsShort() &&
|
boolean isShorts = isShortsActive();
|
||||||
Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get()
|
String currentChannelId = isShorts ? channelIdShorts : channelId;
|
||||||
) {
|
String currentVideoId = isShorts ? videoIdShorts : videoId;
|
||||||
float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null);
|
boolean currentVideoIsLiveStream = isShorts ? isLiveStreamShorts : isLiveStream;
|
||||||
Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed);
|
boolean currentVideoIsWhitelisted = Whitelist.isChannelWhitelistedPlaybackSpeed(currentChannelId);
|
||||||
|
boolean currentVideoIsMusic = !isShorts && isMusic();
|
||||||
|
|
||||||
return defaultPlaybackSpeed;
|
if (currentVideoIsLiveStream || currentVideoIsWhitelisted || currentVideoIsMusic) {
|
||||||
|
Logger.printDebug(() -> "changing playback speed to: 1.0");
|
||||||
|
VideoInformation.setPlaybackSpeed(1.0f);
|
||||||
|
return 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
return playbackSpeed;
|
float defaultPlaybackSpeed = isShorts ? DEFAULT_PLAYBACK_SPEED_SHORTS.get() : DEFAULT_PLAYBACK_SPEED.get();
|
||||||
|
|
||||||
|
if (defaultPlaybackSpeed < 0) {
|
||||||
|
float finalPlaybackSpeed = isShorts ? playbackSpeed : lastSelectedPlaybackSpeed;
|
||||||
|
VideoInformation.overridePlaybackSpeed(finalPlaybackSpeed);
|
||||||
|
Logger.printDebug(() -> "changing playback speed to: " + finalPlaybackSpeed);
|
||||||
|
return finalPlaybackSpeed;
|
||||||
|
} else {
|
||||||
|
if (isShorts) {
|
||||||
|
VideoInformation.setPlaybackSpeed(defaultPlaybackSpeed);
|
||||||
|
}
|
||||||
|
Logger.printDebug(() -> "changing playback speed to: " + defaultPlaybackSpeed);
|
||||||
|
return defaultPlaybackSpeed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,51 +137,57 @@ public class PlaybackSpeedPatch {
|
|||||||
*/
|
*/
|
||||||
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
|
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
|
||||||
try {
|
try {
|
||||||
if (PatchStatus.RememberPlaybackSpeed() &&
|
boolean isShorts = isShortsActive();
|
||||||
Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
|
if (PatchStatus.RememberPlaybackSpeed()) {
|
||||||
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
|
BooleanSetting rememberPlaybackSpeedLastSelectedSetting = isShorts
|
||||||
// then the menu will allow increasing without bounds but the max speed is
|
? Settings.REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED
|
||||||
// still capped to under 8.0x.
|
: Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED;
|
||||||
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
|
FloatSetting playbackSpeedSetting = isShorts
|
||||||
|
? DEFAULT_PLAYBACK_SPEED_SHORTS
|
||||||
|
: DEFAULT_PLAYBACK_SPEED;
|
||||||
|
BooleanSetting showToastSetting = isShorts
|
||||||
|
? Settings.REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED_TOAST
|
||||||
|
: Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST;
|
||||||
|
|
||||||
// Prevent toast spamming if using the 0.05x adjustments.
|
if (rememberPlaybackSpeedLastSelectedSetting.get()) {
|
||||||
// Show exactly one toast after the user stops interacting with the speed menu.
|
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
|
||||||
final long now = System.currentTimeMillis();
|
// then the menu will allow increasing without bounds but the max speed is
|
||||||
lastTimeSpeedChanged = now;
|
// still capped to under 8.0x.
|
||||||
|
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
|
||||||
|
|
||||||
final float finalPlaybackSpeed = playbackSpeed;
|
// Prevent toast spamming if using the 0.05x adjustments.
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
// Show exactly one toast after the user stops interacting with the speed menu.
|
||||||
if (lastTimeSpeedChanged != now) {
|
final long now = System.currentTimeMillis();
|
||||||
// The user made additional speed adjustments and this call is outdated.
|
lastTimeSpeedChanged = now;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Settings.DEFAULT_PLAYBACK_SPEED.get() == finalPlaybackSpeed) {
|
final float finalPlaybackSpeed = playbackSpeed;
|
||||||
// User changed to a different speed and immediately changed back.
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
// Or the user is going past 8.0x in the glitched out 0.05x menu.
|
if (lastTimeSpeedChanged != now) {
|
||||||
return;
|
// The user made additional speed adjustments and this call is outdated.
|
||||||
}
|
return;
|
||||||
Settings.DEFAULT_PLAYBACK_SPEED.save(finalPlaybackSpeed);
|
}
|
||||||
|
if (playbackSpeedSetting.get() == finalPlaybackSpeed) {
|
||||||
|
// User changed to a different speed and immediately changed back.
|
||||||
|
// Or the user is going past 8.0x in the glitched out 0.05x menu.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
playbackSpeedSetting.save(finalPlaybackSpeed);
|
||||||
|
|
||||||
if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) {
|
if (showToastSetting.get()) {
|
||||||
return;
|
Utils.showToastShort(str(isShorts ? "revanced_remember_playback_speed_toast_shorts" : "revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
|
||||||
}
|
}
|
||||||
Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
|
}, TOAST_DELAY_MILLISECONDS);
|
||||||
}, TOAST_DELAY_MILLISECONDS);
|
}
|
||||||
|
} else if (!isShorts) {
|
||||||
|
lastSelectedPlaybackSpeed = playbackSpeed;
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "userSelectedPlaybackSpeed failure", ex);
|
Logger.printException(() -> "userSelectedPlaybackSpeed failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) {
|
private static boolean isMusic() {
|
||||||
return (isLiveStream || Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) || isMusic(videoId))
|
if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && !videoId.isEmpty()) {
|
||||||
? 1.0f
|
|
||||||
: Settings.DEFAULT_PLAYBACK_SPEED.get();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isMusic(@Nullable String videoId) {
|
|
||||||
if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && videoId != null) {
|
|
||||||
try {
|
try {
|
||||||
MusicRequest request = MusicRequest.getRequestForVideoId(videoId);
|
MusicRequest request = MusicRequest.getRequestForVideoId(videoId);
|
||||||
final boolean isMusic = request != null && BooleanUtils.toBoolean(request.getStream());
|
final boolean isMusic = request != null && BooleanUtils.toBoolean(request.getStream());
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.patches.video;
|
package app.revanced.extension.youtube.patches.video;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||||
|
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
@ -14,8 +15,10 @@ import app.revanced.extension.youtube.shared.VideoInformation;
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class VideoQualityPatch {
|
public class VideoQualityPatch {
|
||||||
private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2;
|
private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2;
|
||||||
private static final IntegerSetting mobileQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_MOBILE;
|
private static final IntegerSetting shortsQualityMobile = Settings.DEFAULT_VIDEO_QUALITY_MOBILE_SHORTS;
|
||||||
private static final IntegerSetting wifiQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_WIFI;
|
private static final IntegerSetting shortsQualityWifi = Settings.DEFAULT_VIDEO_QUALITY_WIFI_SHORTS;
|
||||||
|
private static final IntegerSetting videoQualityMobile = Settings.DEFAULT_VIDEO_QUALITY_MOBILE;
|
||||||
|
private static final IntegerSetting videoQualityWifi = Settings.DEFAULT_VIDEO_QUALITY_WIFI;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static String videoId = "";
|
public static String videoId = "";
|
||||||
@ -35,12 +38,11 @@ public class VideoQualityPatch {
|
|||||||
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||||
if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL)
|
if (PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL &&
|
||||||
return;
|
!videoId.equals(newlyLoadedVideoId)) {
|
||||||
if (videoId.equals(newlyLoadedVideoId))
|
videoId = newlyLoadedVideoId;
|
||||||
return;
|
setVideoQuality(750);
|
||||||
videoId = newlyLoadedVideoId;
|
}
|
||||||
setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 750);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -53,42 +55,62 @@ public class VideoQualityPatch {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void setVideoQuality(final long delayMillis) {
|
private static void setVideoQuality(long delayMillis) {
|
||||||
final int defaultQuality = Utils.getNetworkType() == Utils.NetworkType.MOBILE
|
boolean isShorts = isShortsActive();
|
||||||
? mobileQualitySetting.get()
|
IntegerSetting defaultQualitySetting = Utils.getNetworkType() == Utils.NetworkType.MOBILE
|
||||||
: wifiQualitySetting.get();
|
? isShorts ? shortsQualityMobile : videoQualityMobile
|
||||||
|
: isShorts ? shortsQualityWifi : videoQualityWifi;
|
||||||
|
|
||||||
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
|
int defaultQuality = defaultQualitySetting.get();
|
||||||
return;
|
|
||||||
|
|
||||||
Utils.runOnMainThreadDelayed(() -> {
|
if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
|
||||||
final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality);
|
Utils.runOnMainThreadDelayed(() -> {
|
||||||
Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal);
|
final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality);
|
||||||
VideoInformation.overrideVideoQuality(qualityToUseFinal);
|
Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal);
|
||||||
}, delayMillis
|
VideoInformation.overrideVideoQuality(qualityToUseFinal);
|
||||||
);
|
}, delayMillis
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void userSelectedVideoQuality(final int defaultQuality) {
|
private static void userSelectedVideoQuality(final int defaultQuality) {
|
||||||
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get())
|
if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
|
||||||
return;
|
final Utils.NetworkType networkType = Utils.getNetworkType();
|
||||||
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
|
String networkTypeMessage = networkType == Utils.NetworkType.MOBILE
|
||||||
return;
|
? str("revanced_remember_video_quality_mobile")
|
||||||
|
: str("revanced_remember_video_quality_wifi");
|
||||||
|
|
||||||
final Utils.NetworkType networkType = Utils.getNetworkType();
|
if (isShortsActive()) {
|
||||||
|
if (Settings.REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED.get()) {
|
||||||
|
IntegerSetting defaultQualitySetting = networkType == Utils.NetworkType.MOBILE
|
||||||
|
? shortsQualityMobile
|
||||||
|
: shortsQualityWifi;
|
||||||
|
|
||||||
switch (networkType) {
|
defaultQualitySetting.save(defaultQuality);
|
||||||
case NONE -> {
|
|
||||||
Utils.showToastShort(str("revanced_remember_video_quality_none"));
|
if (Settings.REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED_TOAST.get()) {
|
||||||
return;
|
Utils.showToastShort(str(
|
||||||
|
"revanced_remember_video_quality_toast_shorts",
|
||||||
|
networkTypeMessage, (defaultQuality + "p")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) {
|
||||||
|
IntegerSetting defaultQualitySetting = networkType == Utils.NetworkType.MOBILE
|
||||||
|
? videoQualityMobile
|
||||||
|
: videoQualityWifi;
|
||||||
|
|
||||||
|
defaultQualitySetting.save(defaultQuality);
|
||||||
|
|
||||||
|
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) {
|
||||||
|
Utils.showToastShort(str(
|
||||||
|
"revanced_remember_video_quality_toast",
|
||||||
|
networkTypeMessage, (defaultQuality + "p")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case MOBILE -> mobileQualitySetting.save(defaultQuality);
|
|
||||||
default -> wifiQualitySetting.save(defaultQuality);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
|
|
||||||
return;
|
|
||||||
|
|
||||||
Utils.showToastShort(str("revanced_remember_video_quality_" + networkType.getName(), defaultQuality + "p"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,9 +2,13 @@ package app.revanced.extension.youtube.patches.video.requests
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.annotation.GuardedBy
|
import androidx.annotation.GuardedBy
|
||||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient
|
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||||
import app.revanced.extension.shared.patches.client.YouTubeWebClient
|
import app.revanced.extension.shared.innertube.client.YouTubeWebClient
|
||||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createWebInnertubeBody
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_CATEGORY
|
||||||
|
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLIST_PAGE
|
||||||
import app.revanced.extension.shared.requests.Requester
|
import app.revanced.extension.shared.requests.Requester
|
||||||
import app.revanced.extension.shared.utils.Logger
|
import app.revanced.extension.shared.utils.Logger
|
||||||
import app.revanced.extension.shared.utils.Utils
|
import app.revanced.extension.shared.utils.Utils
|
||||||
@ -124,12 +128,12 @@ class MusicRequest private constructor(
|
|||||||
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
|
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
|
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||||
PlayerRoutes.GET_PLAYLIST_PAGE,
|
GET_PLAYLIST_PAGE,
|
||||||
clientType
|
clientType
|
||||||
)
|
)
|
||||||
val requestBody =
|
val requestBody =
|
||||||
PlayerRoutes.createApplicationRequestBody(
|
createApplicationRequestBody(
|
||||||
clientType = clientType,
|
clientType = clientType,
|
||||||
videoId = videoId,
|
videoId = videoId,
|
||||||
playlistId = "RD$videoId"
|
playlistId = "RD$videoId"
|
||||||
@ -168,12 +172,11 @@ class MusicRequest private constructor(
|
|||||||
Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" }
|
Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
|
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||||
PlayerRoutes.GET_CATEGORY,
|
GET_CATEGORY,
|
||||||
clientType
|
clientType
|
||||||
)
|
)
|
||||||
val requestBody =
|
val requestBody = createWebInnertubeBody(clientType, videoId)
|
||||||
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
|
|
||||||
|
|
||||||
connection.setFixedLengthStreamingMode(requestBody.size)
|
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||||
connection.outputStream.write(requestBody)
|
connection.outputStream.write(requestBody)
|
||||||
|
@ -115,7 +115,7 @@ public class ReturnYouTubeDislike {
|
|||||||
private static final Rect middleSeparatorBounds;
|
private static final Rect middleSeparatorBounds;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Left separator horizontal padding for Rolling Number layout.
|
* Horizontal padding between the left and middle separator.
|
||||||
*/
|
*/
|
||||||
public static final int leftSeparatorShapePaddingPixels;
|
public static final int leftSeparatorShapePaddingPixels;
|
||||||
private static final ShapeDrawable leftSeparatorShape;
|
private static final ShapeDrawable leftSeparatorShape;
|
||||||
@ -131,7 +131,7 @@ public class ReturnYouTubeDislike {
|
|||||||
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
|
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
|
||||||
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
|
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
|
||||||
|
|
||||||
leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0f, dp);
|
leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.4f, dp);
|
||||||
|
|
||||||
leftSeparatorShape = new ShapeDrawable(new RectShape());
|
leftSeparatorShape = new ShapeDrawable(new RectShape());
|
||||||
leftSeparatorShape.setBounds(leftSeparatorBounds);
|
leftSeparatorShape.setBounds(leftSeparatorBounds);
|
||||||
|
@ -28,6 +28,8 @@ import app.revanced.extension.shared.settings.LongSetting;
|
|||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.settings.StringSetting;
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||||
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.DeArrowAvailability;
|
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.DeArrowAvailability;
|
||||||
import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability;
|
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.ThumbnailOption;
|
||||||
@ -40,6 +42,7 @@ import app.revanced.extension.youtube.patches.player.ExitFullscreenPatch.Fullscr
|
|||||||
import app.revanced.extension.youtube.patches.player.MiniplayerPatch;
|
import app.revanced.extension.youtube.patches.player.MiniplayerPatch;
|
||||||
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
|
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
|
||||||
import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior;
|
import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior;
|
||||||
|
import app.revanced.extension.youtube.patches.swipe.SwipeControlsPatch;
|
||||||
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
||||||
import app.revanced.extension.youtube.shared.PlaylistIdPrefix;
|
import app.revanced.extension.youtube.shared.PlaylistIdPrefix;
|
||||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||||
@ -148,7 +151,6 @@ public class Settings extends BaseSettings {
|
|||||||
new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
|
new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
|
||||||
public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE);
|
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", PatchStatus.SplashAnimation(), 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 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_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 HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE);
|
||||||
@ -156,6 +158,8 @@ public class Settings extends BaseSettings {
|
|||||||
|
|
||||||
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
|
public static final EnumSetting<FormFactor> 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 CHANGE_LIVE_RING_CLICK_ACTION = new BooleanSetting("revanced_change_live_ring_click_action", FALSE, true);
|
||||||
|
public static final BooleanSetting DISABLE_LAYOUT_UPDATES = new BooleanSetting("revanced_disable_layout_updates", false, true, "revanced_disable_layout_updates_user_dialog_message");
|
||||||
|
public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", FALSE, true, "revanced_disable_translucent_status_bar_user_dialog_message");
|
||||||
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 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));
|
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION));
|
||||||
|
|
||||||
@ -179,12 +183,14 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_NAVIGATION_SUBSCRIPTIONS_BUTTON = new BooleanSetting("revanced_hide_navigation_subscriptions_button", FALSE, true);
|
public static final BooleanSetting HIDE_NAVIGATION_SUBSCRIPTIONS_BUTTON = new BooleanSetting("revanced_hide_navigation_subscriptions_button", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_LABEL = new BooleanSetting("revanced_hide_navigation_label", FALSE, true);
|
public static final BooleanSetting HIDE_NAVIGATION_LABEL = new BooleanSetting("revanced_hide_navigation_label", FALSE, true);
|
||||||
public static final BooleanSetting SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_switch_create_with_notifications_button", TRUE, true, "revanced_switch_create_with_notifications_button_user_dialog_message");
|
public static final BooleanSetting SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_switch_create_with_notifications_button", TRUE, true, "revanced_switch_create_with_notifications_button_user_dialog_message");
|
||||||
public static final BooleanSetting ENABLE_TRANSLUCENT_NAVIGATION_BAR = new BooleanSetting("revanced_enable_translucent_navigation_bar", FALSE, true, "revanced_enable_translucent_navigation_bar_user_dialog_message");
|
public static final BooleanSetting ENABLE_TRANSLUCENT_NAVIGATION_BAR = new BooleanSetting("revanced_enable_translucent_navigation_bar", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true);
|
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true);
|
||||||
|
|
||||||
// PreferenceScreen: General - Override buttons
|
// PreferenceScreen: General - Override buttons
|
||||||
public static final BooleanSetting OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_playlist_download_button", FALSE);
|
public static final BooleanSetting OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_playlist_download_button", FALSE, true);
|
||||||
public static final BooleanSetting OVERRIDE_VIDEO_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_video_download_button", FALSE);
|
public static final BooleanSetting OVERRIDE_VIDEO_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_video_download_button", FALSE, true);
|
||||||
|
public static final BooleanSetting OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER = new BooleanSetting("revanced_override_video_download_button_queue_manager", FALSE, true,
|
||||||
|
"revanced_queue_manager_user_dialog_message", parent(OVERRIDE_VIDEO_DOWNLOAD_BUTTON));
|
||||||
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_PLAYLIST = new StringSetting("revanced_external_downloader_package_name_playlist", "com.deniscerri.ytdl");
|
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_PLAYLIST = new StringSetting("revanced_external_downloader_package_name_playlist", "com.deniscerri.ytdl");
|
||||||
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO = new StringSetting("revanced_external_downloader_package_name_video", "com.deniscerri.ytdl");
|
public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO = new StringSetting("revanced_external_downloader_package_name_video", "com.deniscerri.ytdl");
|
||||||
public static final BooleanSetting OVERRIDE_YOUTUBE_MUSIC_BUTTON = new BooleanSetting("revanced_override_youtube_music_button", FALSE, true
|
public static final BooleanSetting OVERRIDE_YOUTUBE_MUSIC_BUTTON = new BooleanSetting("revanced_override_youtube_music_button", FALSE, true
|
||||||
@ -332,7 +338,7 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_YT_MUSIC = new BooleanSetting("revanced_hide_player_flyout_menu_listen_with_youtube_music", TRUE);
|
public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_YT_MUSIC = new BooleanSetting("revanced_hide_player_flyout_menu_listen_with_youtube_music", TRUE);
|
||||||
|
|
||||||
// PreferenceScreen: Player - Fullscreen
|
// PreferenceScreen: Player - Fullscreen
|
||||||
public static final BooleanSetting DISABLE_ENGAGEMENT_PANEL = new BooleanSetting("revanced_disable_engagement_panel", FALSE);
|
public static final BooleanSetting DISABLE_ENGAGEMENT_PANEL = new BooleanSetting("revanced_disable_engagement_panel", FALSE, true);
|
||||||
public static final BooleanSetting ENTER_FULLSCREEN = new BooleanSetting("revanced_enter_fullscreen", FALSE);
|
public static final BooleanSetting ENTER_FULLSCREEN = new BooleanSetting("revanced_enter_fullscreen", FALSE);
|
||||||
public static final EnumSetting<FullscreenMode> EXIT_FULLSCREEN = new EnumSetting<>("revanced_exit_fullscreen", FullscreenMode.DISABLED);
|
public static final EnumSetting<FullscreenMode> EXIT_FULLSCREEN = new EnumSetting<>("revanced_exit_fullscreen", FullscreenMode.DISABLED);
|
||||||
public static final BooleanSetting SHOW_VIDEO_TITLE_SECTION = new BooleanSetting("revanced_show_video_title_section", TRUE, true, parent(DISABLE_ENGAGEMENT_PANEL));
|
public static final BooleanSetting SHOW_VIDEO_TITLE_SECTION = new BooleanSetting("revanced_show_video_title_section", TRUE, true, parent(DISABLE_ENGAGEMENT_PANEL));
|
||||||
@ -394,6 +400,8 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting OVERLAY_BUTTON_COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_overlay_button_copy_video_url_timestamp", FALSE);
|
public static final BooleanSetting OVERLAY_BUTTON_COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_overlay_button_copy_video_url_timestamp", FALSE);
|
||||||
public static final BooleanSetting OVERLAY_BUTTON_MUTE_VOLUME = new BooleanSetting("revanced_overlay_button_mute_volume", FALSE);
|
public static final BooleanSetting OVERLAY_BUTTON_MUTE_VOLUME = new BooleanSetting("revanced_overlay_button_mute_volume", FALSE);
|
||||||
public static final BooleanSetting OVERLAY_BUTTON_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_overlay_button_external_downloader", FALSE);
|
public static final BooleanSetting OVERLAY_BUTTON_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_overlay_button_external_downloader", FALSE);
|
||||||
|
public static final BooleanSetting OVERLAY_BUTTON_EXTERNAL_DOWNLOADER_QUEUE_MANAGER = new BooleanSetting("revanced_overlay_button_external_downloader_queue_manager", FALSE, true,
|
||||||
|
"revanced_queue_manager_user_dialog_message", parent(OVERLAY_BUTTON_EXTERNAL_DOWNLOADER));
|
||||||
public static final BooleanSetting OVERLAY_BUTTON_SPEED_DIALOG = new BooleanSetting("revanced_overlay_button_speed_dialog", FALSE);
|
public static final BooleanSetting OVERLAY_BUTTON_SPEED_DIALOG = new BooleanSetting("revanced_overlay_button_speed_dialog", FALSE);
|
||||||
public static final BooleanSetting OVERLAY_BUTTON_PLAY_ALL = new BooleanSetting("revanced_overlay_button_play_all", FALSE);
|
public static final BooleanSetting OVERLAY_BUTTON_PLAY_ALL = new BooleanSetting("revanced_overlay_button_play_all", FALSE);
|
||||||
public static final EnumSetting<PlaylistIdPrefix> OVERLAY_BUTTON_PLAY_ALL_TYPE = new EnumSetting<>("revanced_overlay_button_play_all_type", PlaylistIdPrefix.ALL_CONTENTS_WITH_TIME_DESCENDING);
|
public static final EnumSetting<PlaylistIdPrefix> OVERLAY_BUTTON_PLAY_ALL_TYPE = new EnumSetting<>("revanced_overlay_button_play_all_type", PlaylistIdPrefix.ALL_CONTENTS_WITH_TIME_DESCENDING);
|
||||||
@ -490,12 +498,13 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL = new BooleanSetting("revanced_shorts_custom_actions_copy_video_url", FALSE, true);
|
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL = new BooleanSetting("revanced_shorts_custom_actions_copy_video_url", FALSE, true);
|
||||||
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_shorts_custom_actions_external_downloader", FALSE, true);
|
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_shorts_custom_actions_external_downloader", FALSE, true);
|
||||||
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO = new BooleanSetting("revanced_shorts_custom_actions_open_video", FALSE, true);
|
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO = new BooleanSetting("revanced_shorts_custom_actions_open_video", FALSE, true);
|
||||||
|
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG = new BooleanSetting("revanced_shorts_custom_actions_speed_dialog", FALSE, true);
|
||||||
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_REPEAT_STATE = new BooleanSetting("revanced_shorts_custom_actions_repeat_state", FALSE, true);
|
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_REPEAT_STATE = new BooleanSetting("revanced_shorts_custom_actions_repeat_state", FALSE, true);
|
||||||
|
|
||||||
public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU = new BooleanSetting("revanced_enable_shorts_custom_actions_flyout_menu", FALSE, true,
|
public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU = new BooleanSetting("revanced_enable_shorts_custom_actions_flyout_menu", FALSE, true,
|
||||||
parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
|
parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
|
||||||
public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR = new BooleanSetting("revanced_enable_shorts_custom_actions_toolbar", FALSE, true,
|
public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR = new BooleanSetting("revanced_enable_shorts_custom_actions_toolbar", FALSE, true,
|
||||||
parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
|
parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
|
||||||
|
|
||||||
// Experimental Flags
|
// Experimental Flags
|
||||||
public static final BooleanSetting ENABLE_TIME_STAMP = new BooleanSetting("revanced_enable_shorts_time_stamp", FALSE, true);
|
public static final BooleanSetting ENABLE_TIME_STAMP = new BooleanSetting("revanced_enable_shorts_time_stamp", FALSE, true);
|
||||||
@ -515,9 +524,15 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting ENABLE_SWIPE_PRESS_TO_ENGAGE = new BooleanSetting("revanced_enable_swipe_press_to_engage", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
public static final BooleanSetting ENABLE_SWIPE_PRESS_TO_ENGAGE = new BooleanSetting("revanced_enable_swipe_press_to_engage", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||||
public static final BooleanSetting ENABLE_SWIPE_HAPTIC_FEEDBACK = new BooleanSetting("revanced_enable_swipe_haptic_feedback", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
public static final BooleanSetting ENABLE_SWIPE_HAPTIC_FEEDBACK = new BooleanSetting("revanced_enable_swipe_haptic_feedback", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||||
public static final BooleanSetting SWIPE_LOCK_MODE = new BooleanSetting("revanced_swipe_gestures_lock_mode", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
public static final BooleanSetting SWIPE_LOCK_MODE = new BooleanSetting("revanced_swipe_gestures_lock_mode", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||||
|
public static final BooleanSetting SWIPE_OVERLAY_ALTERNATIVE_UI = new BooleanSetting("revanced_swipe_overlay_alternative_ui", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||||
|
public static final BooleanSetting SWIPE_SHOW_CIRCULAR_OVERLAY = new BooleanSetting("revanced_swipe_show_circular_overlay", FALSE, true,
|
||||||
|
new SwipeControlsPatch.SwipeOverlayModernUIAvailability());
|
||||||
|
public static final BooleanSetting SWIPE_OVERLAY_MINIMAL_STYLE = new BooleanSetting("revanced_swipe_overlay_minimal_style", FALSE, true,
|
||||||
|
new SwipeControlsPatch.SwipeOverlayModernUIAvailability());
|
||||||
|
public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||||
|
public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_overlay_text_size", 20, true,
|
||||||
|
new SwipeControlsPatch.SwipeOverlayTextSizeAvailability());
|
||||||
public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_magnitude_threshold", 0, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_magnitude_threshold", 0, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||||
public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_ALPHA = new IntegerSetting("revanced_swipe_overlay_background_alpha", 127, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
|
||||||
public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_overlay_text_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
|
||||||
public static final IntegerSetting SWIPE_OVERLAY_RECT_SIZE = new IntegerSetting("revanced_swipe_overlay_rect_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
public static final IntegerSetting SWIPE_OVERLAY_RECT_SIZE = new IntegerSetting("revanced_swipe_overlay_rect_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||||
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
|
||||||
|
|
||||||
@ -536,30 +551,38 @@ public class Settings extends BaseSettings {
|
|||||||
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1.0f, false, false);
|
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1.0f, false, false);
|
||||||
|
|
||||||
|
|
||||||
// PreferenceScreen: Video
|
// PreferenceScreen: Video - Codec
|
||||||
public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", -2.0f);
|
|
||||||
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
|
|
||||||
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
|
|
||||||
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE, true);
|
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE, true);
|
||||||
|
public static final BooleanSetting DISABLE_VP9_CODEC = new BooleanSetting("revanced_disable_vp9_codec", FALSE, true);
|
||||||
|
public static final BooleanSetting REPLACE_AV1_CODEC = new BooleanSetting("revanced_replace_av1_codec", FALSE, true);
|
||||||
|
|
||||||
|
// PreferenceScreen: Video - Playback speed
|
||||||
|
public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", -2.0f);
|
||||||
|
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE);
|
||||||
|
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED));
|
||||||
|
public static final FloatSetting DEFAULT_PLAYBACK_SPEED_SHORTS = new FloatSetting("revanced_default_playback_speed_shorts", -2.0f);
|
||||||
|
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_shorts_last_selected", TRUE);
|
||||||
|
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_shorts_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED));
|
||||||
public static final BooleanSetting ENABLE_CUSTOM_PLAYBACK_SPEED = new BooleanSetting("revanced_enable_custom_playback_speed", FALSE, true);
|
public static final BooleanSetting ENABLE_CUSTOM_PLAYBACK_SPEED = new BooleanSetting("revanced_enable_custom_playback_speed", FALSE, true);
|
||||||
public static final BooleanSetting CUSTOM_PLAYBACK_SPEED_MENU_TYPE = new BooleanSetting("revanced_custom_playback_speed_menu_type", FALSE, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
|
public static final BooleanSetting CUSTOM_PLAYBACK_SPEED_MENU_TYPE = new BooleanSetting("revanced_custom_playback_speed_menu_type", FALSE, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
|
||||||
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.25\n2.5", true, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
|
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.25\n2.5", true, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
|
||||||
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE);
|
|
||||||
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED));
|
// PreferenceScreen: Video - Video quality
|
||||||
|
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
|
||||||
|
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
|
||||||
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE);
|
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE);
|
||||||
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_LAST_SELECTED));
|
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_LAST_SELECTED));
|
||||||
|
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE_SHORTS = new IntegerSetting("revanced_default_video_quality_mobile_shorts", -2, true);
|
||||||
|
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI_SHORTS = new IntegerSetting("revanced_default_video_quality_wifi_shorts", -2, true);
|
||||||
|
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_shorts_last_selected", TRUE);
|
||||||
|
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_shorts_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED));
|
||||||
public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE, true);
|
public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE, true);
|
||||||
// Experimental Flags
|
|
||||||
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", FALSE, true);
|
|
||||||
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC_TYPE = new BooleanSetting("revanced_disable_default_playback_speed_music_type", FALSE, true, parent(DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC));
|
|
||||||
public static final BooleanSetting ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS = new BooleanSetting("revanced_enable_default_playback_speed_shorts", FALSE);
|
|
||||||
public static final BooleanSetting SKIP_PRELOADED_BUFFER = new BooleanSetting("revanced_skip_preloaded_buffer", FALSE, true, "revanced_skip_preloaded_buffer_user_dialog_message");
|
public static final BooleanSetting SKIP_PRELOADED_BUFFER = new BooleanSetting("revanced_skip_preloaded_buffer", FALSE, true, "revanced_skip_preloaded_buffer_user_dialog_message");
|
||||||
public static final BooleanSetting SKIP_PRELOADED_BUFFER_TOAST = new BooleanSetting("revanced_skip_preloaded_buffer_toast", TRUE);
|
public static final BooleanSetting SKIP_PRELOADED_BUFFER_TOAST = new BooleanSetting("revanced_skip_preloaded_buffer_toast", TRUE);
|
||||||
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true);
|
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true);
|
||||||
public static final BooleanSetting DISABLE_VP9_CODEC = new BooleanSetting("revanced_disable_vp9_codec", FALSE, true);
|
|
||||||
public static final BooleanSetting REPLACE_AV1_CODEC = new BooleanSetting("revanced_replace_av1_codec", FALSE, true);
|
|
||||||
public static final BooleanSetting REJECT_AV1_CODEC = new BooleanSetting("revanced_reject_av1_codec", FALSE, true);
|
|
||||||
|
|
||||||
|
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", FALSE, true);
|
||||||
|
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC_TYPE = new BooleanSetting("revanced_disable_default_playback_speed_music_type", FALSE, true, parent(DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC));
|
||||||
|
|
||||||
// PreferenceScreen: Miscellaneous
|
// PreferenceScreen: Miscellaneous
|
||||||
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
|
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
|
||||||
@ -611,24 +634,34 @@ public class Settings extends BaseSettings {
|
|||||||
|
|
||||||
public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY.reVancedKeyValue);
|
||||||
public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400");
|
public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400");
|
||||||
|
public static final FloatSetting SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f);
|
||||||
public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", SKIP_AUTOMATICALLY.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", SKIP_AUTOMATICALLY.reVancedKeyValue);
|
||||||
public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00");
|
public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00");
|
||||||
|
public static final FloatSetting SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f);
|
||||||
public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
||||||
public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF");
|
public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF");
|
||||||
|
public static final FloatSetting SB_CATEGORY_INTERACTION_OPACITY = new FloatSetting("sb_interaction_opacity", 0.8f);
|
||||||
public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue);
|
||||||
public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684");
|
public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684");
|
||||||
|
public static final FloatSetting SB_CATEGORY_HIGHLIGHT_OPACITY = new FloatSetting("sb_highlight_opacity", 0.8f);
|
||||||
public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
||||||
public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF");
|
public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF");
|
||||||
|
public static final FloatSetting SB_CATEGORY_INTRO_OPACITY = new FloatSetting("sb_intro_opacity", 0.8f);
|
||||||
public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
||||||
public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED");
|
public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED");
|
||||||
|
public static final FloatSetting SB_CATEGORY_OUTRO_OPACITY = new FloatSetting("sb_outro_opacity", 0.8f);
|
||||||
public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
||||||
public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6");
|
public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6");
|
||||||
|
public static final FloatSetting SB_CATEGORY_PREVIEW_OPACITY = new FloatSetting("sb_preview_opacity", 0.8f);
|
||||||
public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
|
||||||
public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF");
|
public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF");
|
||||||
|
public static final FloatSetting SB_CATEGORY_FILLER_OPACITY = new FloatSetting("sb_filler_opacity", 0.8f);
|
||||||
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue);
|
||||||
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900");
|
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900");
|
||||||
|
public static final FloatSetting SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY = new FloatSetting("sb_music_offtopic_opacity", 0.8f);
|
||||||
public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue);
|
public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue);
|
||||||
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF");
|
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF");
|
||||||
|
public static final FloatSetting SB_CATEGORY_UNSUBMITTED_OPACITY = new FloatSetting("sb_unsubmitted_opacity", 1.0f);
|
||||||
|
|
||||||
// SB Setting not exported
|
// SB Setting not exported
|
||||||
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false);
|
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false);
|
||||||
@ -637,6 +670,16 @@ public class Settings extends BaseSettings {
|
|||||||
|
|
||||||
static {
|
static {
|
||||||
// region Migration initialized
|
// region Migration initialized
|
||||||
|
|
||||||
|
// Old spoof versions that no longer work reliably.
|
||||||
|
String spoofAppVersionTarget = SPOOF_APP_VERSION_TARGET.get();
|
||||||
|
if (spoofAppVersionTarget.compareTo(SPOOF_APP_VERSION_TARGET.defaultValue) < 0) {
|
||||||
|
Utils.showToastShort(str("revanced_spoof_app_version_target_invalid_toast", spoofAppVersionTarget));
|
||||||
|
Utils.showToastShort(str("revanced_extended_reset_to_default_toast"));
|
||||||
|
Logger.printInfo(() -> "Resetting spoof app version target");
|
||||||
|
SPOOF_APP_VERSION_TARGET.resetToDefault();
|
||||||
|
}
|
||||||
|
|
||||||
// Categories were previously saved without a 'sb_' key prefix, so they need an additional adjustment.
|
// Categories were previously saved without a 'sb_' key prefix, so they need an additional adjustment.
|
||||||
Set<Setting<?>> sbCategories = new HashSet<>(Arrays.asList(
|
Set<Setting<?>> sbCategories = new HashSet<>(Arrays.asList(
|
||||||
SB_CATEGORY_SPONSOR,
|
SB_CATEGORY_SPONSOR,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
import static com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity.setToolbarLayoutParams;
|
import static com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity.setToolbarLayoutParams;
|
||||||
|
import static app.revanced.extension.shared.settings.BaseSettings.SPOOF_STREAMING_DATA_TYPE;
|
||||||
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog;
|
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog;
|
||||||
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary;
|
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary;
|
||||||
import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier;
|
import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier;
|
||||||
@ -9,6 +10,7 @@ import static app.revanced.extension.shared.utils.Utils.getChildView;
|
|||||||
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
|
import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
|
||||||
import static app.revanced.extension.shared.utils.Utils.showToastShort;
|
import static app.revanced.extension.shared.utils.Utils.showToastShort;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED;
|
import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED;
|
||||||
|
import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED_SHORTS;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT;
|
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT_TYPE;
|
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT_TYPE;
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ import android.preference.PreferenceGroup;
|
|||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.preference.SwitchPreference;
|
import android.preference.SwitchPreference;
|
||||||
|
import android.util.Pair;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
@ -55,12 +58,17 @@ import java.util.Set;
|
|||||||
import java.util.SortedMap;
|
import java.util.SortedMap;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
|
import app.revanced.extension.shared.settings.EnumSetting;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||||
|
import app.revanced.extension.shared.utils.StringRef;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch;
|
import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch;
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||||
import app.revanced.extension.youtube.utils.ThemeUtils;
|
import app.revanced.extension.youtube.utils.ThemeUtils;
|
||||||
|
|
||||||
@ -74,14 +82,19 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
|||||||
@SuppressLint("SuspiciousIndentation")
|
@SuppressLint("SuspiciousIndentation")
|
||||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||||
try {
|
try {
|
||||||
if (str == null) return;
|
if (str == null) {
|
||||||
Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (setting == null) return;
|
Setting<?> setting = Setting.getSettingFromPath(str);
|
||||||
|
if (setting == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Preference mPreference = findPreference(str);
|
Preference mPreference = findPreference(str);
|
||||||
|
if (mPreference == null) {
|
||||||
if (mPreference == null) return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (mPreference instanceof SwitchPreference switchPreference) {
|
if (mPreference instanceof SwitchPreference switchPreference) {
|
||||||
BooleanSetting boolSetting = (BooleanSetting) setting;
|
BooleanSetting boolSetting = (BooleanSetting) setting;
|
||||||
@ -108,9 +121,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
|||||||
} else {
|
} else {
|
||||||
Setting.privateSetValueFromString(setting, listPreference.getValue());
|
Setting.privateSetValueFromString(setting, listPreference.getValue());
|
||||||
}
|
}
|
||||||
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
|
if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
|
||||||
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
|
listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
|
||||||
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
|
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getEntryValues());
|
||||||
|
}
|
||||||
|
if (setting.equals(SPOOF_STREAMING_DATA_TYPE)) {
|
||||||
|
listPreference.setEntries(SpoofStreamingDataPatch.getEntries());
|
||||||
|
listPreference.setEntryValues(SpoofStreamingDataPatch.getEntryValues());
|
||||||
}
|
}
|
||||||
if (!(mPreference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
|
if (!(mPreference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
|
||||||
updateListPreferenceSummary(listPreference, setting);
|
updateListPreferenceSummary(listPreference, setting);
|
||||||
@ -122,18 +139,11 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
|||||||
|
|
||||||
ReVancedSettingsPreference.initializeReVancedSettings();
|
ReVancedSettingsPreference.initializeReVancedSettings();
|
||||||
|
|
||||||
if (settingImportInProgress) {
|
if (!settingImportInProgress && !showingUserDialogMessage) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!showingUserDialogMessage) {
|
|
||||||
final Context context = getActivity();
|
final Context context = getActivity();
|
||||||
|
|
||||||
if (setting.userDialogMessage != null
|
if (setting.userDialogMessage != null && !prefIsSetToDefault(mPreference, setting)) {
|
||||||
&& mPreference instanceof SwitchPreference switchPreference
|
showSettingUserDialogConfirmation(context, mPreference, setting);
|
||||||
&& setting.defaultValue instanceof Boolean defaultValue
|
|
||||||
&& switchPreference.isChecked() != defaultValue) {
|
|
||||||
showSettingUserDialogConfirmation(context, switchPreference, (BooleanSetting) setting);
|
|
||||||
} else if (setting.rebootApp) {
|
} else if (setting.rebootApp) {
|
||||||
showRestartDialog(context);
|
showRestartDialog(context);
|
||||||
}
|
}
|
||||||
@ -143,25 +153,56 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private void showSettingUserDialogConfirmation(Context context, SwitchPreference switchPreference, BooleanSetting setting) {
|
/**
|
||||||
|
* @return If the preference is currently set to the default value of the Setting.
|
||||||
|
*/
|
||||||
|
private boolean prefIsSetToDefault(Preference pref, Setting<?> setting) {
|
||||||
|
Object defaultValue = setting.defaultValue;
|
||||||
|
if (pref instanceof SwitchPreference switchPref) {
|
||||||
|
return switchPref.isChecked() == (Boolean) defaultValue;
|
||||||
|
}
|
||||||
|
String defaultValueString = defaultValue.toString();
|
||||||
|
if (pref instanceof EditTextPreference editPreference) {
|
||||||
|
return editPreference.getText().equals(defaultValueString);
|
||||||
|
}
|
||||||
|
if (pref instanceof ListPreference listPref) {
|
||||||
|
return listPref.getValue().equals(defaultValueString);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("Must override method to handle "
|
||||||
|
+ "preference type: " + pref.getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showSettingUserDialogConfirmation(Context context, Preference pref, Setting<?> setting) {
|
||||||
Utils.verifyOnMainThread();
|
Utils.verifyOnMainThread();
|
||||||
|
|
||||||
showingUserDialogMessage = true;
|
final StringRef userDialogMessage = setting.userDialogMessage;
|
||||||
assert setting.userDialogMessage != null;
|
if (context != null && userDialogMessage != null) {
|
||||||
new AlertDialog.Builder(context)
|
showingUserDialogMessage = true;
|
||||||
.setTitle(str("revanced_extended_confirm_user_dialog_title"))
|
|
||||||
.setMessage(setting.userDialogMessage.toString())
|
new AlertDialog.Builder(context)
|
||||||
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
.setTitle(str("revanced_extended_confirm_user_dialog_title"))
|
||||||
if (setting.rebootApp) {
|
.setMessage(userDialogMessage.toString())
|
||||||
showRestartDialog(context);
|
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
||||||
}
|
if (setting.rebootApp) {
|
||||||
})
|
showRestartDialog(context);
|
||||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
}
|
||||||
switchPreference.setChecked(setting.defaultValue); // Recursive call that resets the Setting value.
|
})
|
||||||
})
|
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||||
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
|
// Restore whatever the setting was before the change.
|
||||||
.setCancelable(false)
|
if (setting instanceof BooleanSetting booleanSetting &&
|
||||||
.show();
|
pref instanceof SwitchPreference switchPreference) {
|
||||||
|
switchPreference.setChecked(booleanSetting.defaultValue);
|
||||||
|
} else if (setting instanceof EnumSetting<?> enumSetting &&
|
||||||
|
pref instanceof ListPreference listPreference) {
|
||||||
|
listPreference.setValue(enumSetting.defaultValue.toString());
|
||||||
|
updateListPreferenceSummary(listPreference, setting);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
|
||||||
|
.setCancelable(false)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static PreferenceManager mPreferenceManager;
|
static PreferenceManager mPreferenceManager;
|
||||||
@ -197,6 +238,9 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Integer targetSDKVersion = ExtendedUtils.getTargetSDKVersion(getContext().getPackageName());
|
||||||
|
boolean isEdgeToEdgeSupported = isSDKAbove(35) && targetSDKVersion != null && targetSDKVersion >= 35;
|
||||||
|
|
||||||
for (PreferenceScreen mPreferenceScreen : preferenceScreenMap.values()) {
|
for (PreferenceScreen mPreferenceScreen : preferenceScreenMap.values()) {
|
||||||
mPreferenceScreen.setOnPreferenceClickListener(
|
mPreferenceScreen.setOnPreferenceClickListener(
|
||||||
preferenceScreen -> {
|
preferenceScreen -> {
|
||||||
@ -205,11 +249,24 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
|||||||
.findViewById(android.R.id.content)
|
.findViewById(android.R.id.content)
|
||||||
.getParent();
|
.getParent();
|
||||||
|
|
||||||
// Fix required for Android 15
|
// Edge-to-edge is enforced if the following conditions are met:
|
||||||
if (isSDKAbove(35)) {
|
// 1. targetSDK is 35 or greater (YouTube 19.44.39 or greater).
|
||||||
|
// 2. user is using Android 15 or greater.
|
||||||
|
//
|
||||||
|
// Related Issues:
|
||||||
|
// https://github.com/ReVanced/revanced-patches/issues/3976
|
||||||
|
// https://github.com/ReVanced/revanced-patches/issues/4606
|
||||||
|
//
|
||||||
|
// Docs:
|
||||||
|
// https://developer.android.com/develop/ui/views/layout/edge-to-edge#system-bars-insets
|
||||||
|
//
|
||||||
|
// Since ReVanced Settings Activity do not use AndroidX libraries,
|
||||||
|
// You will need to manually fix the layout breakage caused by edge-to-edge.
|
||||||
|
if (isEdgeToEdgeSupported) {
|
||||||
rootView.setOnApplyWindowInsetsListener((v, insets) -> {
|
rootView.setOnApplyWindowInsetsListener((v, insets) -> {
|
||||||
Insets statusInsets = insets.getInsets(WindowInsets.Type.statusBars());
|
Insets statusInsets = insets.getInsets(WindowInsets.Type.statusBars());
|
||||||
v.setPadding(0, statusInsets.top, 0, 0);
|
Insets navInsets = insets.getInsets(WindowInsets.Type.navigationBars());
|
||||||
|
v.setPadding(0, statusInsets.top, 0, navInsets.bottom);
|
||||||
return insets;
|
return insets;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -283,9 +340,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
|||||||
} else if (preference instanceof EditTextPreference editTextPreference) {
|
} else if (preference instanceof EditTextPreference editTextPreference) {
|
||||||
editTextPreference.setText(setting.get().toString());
|
editTextPreference.setText(setting.get().toString());
|
||||||
} else if (preference instanceof ListPreference listPreference) {
|
} else if (preference instanceof ListPreference listPreference) {
|
||||||
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
|
if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
|
||||||
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
|
listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
|
||||||
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
|
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getEntryValues());
|
||||||
|
}
|
||||||
|
if (setting.equals(SPOOF_STREAMING_DATA_TYPE)) {
|
||||||
|
listPreference.setEntries(SpoofStreamingDataPatch.getEntries());
|
||||||
|
listPreference.setEntryValues(SpoofStreamingDataPatch.getEntryValues());
|
||||||
}
|
}
|
||||||
if (!(preference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
|
if (!(preference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
|
||||||
updateListPreferenceSummary(listPreference, setting);
|
updateListPreferenceSummary(listPreference, setting);
|
||||||
@ -298,6 +359,10 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
|||||||
|
|
||||||
originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity());
|
originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity());
|
||||||
copyPreferences(getPreferenceScreen(), originalPreferenceScreen);
|
copyPreferences(getPreferenceScreen(), originalPreferenceScreen);
|
||||||
|
|
||||||
|
sortPreferenceListMenu(Settings.CHANGE_START_PAGE);
|
||||||
|
sortPreferenceListMenu(Settings.SPOOF_STREAMING_DATA_LANGUAGE);
|
||||||
|
sortPreferenceListMenu(BaseSettings.REVANCED_LANGUAGE);
|
||||||
} catch (Exception th) {
|
} catch (Exception th) {
|
||||||
Logger.printException(() -> "Error during onCreate()", th);
|
Logger.printException(() -> "Error during onCreate()", th);
|
||||||
}
|
}
|
||||||
@ -312,9 +377,69 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroy() {
|
public void onDestroy() {
|
||||||
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
|
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
|
||||||
|
Utils.resetLocalizedContext();
|
||||||
super.onDestroy();
|
super.onDestroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts a preference list by menu entries, but preserves the first value as the first entry.
|
||||||
|
*
|
||||||
|
* @noinspection SameParameterValue
|
||||||
|
*/
|
||||||
|
private static void sortListPreferenceByValues(ListPreference listPreference, int firstEntriesToPreserve) {
|
||||||
|
CharSequence[] entries = listPreference.getEntries();
|
||||||
|
CharSequence[] entryValues = listPreference.getEntryValues();
|
||||||
|
final int entrySize = entries.length;
|
||||||
|
|
||||||
|
if (entrySize != entryValues.length) {
|
||||||
|
// Xml array declaration has a missing/extra entry.
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since the text of Preference is Spanned, CharSequence#toString() should not be used.
|
||||||
|
// If CharSequence#toString() is used, Spanned styling, such as HTML syntax, will be broken.
|
||||||
|
List<Pair<CharSequence, CharSequence>> firstPairs = new ArrayList<>(firstEntriesToPreserve);
|
||||||
|
List<Pair<CharSequence, CharSequence>> pairsToSort = new ArrayList<>(entrySize);
|
||||||
|
|
||||||
|
for (int i = 0; i < entrySize; i++) {
|
||||||
|
Pair<CharSequence, CharSequence> pair = new Pair<>(entries[i], entryValues[i]);
|
||||||
|
if (i < firstEntriesToPreserve) {
|
||||||
|
firstPairs.add(pair);
|
||||||
|
} else {
|
||||||
|
pairsToSort.add(pair);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pairsToSort.sort((pair1, pair2)
|
||||||
|
-> pair1.first.toString().compareToIgnoreCase(pair2.first.toString()));
|
||||||
|
|
||||||
|
CharSequence[] sortedEntries = new CharSequence[entrySize];
|
||||||
|
CharSequence[] sortedEntryValues = new CharSequence[entrySize];
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (Pair<CharSequence, CharSequence> pair : firstPairs) {
|
||||||
|
sortedEntries[i] = pair.first;
|
||||||
|
sortedEntryValues[i] = pair.second;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Pair<CharSequence, CharSequence> pair : pairsToSort) {
|
||||||
|
sortedEntries[i] = pair.first;
|
||||||
|
sortedEntryValues[i] = pair.second;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
listPreference.setEntries(sortedEntries);
|
||||||
|
listPreference.setEntryValues(sortedEntryValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sortPreferenceListMenu(EnumSetting<?> setting) {
|
||||||
|
Preference preference = findPreference(setting.key);
|
||||||
|
if (preference instanceof ListPreference languagePreference) {
|
||||||
|
sortListPreferenceByValues(languagePreference, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively stores all preferences and their dependencies grouped by their parent PreferenceGroup.
|
* Recursively stores all preferences and their dependencies grouped by their parent PreferenceGroup.
|
||||||
*
|
*
|
||||||
|
@ -2,7 +2,6 @@ package app.revanced.extension.youtube.settings.preference;
|
|||||||
|
|
||||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
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.isSDKAbove;
|
||||||
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
|
|
||||||
|
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.SwitchPreference;
|
import android.preference.SwitchPreference;
|
||||||
@ -43,11 +42,11 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
|
|||||||
enableDisablePreferences();
|
enableDisablePreferences();
|
||||||
|
|
||||||
AmbientModePreferenceLinks();
|
AmbientModePreferenceLinks();
|
||||||
ExternalDownloaderPreferenceLinks();
|
|
||||||
FullScreenPanelPreferenceLinks();
|
FullScreenPanelPreferenceLinks();
|
||||||
NavigationPreferenceLinks();
|
NavigationPreferenceLinks();
|
||||||
RYDPreferenceLinks();
|
RYDPreferenceLinks();
|
||||||
SeekBarPreferenceLinks();
|
SeekBarPreferenceLinks();
|
||||||
|
ShortsPreferenceLinks();
|
||||||
SpeedOverlayPreferenceLinks();
|
SpeedOverlayPreferenceLinks();
|
||||||
QuickActionsPreferenceLinks();
|
QuickActionsPreferenceLinks();
|
||||||
TabletLayoutLinks();
|
TabletLayoutLinks();
|
||||||
@ -65,18 +64,6 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable/Disable Preference for External downloader settings
|
|
||||||
*/
|
|
||||||
private static void ExternalDownloaderPreferenceLinks() {
|
|
||||||
// Override download button will not work if spoofed with YouTube 18.24.xx or earlier.
|
|
||||||
enableDisablePreferences(
|
|
||||||
isSpoofingToLessThan("18.24.00"),
|
|
||||||
Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON,
|
|
||||||
Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable/Disable Preferences not working in tablet layout
|
* Enable/Disable Preferences not working in tablet layout
|
||||||
*/
|
*/
|
||||||
@ -200,6 +187,19 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable/Disable Preference related to Shorts settings
|
||||||
|
*/
|
||||||
|
private static void ShortsPreferenceLinks() {
|
||||||
|
if (!PatchStatus.RememberPlaybackSpeed()) {
|
||||||
|
enableDisablePreferences(
|
||||||
|
true,
|
||||||
|
Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG
|
||||||
|
);
|
||||||
|
Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG.save(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable/Disable Preference related to Speed overlay settings
|
* Enable/Disable Preference related to Speed overlay settings
|
||||||
*/
|
*/
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||||
|
import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
@ -12,41 +13,51 @@ import android.text.InputType;
|
|||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.GridLayout;
|
||||||
import android.widget.TableLayout;
|
|
||||||
import android.widget.TableRow;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
|
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
||||||
|
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
public class SegmentCategoryListPreference extends ListPreference {
|
public class SegmentCategoryListPreference extends ListPreference {
|
||||||
private SegmentCategory mCategory;
|
private SegmentCategory category;
|
||||||
private EditText mEditText;
|
private TextView colorDotView;
|
||||||
private int mClickedDialogEntryIndex;
|
private EditText colorEditText;
|
||||||
|
private EditText opacityEditText;
|
||||||
|
/**
|
||||||
|
* #RRGGBB
|
||||||
|
*/
|
||||||
|
private int categoryColor;
|
||||||
|
/**
|
||||||
|
* [0, 1]
|
||||||
|
*/
|
||||||
|
private float categoryOpacity;
|
||||||
|
private int selectedDialogEntryIndex;
|
||||||
|
|
||||||
private void init() {
|
private void init() {
|
||||||
final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey());
|
final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey());
|
||||||
final boolean isHighlightCategory = segmentCategory == SegmentCategory.HIGHLIGHT;
|
category = Objects.requireNonNull(segmentCategory);
|
||||||
mCategory = Objects.requireNonNull(segmentCategory);
|
|
||||||
// Edit: Using preferences to sync together multiple pieces
|
// Edit: Using preferences to sync together multiple pieces
|
||||||
// of code together is messy and should be rethought.
|
// of code is messy and should be rethought.
|
||||||
setKey(segmentCategory.behaviorSetting.key);
|
setKey(segmentCategory.behaviorSetting.key);
|
||||||
setDefaultValue(segmentCategory.behaviorSetting.defaultValue);
|
setDefaultValue(segmentCategory.behaviorSetting.defaultValue);
|
||||||
|
|
||||||
|
final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
|
||||||
setEntries(isHighlightCategory
|
setEntries(isHighlightCategory
|
||||||
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
|
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
|
||||||
: CategoryBehaviour.getBehaviorDescriptions());
|
: CategoryBehaviour.getBehaviorDescriptions());
|
||||||
setEntryValues(isHighlightCategory
|
setEntryValues(isHighlightCategory
|
||||||
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
|
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
|
||||||
: CategoryBehaviour.getBehaviorKeyValues());
|
: CategoryBehaviour.getBehaviorKeyValues());
|
||||||
updateTitle();
|
|
||||||
|
updateTitleFromCategory();
|
||||||
}
|
}
|
||||||
|
|
||||||
public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
@ -73,28 +84,41 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||||
try {
|
try {
|
||||||
Utils.setEditTextDialogTheme(builder);
|
Utils.setEditTextDialogTheme(builder);
|
||||||
super.onPrepareDialogBuilder(builder);
|
|
||||||
|
categoryColor = category.getColorNoOpacity();
|
||||||
|
categoryOpacity = category.getOpacity();
|
||||||
|
|
||||||
Context context = builder.getContext();
|
Context context = builder.getContext();
|
||||||
TableLayout table = new TableLayout(context);
|
GridLayout gridLayout = new GridLayout(context);
|
||||||
table.setOrientation(LinearLayout.HORIZONTAL);
|
gridLayout.setPadding(70, 0, 150, 0); // Padding for the entire layout.
|
||||||
table.setPadding(70, 0, 150, 0);
|
gridLayout.setColumnCount(3);
|
||||||
|
gridLayout.setRowCount(2);
|
||||||
TableRow row = new TableRow(context);
|
|
||||||
|
|
||||||
|
GridLayout.LayoutParams gridParams = new GridLayout.LayoutParams();
|
||||||
|
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
||||||
|
gridParams.columnSpec = GridLayout.spec(0); // First column.
|
||||||
TextView colorTextLabel = new TextView(context);
|
TextView colorTextLabel = new TextView(context);
|
||||||
colorTextLabel.setText(str("revanced_sb_color_dot_label"));
|
colorTextLabel.setText(str("revanced_sb_color_dot_label"));
|
||||||
row.addView(colorTextLabel);
|
colorTextLabel.setLayoutParams(gridParams);
|
||||||
|
gridLayout.addView(colorTextLabel);
|
||||||
|
|
||||||
TextView colorDotView = new TextView(context);
|
gridParams = new GridLayout.LayoutParams();
|
||||||
colorDotView.setText(mCategory.getCategoryColorDot());
|
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
||||||
colorDotView.setPadding(30, 0, 30, 0);
|
gridParams.columnSpec = GridLayout.spec(1); // Second column.
|
||||||
row.addView(colorDotView);
|
gridParams.setMargins(0, 0, 10, 0);
|
||||||
|
colorDotView = new TextView(context);
|
||||||
|
colorDotView.setLayoutParams(gridParams);
|
||||||
|
gridLayout.addView(colorDotView);
|
||||||
|
updateCategoryColorDot();
|
||||||
|
|
||||||
mEditText = new EditText(context);
|
gridParams = new GridLayout.LayoutParams();
|
||||||
mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
|
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
||||||
mEditText.setText(mCategory.colorString());
|
gridParams.columnSpec = GridLayout.spec(2); // Third column.
|
||||||
mEditText.addTextChangedListener(new TextWatcher() {
|
colorEditText = new EditText(context);
|
||||||
|
colorEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
|
||||||
|
colorEditText.setTextLocale(Locale.US);
|
||||||
|
colorEditText.setText(category.getColorString());
|
||||||
|
colorEditText.addTextChangedListener(new TextWatcher() {
|
||||||
@Override
|
@Override
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
}
|
}
|
||||||
@ -104,44 +128,111 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterTextChanged(Editable s) {
|
public void afterTextChanged(Editable edit) {
|
||||||
try {
|
try {
|
||||||
String colorString = s.toString();
|
String colorString = edit.toString();
|
||||||
|
final int colorStringLength = colorString.length();
|
||||||
|
|
||||||
if (!colorString.startsWith("#")) {
|
if (!colorString.startsWith("#")) {
|
||||||
s.insert(0, "#"); // recursively calls back into this method
|
edit.insert(0, "#"); // Recursively calls back into this method.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (colorString.length() > 7) {
|
|
||||||
s.delete(7, colorString.length());
|
final int maxColorStringLength = 7; // #RRGGBB
|
||||||
|
if (colorStringLength > maxColorStringLength) {
|
||||||
|
edit.delete(maxColorStringLength, colorStringLength);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final int color = Color.parseColor(colorString);
|
|
||||||
colorDotView.setText(SegmentCategory.getCategoryColorDot(color));
|
categoryColor = Color.parseColor(colorString);
|
||||||
|
updateCategoryColorDot();
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
// ignore
|
// Ignore.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f));
|
colorEditText.setLayoutParams(gridParams);
|
||||||
row.addView(mEditText);
|
gridLayout.addView(colorEditText);
|
||||||
|
|
||||||
table.addView(row);
|
gridParams = new GridLayout.LayoutParams();
|
||||||
builder.setView(table);
|
gridParams.rowSpec = GridLayout.spec(1); // Second row.
|
||||||
builder.setTitle(mCategory.title.toString());
|
gridParams.columnSpec = GridLayout.spec(0, 1); // First and second column.
|
||||||
|
TextView opacityLabel = new TextView(context);
|
||||||
|
opacityLabel.setText(str("revanced_sb_color_opacity_label"));
|
||||||
|
opacityLabel.setLayoutParams(gridParams);
|
||||||
|
gridLayout.addView(opacityLabel);
|
||||||
|
|
||||||
|
gridParams = new GridLayout.LayoutParams();
|
||||||
|
gridParams.rowSpec = GridLayout.spec(1); // Second row.
|
||||||
|
gridParams.columnSpec = GridLayout.spec(2); // Third column.
|
||||||
|
opacityEditText = new EditText(context);
|
||||||
|
opacityEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
||||||
|
opacityEditText.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable edit) {
|
||||||
|
try {
|
||||||
|
String editString = edit.toString();
|
||||||
|
final int opacityStringLength = editString.length();
|
||||||
|
|
||||||
|
final int maxOpacityStringLength = 4; // [0.00, 1.00]
|
||||||
|
if (opacityStringLength > maxOpacityStringLength) {
|
||||||
|
edit.delete(maxOpacityStringLength, opacityStringLength);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final float opacity = opacityStringLength == 0
|
||||||
|
? 0
|
||||||
|
: Float.parseFloat(editString);
|
||||||
|
if (opacity < 0) {
|
||||||
|
categoryOpacity = 0;
|
||||||
|
edit.replace(0, opacityStringLength, "0");
|
||||||
|
return;
|
||||||
|
} else if (opacity > 1.0f) {
|
||||||
|
categoryOpacity = 1;
|
||||||
|
edit.replace(0, opacityStringLength, "1.0");
|
||||||
|
return;
|
||||||
|
} else if (!editString.endsWith(".")) {
|
||||||
|
// Ignore "0." and "1." until the user finishes entering a valid number.
|
||||||
|
categoryOpacity = opacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCategoryColorDot();
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
// Should never happen.
|
||||||
|
Logger.printException(() -> "Could not parse opacity string", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
opacityEditText.setLayoutParams(gridParams);
|
||||||
|
gridLayout.addView(opacityEditText);
|
||||||
|
updateOpacityText();
|
||||||
|
|
||||||
|
builder.setView(gridLayout);
|
||||||
|
builder.setTitle(category.title.toString());
|
||||||
|
|
||||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> onClick(dialog, DialogInterface.BUTTON_POSITIVE));
|
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> onClick(dialog, DialogInterface.BUTTON_POSITIVE));
|
||||||
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
|
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
|
||||||
try {
|
try {
|
||||||
mCategory.resetColor();
|
category.resetColorAndOpacity();
|
||||||
updateTitle();
|
updateTitleFromCategory();
|
||||||
Utils.showToastShort(str("revanced_sb_color_reset"));
|
Utils.showToastShort(str("revanced_sb_color_reset"));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "setNeutralButton failure", ex);
|
Logger.printException(() -> "setNeutralButton failure", ex);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
builder.setNegativeButton(android.R.string.cancel, null);
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
mClickedDialogEntryIndex = findIndexOfValue(getValue());
|
|
||||||
builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which);
|
selectedDialogEntryIndex = findIndexOfValue(getValue());
|
||||||
|
builder.setSingleChoiceItems(getEntries(), selectedDialogEntryIndex,
|
||||||
|
(dialog, which) -> selectedDialogEntryIndex = which);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
||||||
}
|
}
|
||||||
@ -150,31 +241,50 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
@Override
|
@Override
|
||||||
protected void onDialogClosed(boolean positiveResult) {
|
protected void onDialogClosed(boolean positiveResult) {
|
||||||
try {
|
try {
|
||||||
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) {
|
if (positiveResult && selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
|
||||||
String value = getEntryValues()[mClickedDialogEntryIndex].toString();
|
String value = getEntryValues()[selectedDialogEntryIndex].toString();
|
||||||
if (callChangeListener(value)) {
|
if (callChangeListener(value)) {
|
||||||
setValue(value);
|
setValue(value);
|
||||||
mCategory.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
|
category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
|
||||||
SegmentCategory.updateEnabledCategories();
|
SegmentCategory.updateEnabledCategories();
|
||||||
}
|
}
|
||||||
String colorString = mEditText.getText().toString();
|
|
||||||
try {
|
try {
|
||||||
if (!colorString.equals(mCategory.colorString())) {
|
String colorString = colorEditText.getText().toString();
|
||||||
mCategory.setColor(colorString);
|
if (!colorString.equals(category.getColorString()) || categoryOpacity != category.getOpacity()) {
|
||||||
|
category.setColor(colorString);
|
||||||
|
category.setOpacity(categoryOpacity);
|
||||||
Utils.showToastShort(str("revanced_sb_color_changed"));
|
Utils.showToastShort(str("revanced_sb_color_changed"));
|
||||||
}
|
}
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
Utils.showToastShort(str("revanced_sb_color_invalid"));
|
Utils.showToastShort(str("revanced_sb_color_invalid"));
|
||||||
}
|
}
|
||||||
updateTitle();
|
|
||||||
|
updateTitleFromCategory();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "onDialogClosed failure", ex);
|
Logger.printException(() -> "onDialogClosed failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTitle() {
|
private void applyOpacityToCategoryColor() {
|
||||||
setTitle(mCategory.getTitleWithColorDot());
|
categoryColor = applyOpacityToColor(categoryColor, categoryOpacity);
|
||||||
setEnabled(Settings.SB_ENABLED.get());
|
}
|
||||||
|
|
||||||
|
private void updateTitleFromCategory() {
|
||||||
|
categoryColor = category.getColorNoOpacity();
|
||||||
|
categoryOpacity = category.getOpacity();
|
||||||
|
applyOpacityToCategoryColor();
|
||||||
|
|
||||||
|
setTitle(category.getTitleWithColorDot(categoryColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCategoryColorDot() {
|
||||||
|
applyOpacityToCategoryColor();
|
||||||
|
|
||||||
|
colorDotView.setText(SegmentCategory.getCategoryColorDot(categoryColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateOpacityText() {
|
||||||
|
opacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -233,6 +233,7 @@ public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
|
|||||||
statsCategory = new PreferenceCategory(mActivity);
|
statsCategory = new PreferenceCategory(mActivity);
|
||||||
statsCategory.setLayoutResource(preferencesCategoryLayout);
|
statsCategory.setLayoutResource(preferencesCategoryLayout);
|
||||||
statsCategory.setTitle(str("revanced_sb_stats"));
|
statsCategory.setTitle(str("revanced_sb_stats"));
|
||||||
|
statsCategory.setEnabled(Settings.SB_ENABLED.get());
|
||||||
mPreferenceScreen.addPreference(statsCategory);
|
mPreferenceScreen.addPreference(statsCategory);
|
||||||
fetchAndDisplayStats();
|
fetchAndDisplayStats();
|
||||||
|
|
||||||
@ -261,7 +262,6 @@ public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
|
|||||||
final String key = category.keyValue;
|
final String key = category.keyValue;
|
||||||
if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) {
|
if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) {
|
||||||
segmentCategoryListPreference.setTitle(category.getTitleWithColorDot());
|
segmentCategoryListPreference.setTitle(category.getTitleWithColorDot());
|
||||||
segmentCategoryListPreference.setEnabled(Settings.SB_ENABLED.get());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
@ -58,6 +58,10 @@ public final class RootView {
|
|||||||
return PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get();
|
return PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isShortsActive() {
|
||||||
|
return ShortsPlayerState.getCurrent().isOpen();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current BrowseId.
|
* Get current BrowseId.
|
||||||
* Rest of the implementation added by patch.
|
* Rest of the implementation added by patch.
|
||||||
|
@ -48,4 +48,12 @@ enum class ShortsPlayerState {
|
|||||||
fun isClosed(): Boolean {
|
fun isClosed(): Boolean {
|
||||||
return this == CLOSED
|
return this == CLOSED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the shorts player is [OPEN].
|
||||||
|
* Useful for checking if a shorts player is open.
|
||||||
|
*/
|
||||||
|
fun isOpen(): Boolean {
|
||||||
|
return this == OPEN
|
||||||
|
}
|
||||||
}
|
}
|
@ -139,7 +139,7 @@ public class SponsorBlockSettings {
|
|||||||
for (SegmentCategory category : categories) {
|
for (SegmentCategory category : categories) {
|
||||||
JSONObject categoryObject = new JSONObject();
|
JSONObject categoryObject = new JSONObject();
|
||||||
String categoryKey = category.keyValue;
|
String categoryKey = category.keyValue;
|
||||||
categoryObject.put("color", category.colorString());
|
categoryObject.put("color", category.getColorString());
|
||||||
barTypesObject.put(categoryKey, categoryObject);
|
barTypesObject.put(categoryKey, categoryObject);
|
||||||
|
|
||||||
if (category.behaviour != CategoryBehaviour.IGNORE) {
|
if (category.behaviour != CategoryBehaviour.IGNORE) {
|
||||||
|
@ -6,7 +6,12 @@ import android.annotation.TargetApi;
|
|||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.text.Html;
|
import android.graphics.Color;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
import android.text.style.StyleSpan;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@ -32,11 +37,9 @@ import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Not thread safe. All fields/methods must be accessed from the main thread.
|
* Not thread safe. All fields/methods must be accessed from the main thread.
|
||||||
*
|
|
||||||
* @noinspection deprecation
|
|
||||||
*/
|
*/
|
||||||
public class SponsorBlockUtils {
|
public class SponsorBlockUtils {
|
||||||
private static final String LOCKED_COLOR = "#FFC83D";
|
private static final int LOCKED_COLOR = Color.parseColor("#FFC83D");
|
||||||
|
|
||||||
private static final String MANUAL_EDIT_TIME_TEXT_HINT = "hh:mm:ss.sss";
|
private static final String MANUAL_EDIT_TIME_TEXT_HINT = "hh:mm:ss.sss";
|
||||||
private static final Pattern manualEditTimePattern
|
private static final Pattern manualEditTimePattern
|
||||||
@ -162,28 +165,34 @@ public class SponsorBlockUtils {
|
|||||||
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
|
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
|
||||||
? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category
|
? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category
|
||||||
: SegmentVote.values();
|
: SegmentVote.values();
|
||||||
CharSequence[] items = new CharSequence[voteOptions.length];
|
final int voteOptionsLength = voteOptions.length;
|
||||||
|
final boolean userIsVip = Settings.SB_USER_IS_VIP.get();
|
||||||
|
CharSequence[] items = new CharSequence[voteOptionsLength];
|
||||||
|
|
||||||
for (int i = 0; i < voteOptions.length; i++) {
|
for (int i = 0; i < voteOptionsLength; i++) {
|
||||||
SegmentVote voteOption = voteOptions[i];
|
SegmentVote voteOption = voteOptions[i];
|
||||||
String title = voteOption.title.toString();
|
CharSequence title = voteOption.title.toString();
|
||||||
if (Settings.SB_USER_IS_VIP.get() && segment.isLocked && voteOption.shouldHighlight) {
|
if (userIsVip && segment.isLocked && voteOption.highlightIfVipAndVideoIsLocked) {
|
||||||
items[i] = Html.fromHtml(String.format("<font color=\"%s\">%s</font>", LOCKED_COLOR, title));
|
SpannableString coloredTitle = new SpannableString(title);
|
||||||
} else {
|
coloredTitle.setSpan(new ForegroundColorSpan(LOCKED_COLOR),
|
||||||
items[i] = title;
|
0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
title = coloredTitle;
|
||||||
}
|
}
|
||||||
|
items[i] = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context).setItems(items, (dialog1, which1) -> {
|
||||||
.setItems(items, (dialog1, which1) -> {
|
SegmentVote voteOption = voteOptions[which1];
|
||||||
SegmentVote voteOption = voteOptions[which1];
|
switch (voteOption) {
|
||||||
switch (voteOption) {
|
case UPVOTE:
|
||||||
case UPVOTE, DOWNVOTE ->
|
case DOWNVOTE:
|
||||||
SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
|
SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
|
||||||
case CATEGORY_CHANGE -> onNewCategorySelect(segment, context);
|
break;
|
||||||
}
|
case CATEGORY_CHANGE:
|
||||||
})
|
onNewCategorySelect(segment, context);
|
||||||
.show();
|
break;
|
||||||
|
}
|
||||||
|
}).show();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "segmentVoteClickListener failure", ex);
|
Logger.printException(() -> "segmentVoteClickListener failure", ex);
|
||||||
}
|
}
|
||||||
@ -287,22 +296,33 @@ public class SponsorBlockUtils {
|
|||||||
if (segment.category == SegmentCategory.UNSUBMITTED) {
|
if (segment.category == SegmentCategory.UNSUBMITTED) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
StringBuilder htmlBuilder = new StringBuilder();
|
|
||||||
htmlBuilder.append(String.format("<b><font color=\"#%06X\">⬤</font> %s<br>",
|
SpannableStringBuilder spannableBuilder = new SpannableStringBuilder();
|
||||||
segment.category.color, segment.category.title));
|
|
||||||
htmlBuilder.append(formatSegmentTime(segment.start));
|
spannableBuilder.append(segment.category.getTitleWithColorDot());
|
||||||
if (segment.category != SegmentCategory.HIGHLIGHT) {
|
spannableBuilder.append('\n');
|
||||||
htmlBuilder.append(" to ").append(formatSegmentTime(segment.end));
|
|
||||||
|
String startTime = formatSegmentTime(segment.start);
|
||||||
|
if (segment.category == SegmentCategory.HIGHLIGHT) {
|
||||||
|
spannableBuilder.append(startTime);
|
||||||
|
} else {
|
||||||
|
String toFromString = str("revanced_sb_vote_segment_time_to_from",
|
||||||
|
startTime, formatSegmentTime(segment.end));
|
||||||
|
spannableBuilder.append(toFromString);
|
||||||
}
|
}
|
||||||
htmlBuilder.append("</b>");
|
|
||||||
if (i + 1 != numberOfSegments) // prevents trailing new line after last segment
|
if (i + 1 != numberOfSegments) {
|
||||||
htmlBuilder.append("<br>");
|
// prevents trailing new line after last segment
|
||||||
titles[i] = Html.fromHtml(htmlBuilder.toString());
|
spannableBuilder.append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
spannableBuilder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD),
|
||||||
|
0, spannableBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
titles[i] = spannableBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context).setItems(titles, segmentVoteClickListener).show();
|
||||||
.setItems(titles, segmentVoteClickListener)
|
|
||||||
.show();
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "onVotingClicked failure", ex);
|
Logger.printException(() -> "onVotingClicked failure", ex);
|
||||||
}
|
}
|
||||||
|
@ -3,30 +3,41 @@ package app.revanced.extension.youtube.sponsorblock.objects;
|
|||||||
import static app.revanced.extension.shared.utils.StringRef.sf;
|
import static app.revanced.extension.shared.utils.StringRef.sf;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_COLOR;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_COLOR;
|
||||||
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_OPACITY;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_COLOR;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_COLOR;
|
||||||
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_OPACITY;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_COLOR;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_COLOR;
|
||||||
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_OPACITY;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_COLOR;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_COLOR;
|
||||||
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_OPACITY;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_COLOR;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_COLOR;
|
||||||
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_COLOR;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_COLOR;
|
||||||
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_OPACITY;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_COLOR;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_COLOR;
|
||||||
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_OPACITY;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO_COLOR;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO_COLOR;
|
||||||
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO_OPACITY;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_COLOR;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_COLOR;
|
||||||
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_OPACITY;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_COLOR;
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_COLOR;
|
||||||
|
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_OPACITY;
|
||||||
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.text.Html;
|
import android.text.Spannable;
|
||||||
import android.text.Spanned;
|
import android.text.SpannableString;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@ -34,45 +45,46 @@ import androidx.annotation.Nullable;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.FloatSetting;
|
||||||
import app.revanced.extension.shared.settings.StringSetting;
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
import app.revanced.extension.shared.utils.Logger;
|
import app.revanced.extension.shared.utils.Logger;
|
||||||
import app.revanced.extension.shared.utils.StringRef;
|
import app.revanced.extension.shared.utils.StringRef;
|
||||||
import app.revanced.extension.shared.utils.Utils;
|
import app.revanced.extension.shared.utils.Utils;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@SuppressWarnings({"deprecation", "StaticFieldLeak"})
|
@SuppressWarnings("StaticFieldLeak")
|
||||||
public enum SegmentCategory {
|
public enum SegmentCategory {
|
||||||
SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"),
|
SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"),
|
||||||
SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR),
|
SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR, SB_CATEGORY_SPONSOR_OPACITY),
|
||||||
SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"),
|
SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"),
|
||||||
SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR),
|
SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR, SB_CATEGORY_SELF_PROMO_OPACITY),
|
||||||
INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"),
|
INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"),
|
||||||
SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR),
|
SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR, SB_CATEGORY_INTERACTION_OPACITY),
|
||||||
/**
|
/**
|
||||||
* Unique category that is treated differently than the rest.
|
* Unique category that is treated differently than the rest.
|
||||||
*/
|
*/
|
||||||
HIGHLIGHT("poi_highlight", sf("revanced_sb_segments_highlight"), sf("revanced_sb_skip_button_highlight"), sf("revanced_sb_skipped_highlight"),
|
HIGHLIGHT("poi_highlight", sf("revanced_sb_segments_highlight"), sf("revanced_sb_skip_button_highlight"), sf("revanced_sb_skipped_highlight"),
|
||||||
SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR),
|
SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR, SB_CATEGORY_HIGHLIGHT_OPACITY),
|
||||||
INTRO("intro", sf("revanced_sb_segments_intro"),
|
INTRO("intro", sf("revanced_sb_segments_intro"),
|
||||||
sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"),
|
sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"),
|
||||||
sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"),
|
sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"),
|
||||||
SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR),
|
SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR, SB_CATEGORY_INTRO_OPACITY),
|
||||||
OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"),
|
OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"),
|
||||||
SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR),
|
SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR, SB_CATEGORY_OUTRO_OPACITY),
|
||||||
PREVIEW("preview", sf("revanced_sb_segments_preview"),
|
PREVIEW("preview", sf("revanced_sb_segments_preview"),
|
||||||
sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"),
|
sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"),
|
||||||
sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"),
|
sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"),
|
||||||
SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR),
|
SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR, SB_CATEGORY_PREVIEW_OPACITY),
|
||||||
FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"),
|
FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"),
|
||||||
SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR),
|
SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR, SB_CATEGORY_FILLER_OPACITY),
|
||||||
MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"),
|
MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"),
|
||||||
SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR),
|
SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR, SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY),
|
||||||
UNSUBMITTED("unsubmitted", StringRef.empty, sf("revanced_sb_skip_button_unsubmitted"), sf("revanced_sb_skipped_unsubmitted"),
|
UNSUBMITTED("unsubmitted", StringRef.empty, sf("revanced_sb_skip_button_unsubmitted"), sf("revanced_sb_skipped_unsubmitted"),
|
||||||
SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR),
|
SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR, SB_CATEGORY_UNSUBMITTED_OPACITY);
|
||||||
;
|
|
||||||
|
|
||||||
private static final StringRef skipSponsorTextCompact = sf("revanced_sb_skip_button_compact");
|
private static final StringRef skipSponsorTextCompact = sf("revanced_sb_skip_button_compact");
|
||||||
private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight");
|
private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight");
|
||||||
@ -111,12 +123,10 @@ public enum SegmentCategory {
|
|||||||
mValuesMap.put(value.keyValue, value);
|
mValuesMap.put(value.keyValue, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
|
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
|
||||||
return categoriesWithoutUnsubmitted;
|
return categoriesWithoutUnsubmitted;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static SegmentCategory[] categoriesWithoutHighlights() {
|
public static SegmentCategory[] categoriesWithoutHighlights() {
|
||||||
return categoriesWithoutHighlights;
|
return categoriesWithoutHighlights;
|
||||||
}
|
}
|
||||||
@ -127,7 +137,7 @@ public enum SegmentCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Must be called if behavior of any category is changed
|
* Must be called if behavior of any category is changed.
|
||||||
*/
|
*/
|
||||||
public static void updateEnabledCategories() {
|
public static void updateEnabledCategories() {
|
||||||
Utils.verifyOnMainThread();
|
Utils.verifyOnMainThread();
|
||||||
@ -154,30 +164,32 @@ public enum SegmentCategory {
|
|||||||
updateEnabledCategories();
|
updateEnabledCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
public static int applyOpacityToColor(int color, float opacity) {
|
||||||
public final String keyValue;
|
if (opacity < 0 || opacity > 1.0f) {
|
||||||
@NonNull
|
throw new IllegalArgumentException("Invalid opacity: " + opacity);
|
||||||
public final StringSetting behaviorSetting;
|
}
|
||||||
@NonNull
|
final int opacityInt = (int) (255 * opacity);
|
||||||
private final StringSetting colorSetting;
|
return (color & 0x00FFFFFF) | (opacityInt << 24);
|
||||||
|
}
|
||||||
|
|
||||||
|
public final String keyValue;
|
||||||
|
public final StringSetting behaviorSetting; // TODO: Replace with EnumSetting.
|
||||||
|
private final StringSetting colorSetting;
|
||||||
|
private final FloatSetting opacitySetting;
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public final StringRef title;
|
public final StringRef title;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Skip button text, if the skip occurs in the first quarter of the video
|
* Skip button text, if the skip occurs in the first quarter of the video
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public final StringRef skipButtonTextBeginning;
|
public final StringRef skipButtonTextBeginning;
|
||||||
/**
|
/**
|
||||||
* Skip button text, if the skip occurs in the middle half of the video
|
* Skip button text, if the skip occurs in the middle half of the video
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public final StringRef skipButtonTextMiddle;
|
public final StringRef skipButtonTextMiddle;
|
||||||
/**
|
/**
|
||||||
* Skip button text, if the skip occurs in the last quarter of the video
|
* Skip button text, if the skip occurs in the last quarter of the video
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public final StringRef skipButtonTextEnd;
|
public final StringRef skipButtonTextEnd;
|
||||||
/**
|
/**
|
||||||
* Skipped segment toast, if the skip occurred in the first quarter of the video
|
* Skipped segment toast, if the skip occurred in the first quarter of the video
|
||||||
@ -198,10 +210,7 @@ public enum SegmentCategory {
|
|||||||
@NonNull
|
@NonNull
|
||||||
public final Paint paint;
|
public final Paint paint;
|
||||||
|
|
||||||
/**
|
private int color;
|
||||||
* Value must be changed using {@link #setColor(String)}.
|
|
||||||
*/
|
|
||||||
public int color;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
|
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
|
||||||
@ -213,17 +222,20 @@ public enum SegmentCategory {
|
|||||||
SegmentCategory(String keyValue, StringRef title,
|
SegmentCategory(String keyValue, StringRef title,
|
||||||
StringRef skipButtonText,
|
StringRef skipButtonText,
|
||||||
StringRef skippedToastText,
|
StringRef skippedToastText,
|
||||||
StringSetting behavior, StringSetting color) {
|
StringSetting behavior,
|
||||||
|
StringSetting color, FloatSetting opacity) {
|
||||||
this(keyValue, title,
|
this(keyValue, title,
|
||||||
skipButtonText, skipButtonText, skipButtonText,
|
skipButtonText, skipButtonText, skipButtonText,
|
||||||
skippedToastText, skippedToastText, skippedToastText,
|
skippedToastText, skippedToastText, skippedToastText,
|
||||||
behavior, color);
|
behavior,
|
||||||
|
color, opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
SegmentCategory(String keyValue, StringRef title,
|
SegmentCategory(String keyValue, StringRef title,
|
||||||
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
|
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
|
||||||
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
|
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
|
||||||
StringSetting behavior, StringSetting color) {
|
StringSetting behavior,
|
||||||
|
StringSetting color, FloatSetting opacity) {
|
||||||
this.keyValue = Objects.requireNonNull(keyValue);
|
this.keyValue = Objects.requireNonNull(keyValue);
|
||||||
this.title = Objects.requireNonNull(title);
|
this.title = Objects.requireNonNull(title);
|
||||||
this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning);
|
this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning);
|
||||||
@ -234,6 +246,7 @@ public enum SegmentCategory {
|
|||||||
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
|
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
|
||||||
this.behaviorSetting = Objects.requireNonNull(behavior);
|
this.behaviorSetting = Objects.requireNonNull(behavior);
|
||||||
this.colorSetting = Objects.requireNonNull(color);
|
this.colorSetting = Objects.requireNonNull(color);
|
||||||
|
this.opacitySetting = Objects.requireNonNull(opacity);
|
||||||
this.paint = new Paint();
|
this.paint = new Paint();
|
||||||
loadFromSettings();
|
loadFromSettings();
|
||||||
}
|
}
|
||||||
@ -250,11 +263,14 @@ public enum SegmentCategory {
|
|||||||
this.behaviour = savedBehavior;
|
this.behaviour = savedBehavior;
|
||||||
|
|
||||||
String colorString = colorSetting.get();
|
String colorString = colorSetting.get();
|
||||||
|
final float opacity = opacitySetting.get();
|
||||||
try {
|
try {
|
||||||
setColor(colorString);
|
setColor(colorString);
|
||||||
|
setOpacity(opacity);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "Invalid color: " + colorString, ex);
|
Logger.printException(() -> "Invalid color: " + colorString + " opacity: " + opacity, ex);
|
||||||
colorSetting.resetToDefault();
|
colorSetting.resetToDefault();
|
||||||
|
opacitySetting.resetToDefault();
|
||||||
loadFromSettings();
|
loadFromSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,45 +280,77 @@ public enum SegmentCategory {
|
|||||||
this.behaviorSetting.save(behaviour.reVancedKeyValue);
|
this.behaviorSetting.save(behaviour.reVancedKeyValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private void updateColor() {
|
||||||
* @return HTML color format string
|
color = applyOpacityToColor(color, opacitySetting.get());
|
||||||
*/
|
|
||||||
@NonNull
|
|
||||||
public String colorString() {
|
|
||||||
return String.format("#%06X", color);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setColor(@NonNull String colorString) throws IllegalArgumentException {
|
|
||||||
final int color = Color.parseColor(colorString) & 0xFFFFFF;
|
|
||||||
this.color = color;
|
|
||||||
paint.setColor(color);
|
paint.setColor(color);
|
||||||
paint.setAlpha(255);
|
|
||||||
colorSetting.save(colorString); // Save after parsing.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void resetColor() {
|
/**
|
||||||
|
* @param opacity Segment color opacity between [0, 1].
|
||||||
|
*/
|
||||||
|
public void setOpacity(float opacity) throws IllegalArgumentException {
|
||||||
|
if (opacity < 0 || opacity > 1) {
|
||||||
|
throw new IllegalArgumentException("Invalid opacity: " + opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
opacitySetting.save(opacity);
|
||||||
|
updateColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getOpacity() {
|
||||||
|
return opacitySetting.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetColorAndOpacity() {
|
||||||
setColor(colorSetting.defaultValue);
|
setColor(colorSetting.defaultValue);
|
||||||
|
setOpacity(opacitySetting.defaultValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
/**
|
||||||
private static String getCategoryColorDotHTML(int color) {
|
* @param colorString Segment color with #RRGGBB format.
|
||||||
color &= 0xFFFFFF;
|
*/
|
||||||
return String.format("<font color=\"#%06X\">⬤</font>", color);
|
public void setColor(String colorString) throws IllegalArgumentException {
|
||||||
|
color = Color.parseColor(colorString);
|
||||||
|
colorSetting.save(colorString);
|
||||||
|
|
||||||
|
updateColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
/**
|
||||||
public static Spanned getCategoryColorDot(int color) {
|
* @return Integer color of #RRGGBB format.
|
||||||
return Html.fromHtml(getCategoryColorDotHTML(color));
|
*/
|
||||||
|
public int getColorNoOpacity() {
|
||||||
|
return color & 0x00FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
/**
|
||||||
public Spanned getCategoryColorDot() {
|
* @return Hex color string of #RRGGBB format with no opacity level.
|
||||||
|
*/
|
||||||
|
public String getColorString() {
|
||||||
|
return String.format(Locale.US, "#%06X", getColorNoOpacity());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SpannableString getCategoryColorDotSpan(String text, int color) {
|
||||||
|
SpannableString dotSpan = new SpannableString('⬤' + text);
|
||||||
|
dotSpan.setSpan(new ForegroundColorSpan(color), 0, 1,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
return dotSpan;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SpannableString getCategoryColorDot(int color) {
|
||||||
|
return getCategoryColorDotSpan("", color);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpannableString getCategoryColorDot() {
|
||||||
return getCategoryColorDot(color);
|
return getCategoryColorDot(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
public SpannableString getTitleWithColorDot(int categoryColor) {
|
||||||
public Spanned getTitleWithColorDot() {
|
return getCategoryColorDotSpan(" " + title, categoryColor);
|
||||||
return Html.fromHtml(getCategoryColorDotHTML(color) + " " + title);
|
}
|
||||||
|
|
||||||
|
public SpannableString getTitleWithColorDot() {
|
||||||
|
return getTitleWithColorDot(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -310,7 +358,6 @@ public enum SegmentCategory {
|
|||||||
* @param videoLength length of the video
|
* @param videoLength length of the video
|
||||||
* @return the skip button text
|
* @return the skip button text
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
|
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
|
||||||
if (Settings.SB_COMPACT_SKIP_BUTTON.get()) {
|
if (Settings.SB_COMPACT_SKIP_BUTTON.get()) {
|
||||||
return (this == SegmentCategory.HIGHLIGHT)
|
return (this == SegmentCategory.HIGHLIGHT)
|
||||||
@ -319,7 +366,7 @@ public enum SegmentCategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (videoLength == 0) {
|
if (videoLength == 0) {
|
||||||
return skipButtonTextBeginning; // video is still loading. Assume it's the beginning
|
return skipButtonTextBeginning; // Video is still loading. Assume it's the beginning.
|
||||||
}
|
}
|
||||||
final float position = segmentStartTime / (float) videoLength;
|
final float position = segmentStartTime / (float) videoLength;
|
||||||
if (position < 0.25f) {
|
if (position < 0.25f) {
|
||||||
@ -335,10 +382,9 @@ public enum SegmentCategory {
|
|||||||
* @param videoLength length of the video
|
* @param videoLength length of the video
|
||||||
* @return 'skipped segment' toast message
|
* @return 'skipped segment' toast message
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
|
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
|
||||||
if (videoLength == 0) {
|
if (videoLength == 0) {
|
||||||
return skippedToastBeginning; // video is still loading. Assume it's the beginning
|
return skippedToastBeginning; // Video is still loading. Assume it's the beginning.
|
||||||
}
|
}
|
||||||
final float position = segmentStartTime / (float) videoLength;
|
final float position = segmentStartTime / (float) videoLength;
|
||||||
if (position < 0.25f) {
|
if (position < 0.25f) {
|
||||||
|
@ -24,12 +24,15 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
|||||||
@NonNull
|
@NonNull
|
||||||
public final StringRef title;
|
public final StringRef title;
|
||||||
public final int apiVoteType;
|
public final int apiVoteType;
|
||||||
public final boolean shouldHighlight;
|
/**
|
||||||
|
* If the option should be highlighted for VIP users.
|
||||||
|
*/
|
||||||
|
public final boolean highlightIfVipAndVideoIsLocked;
|
||||||
|
|
||||||
SegmentVote(@NonNull StringRef title, int apiVoteType, boolean shouldHighlight) {
|
SegmentVote(@NonNull StringRef title, int apiVoteType, boolean highlightIfVipAndVideoIsLocked) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.apiVoteType = apiVoteType;
|
this.apiVoteType = apiVoteType;
|
||||||
this.shouldHighlight = shouldHighlight;
|
this.highlightIfVipAndVideoIsLocked = highlightIfVipAndVideoIsLocked;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +125,7 @@ class SwipeControlsConfigurationProvider(
|
|||||||
* get the background color for text on the overlay, as a color int
|
* get the background color for text on the overlay, as a color int
|
||||||
*/
|
*/
|
||||||
val overlayTextBackgroundColor: Int
|
val overlayTextBackgroundColor: Int
|
||||||
get() = Color.argb(Settings.SWIPE_OVERLAY_BACKGROUND_ALPHA.get(), 0, 0, 0)
|
get() = overlayBackgroundOpacity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get the foreground color for text on the overlay, as a color int
|
* get the foreground color for text on the overlay, as a color int
|
||||||
@ -133,6 +133,59 @@ class SwipeControlsConfigurationProvider(
|
|||||||
val overlayForegroundColor: Int
|
val overlayForegroundColor: Int
|
||||||
get() = Color.WHITE
|
get() = Color.WHITE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the opacity value (0-100%) is converted to an alpha value (0-255) for transparency.
|
||||||
|
* If the opacity value is out of range, it resets to the default and displays a warning message.
|
||||||
|
*/
|
||||||
|
val overlayBackgroundOpacity: Int
|
||||||
|
get() {
|
||||||
|
var opacity = validateValue(
|
||||||
|
Settings.SWIPE_OVERLAY_BACKGROUND_OPACITY,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
"revanced_swipe_overlay_background_opacity_invalid_toast"
|
||||||
|
)
|
||||||
|
|
||||||
|
opacity = opacity * 255 / 100
|
||||||
|
return Color.argb(opacity, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The color of the progress overlay.
|
||||||
|
*/
|
||||||
|
val overlayProgressColor: Int
|
||||||
|
get() = 0xBFFFFFFF.toInt()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The color used for the background of the progress overlay fill.
|
||||||
|
*/
|
||||||
|
val overlayFillBackgroundPaint: Int
|
||||||
|
get() = 0x80D3D3D3.toInt()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The color used for the text and icons in the overlay.
|
||||||
|
*/
|
||||||
|
val overlayTextColor: Int
|
||||||
|
get() = Color.WHITE
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flag that determines whether to use the alternate UI.
|
||||||
|
*/
|
||||||
|
val isAlternativeUI: Boolean
|
||||||
|
get() = Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flag that determines if the overlay should only show the icon.
|
||||||
|
*/
|
||||||
|
val overlayShowOverlayMinimalStyle: Boolean
|
||||||
|
get() = isAlternativeUI && Settings.SWIPE_OVERLAY_MINIMAL_STYLE.get()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A flag that determines if the progress bar should be circular.
|
||||||
|
*/
|
||||||
|
val isCircularProgressBar: Boolean
|
||||||
|
get() = isAlternativeUI && Settings.SWIPE_SHOW_CIRCULAR_OVERLAY.get()
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region behaviour
|
// region behaviour
|
||||||
|
@ -24,7 +24,7 @@ import java.lang.ref.WeakReference
|
|||||||
* The main controller for volume and brightness swipe controls.
|
* The main controller for volume and brightness swipe controls.
|
||||||
* note that the superclass is overwritten to the superclass of the MainActivity at patch time
|
* note that the superclass is overwritten to the superclass of the MainActivity at patch time
|
||||||
*
|
*
|
||||||
* @smali Lapp/revanced/integrations/youtube/swipecontrols/SwipeControlsHostActivity;
|
* @smali Lapp/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity;
|
||||||
*/
|
*/
|
||||||
class SwipeControlsHostActivity : Activity() {
|
class SwipeControlsHostActivity : Activity() {
|
||||||
/**
|
/**
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
package app.revanced.extension.youtube.swipecontrols.views
|
package app.revanced.extension.youtube.swipecontrols.views
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.graphics.drawable.GradientDrawable
|
import android.graphics.drawable.GradientDrawable
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import app.revanced.extension.shared.utils.ResourceUtils.ResourceType
|
import app.revanced.extension.shared.utils.ResourceUtils.ResourceType
|
||||||
@ -17,6 +21,7 @@ import app.revanced.extension.shared.utils.StringRef.str
|
|||||||
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider
|
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider
|
||||||
import app.revanced.extension.youtube.swipecontrols.misc.SwipeControlsOverlay
|
import app.revanced.extension.youtube.swipecontrols.misc.SwipeControlsOverlay
|
||||||
import app.revanced.extension.youtube.swipecontrols.misc.applyDimension
|
import app.revanced.extension.youtube.swipecontrols.misc.applyDimension
|
||||||
|
import kotlin.math.min
|
||||||
import kotlin.math.round
|
import kotlin.math.round
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -33,36 +38,82 @@ class SwipeControlsOverlayLayout(
|
|||||||
*/
|
*/
|
||||||
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context))
|
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context))
|
||||||
|
|
||||||
private val feedbackTextView: TextView
|
|
||||||
private val autoBrightnessIcon: Drawable
|
private val autoBrightnessIcon: Drawable
|
||||||
|
private val lowBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_low")
|
||||||
|
private val mediumBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_medium")
|
||||||
|
private val highBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_high")
|
||||||
|
private val fullBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_full")
|
||||||
private val manualBrightnessIcon: Drawable
|
private val manualBrightnessIcon: Drawable
|
||||||
private val mutedVolumeIcon: Drawable
|
private val mutedVolumeIcon: Drawable
|
||||||
|
private val lowVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_low")
|
||||||
private val normalVolumeIcon: Drawable
|
private val normalVolumeIcon: Drawable
|
||||||
|
private val feedbackTextView: TextView
|
||||||
|
private val fullVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_high")
|
||||||
|
|
||||||
private fun getDrawable(name: String, width: Int, height: Int): Drawable {
|
private val circularProgressView: CircularProgressView = CircularProgressView(
|
||||||
return resources.getDrawable(
|
context,
|
||||||
|
config.overlayBackgroundOpacity,
|
||||||
|
config.overlayShowOverlayMinimalStyle,
|
||||||
|
config.overlayProgressColor,
|
||||||
|
config.overlayFillBackgroundPaint,
|
||||||
|
config.overlayTextColor
|
||||||
|
).apply {
|
||||||
|
layoutParams = LayoutParams(300, 300).apply {
|
||||||
|
addRule(CENTER_IN_PARENT, TRUE)
|
||||||
|
}
|
||||||
|
visibility = GONE // Initially hidden
|
||||||
|
}
|
||||||
|
private val horizontalProgressView: HorizontalProgressView
|
||||||
|
|
||||||
|
private val isAlternativeUI: Boolean = config.isAlternativeUI
|
||||||
|
|
||||||
|
private fun getDrawable(name: String, width: Int? = null, height: Int? = null): Drawable {
|
||||||
|
val drawable = resources.getDrawable(
|
||||||
getIdentifier(name, ResourceType.DRAWABLE, context),
|
getIdentifier(name, ResourceType.DRAWABLE, context),
|
||||||
context.theme
|
context.theme,
|
||||||
).apply {
|
)
|
||||||
setTint(config.overlayForegroundColor)
|
|
||||||
setBounds(
|
if (width != null && height != null) {
|
||||||
|
drawable.setTint(config.overlayForegroundColor)
|
||||||
|
drawable.setBounds(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
drawable.setTint(config.overlayTextColor)
|
||||||
}
|
}
|
||||||
|
return drawable
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
// Initialize horizontal progress bar
|
||||||
|
val screenWidth = resources.displayMetrics.widthPixels
|
||||||
|
val layoutWidth = (screenWidth * 2 / 3).toInt() // 2/3 of screen width
|
||||||
|
horizontalProgressView = HorizontalProgressView(
|
||||||
|
context,
|
||||||
|
config.overlayBackgroundOpacity,
|
||||||
|
config.overlayShowOverlayMinimalStyle,
|
||||||
|
config.overlayProgressColor,
|
||||||
|
config.overlayFillBackgroundPaint,
|
||||||
|
config.overlayTextColor
|
||||||
|
).apply {
|
||||||
|
layoutParams = LayoutParams(layoutWidth, 100).apply {
|
||||||
|
addRule(CENTER_HORIZONTAL)
|
||||||
|
topMargin = 40 // Top margin
|
||||||
|
}
|
||||||
|
visibility = GONE // Initially hidden
|
||||||
|
}
|
||||||
|
|
||||||
// init views
|
// init views
|
||||||
val feedbackYTextViewPadding = 5.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
val feedbackYTextViewPadding = 5.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||||
val feedbackXTextViewPadding = 12.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
val feedbackXTextViewPadding = 12.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||||
val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||||
feedbackTextView = TextView(context).apply {
|
feedbackTextView = TextView(context).apply {
|
||||||
layoutParams = LayoutParams(
|
layoutParams = LayoutParams(
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
LayoutParams.WRAP_CONTENT,
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
LayoutParams.WRAP_CONTENT,
|
||||||
).apply {
|
).apply {
|
||||||
addRule(CENTER_IN_PARENT, TRUE)
|
addRule(CENTER_IN_PARENT, TRUE)
|
||||||
setPadding(
|
setPadding(
|
||||||
@ -81,19 +132,36 @@ class SwipeControlsOverlayLayout(
|
|||||||
compoundDrawablePadding = compoundIconPadding
|
compoundDrawablePadding = compoundIconPadding
|
||||||
visibility = GONE
|
visibility = GONE
|
||||||
}
|
}
|
||||||
addView(feedbackTextView)
|
|
||||||
|
|
||||||
// get icons scaled, assuming square icons
|
if (isAlternativeUI) {
|
||||||
val iconHeight = round(feedbackTextView.lineHeight * .8).toInt()
|
addView(circularProgressView)
|
||||||
autoBrightnessIcon = getDrawable("ic_sc_brightness_auto", iconHeight, iconHeight)
|
addView(horizontalProgressView)
|
||||||
manualBrightnessIcon = getDrawable("ic_sc_brightness_manual", iconHeight, iconHeight)
|
|
||||||
mutedVolumeIcon = getDrawable("ic_sc_volume_mute", iconHeight, iconHeight)
|
autoBrightnessIcon = getDrawable("revanced_ic_sc_brightness_auto")
|
||||||
normalVolumeIcon = getDrawable("ic_sc_volume_normal", iconHeight, iconHeight)
|
manualBrightnessIcon = getDrawable("revanced_ic_sc_brightness_manual")
|
||||||
|
mutedVolumeIcon = getDrawable("revanced_ic_sc_volume_mute")
|
||||||
|
normalVolumeIcon = getDrawable("revanced_ic_sc_volume_normal")
|
||||||
|
} else {
|
||||||
|
addView(feedbackTextView)
|
||||||
|
// get icons scaled, assuming square icons
|
||||||
|
val iconHeight = round(feedbackTextView.lineHeight * .8).toInt()
|
||||||
|
autoBrightnessIcon =
|
||||||
|
getDrawable("revanced_ic_sc_brightness_auto", iconHeight, iconHeight)
|
||||||
|
manualBrightnessIcon =
|
||||||
|
getDrawable("revanced_ic_sc_brightness_manual", iconHeight, iconHeight)
|
||||||
|
mutedVolumeIcon = getDrawable("revanced_ic_sc_volume_mute", iconHeight, iconHeight)
|
||||||
|
normalVolumeIcon = getDrawable("revanced_ic_sc_volume_normal", iconHeight, iconHeight)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val feedbackHideHandler = Handler(Looper.getMainLooper())
|
private val feedbackHideHandler = Handler(Looper.getMainLooper())
|
||||||
private val feedbackHideCallback = Runnable {
|
private val feedbackHideCallback = Runnable {
|
||||||
feedbackTextView.visibility = View.GONE
|
if (isAlternativeUI) {
|
||||||
|
circularProgressView.visibility = GONE
|
||||||
|
horizontalProgressView.visibility = GONE
|
||||||
|
} else {
|
||||||
|
feedbackTextView.visibility = GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -117,21 +185,81 @@ class SwipeControlsOverlayLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the progress bar with the appropriate value, icon, and type (brightness or volume).
|
||||||
|
*/
|
||||||
|
private fun showFeedbackView(
|
||||||
|
value: String,
|
||||||
|
progress: Int,
|
||||||
|
max: Int,
|
||||||
|
icon: Drawable,
|
||||||
|
isBrightness: Boolean
|
||||||
|
) {
|
||||||
|
feedbackHideHandler.removeCallbacks(feedbackHideCallback)
|
||||||
|
feedbackHideHandler.postDelayed(feedbackHideCallback, config.overlayShowTimeoutMillis)
|
||||||
|
|
||||||
|
val viewToShow =
|
||||||
|
if (config.isCircularProgressBar) circularProgressView else horizontalProgressView
|
||||||
|
viewToShow.apply {
|
||||||
|
setProgress(progress, max, value, isBrightness)
|
||||||
|
this.icon = icon
|
||||||
|
visibility = VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
|
override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
|
||||||
showFeedbackView(
|
if (isAlternativeUI) {
|
||||||
"$newVolume",
|
val volumePercentage = (newVolume.toFloat() / maximumVolume) * 100
|
||||||
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon,
|
val icon = when {
|
||||||
)
|
newVolume == 0 -> mutedVolumeIcon
|
||||||
|
volumePercentage < 33 -> lowVolumeIcon
|
||||||
|
volumePercentage < 66 -> normalVolumeIcon
|
||||||
|
else -> fullVolumeIcon
|
||||||
|
}
|
||||||
|
showFeedbackView("$newVolume", newVolume, maximumVolume, icon, isBrightness = false)
|
||||||
|
} else {
|
||||||
|
showFeedbackView(
|
||||||
|
"$newVolume",
|
||||||
|
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBrightnessChanged(brightness: Double) {
|
override fun onBrightnessChanged(brightness: Double) {
|
||||||
if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) {
|
if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) {
|
||||||
showFeedbackView(
|
if (isAlternativeUI) {
|
||||||
str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
|
showFeedbackView(
|
||||||
autoBrightnessIcon,
|
str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
|
||||||
)
|
0,
|
||||||
|
100,
|
||||||
|
autoBrightnessIcon,
|
||||||
|
isBrightness = true,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showFeedbackView(
|
||||||
|
str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
|
||||||
|
autoBrightnessIcon,
|
||||||
|
)
|
||||||
|
}
|
||||||
} else if (brightness >= 0) {
|
} else if (brightness >= 0) {
|
||||||
showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon)
|
if (isAlternativeUI) {
|
||||||
|
val brightnessValue = round(brightness).toInt()
|
||||||
|
val icon = when {
|
||||||
|
brightnessValue < 25 -> lowBrightnessIcon
|
||||||
|
brightnessValue < 50 -> mediumBrightnessIcon
|
||||||
|
brightnessValue < 75 -> highBrightnessIcon
|
||||||
|
else -> fullBrightnessIcon
|
||||||
|
}
|
||||||
|
showFeedbackView(
|
||||||
|
"$brightnessValue%",
|
||||||
|
brightnessValue,
|
||||||
|
100,
|
||||||
|
icon,
|
||||||
|
isBrightness = true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,3 +273,255 @@ class SwipeControlsOverlayLayout(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for progress views.
|
||||||
|
*/
|
||||||
|
abstract class AbstractProgressView(
|
||||||
|
context: Context,
|
||||||
|
overlayBackgroundOpacity: Int,
|
||||||
|
protected val overlayShowOverlayMinimalStyle: Boolean,
|
||||||
|
overlayProgressColor: Int,
|
||||||
|
overlayFillBackgroundPaint: Int,
|
||||||
|
protected val overlayTextColor: Int,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
|
// Combined paint creation function for both fill and stroke styles
|
||||||
|
private fun createPaint(
|
||||||
|
color: Int,
|
||||||
|
style: Paint.Style = Paint.Style.FILL,
|
||||||
|
strokeCap: Paint.Cap = Paint.Cap.BUTT,
|
||||||
|
strokeWidth: Float = 0f
|
||||||
|
) = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
this.style = style
|
||||||
|
this.color = color
|
||||||
|
this.strokeCap = strokeCap
|
||||||
|
this.strokeWidth = strokeWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize paints
|
||||||
|
val backgroundPaint = createPaint(overlayBackgroundOpacity, style = Paint.Style.FILL)
|
||||||
|
val progressPaint = createPaint(
|
||||||
|
overlayProgressColor,
|
||||||
|
style = Paint.Style.STROKE,
|
||||||
|
strokeCap = Paint.Cap.ROUND,
|
||||||
|
strokeWidth = 20f
|
||||||
|
)
|
||||||
|
val fillBackgroundPaint = createPaint(overlayFillBackgroundPaint, style = Paint.Style.FILL)
|
||||||
|
val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = overlayTextColor
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
textSize = 40f // Can adjust based on need
|
||||||
|
}
|
||||||
|
|
||||||
|
protected var progress = 0
|
||||||
|
protected var maxProgress = 100
|
||||||
|
protected var displayText: String = "0"
|
||||||
|
protected var isBrightness = true
|
||||||
|
var icon: Drawable? = null
|
||||||
|
|
||||||
|
fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) {
|
||||||
|
progress = value
|
||||||
|
maxProgress = max
|
||||||
|
displayText = text
|
||||||
|
isBrightness = isBrightnessMode
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
// Base class implementation can be empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom view for rendering a circular progress indicator with icons and text.
|
||||||
|
*/
|
||||||
|
@SuppressLint("ViewConstructor")
|
||||||
|
class CircularProgressView(
|
||||||
|
context: Context,
|
||||||
|
overlayBackgroundOpacity: Int,
|
||||||
|
overlayShowOverlayMinimalStyle: Boolean,
|
||||||
|
overlayProgressColor: Int,
|
||||||
|
overlayFillBackgroundPaint: Int,
|
||||||
|
overlayTextColor: Int,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : AbstractProgressView(
|
||||||
|
context,
|
||||||
|
overlayBackgroundOpacity,
|
||||||
|
overlayShowOverlayMinimalStyle,
|
||||||
|
overlayProgressColor,
|
||||||
|
overlayFillBackgroundPaint,
|
||||||
|
overlayTextColor,
|
||||||
|
attrs,
|
||||||
|
defStyleAttr
|
||||||
|
) {
|
||||||
|
private val rectF = RectF()
|
||||||
|
|
||||||
|
init {
|
||||||
|
textPaint.textSize = 40f // Override default text size for circular view
|
||||||
|
progressPaint.strokeWidth = 20f
|
||||||
|
fillBackgroundPaint.strokeWidth = 20f
|
||||||
|
progressPaint.strokeCap = Paint.Cap.ROUND
|
||||||
|
fillBackgroundPaint.strokeCap = Paint.Cap.BUTT
|
||||||
|
progressPaint.style = Paint.Style.STROKE
|
||||||
|
fillBackgroundPaint.style = Paint.Style.STROKE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
|
||||||
|
val size = min(width, height).toFloat()
|
||||||
|
rectF.set(20f, 20f, size - 20f, size - 20f)
|
||||||
|
|
||||||
|
canvas.drawOval(rectF, fillBackgroundPaint) // Draw the outer ring.
|
||||||
|
canvas.drawCircle(
|
||||||
|
width / 2f,
|
||||||
|
height / 2f,
|
||||||
|
size / 3,
|
||||||
|
backgroundPaint
|
||||||
|
) // Draw the inner circle.
|
||||||
|
|
||||||
|
// Select the paint for drawing based on whether it's brightness or volume.
|
||||||
|
val sweepAngle = (progress.toFloat() / maxProgress) * 360
|
||||||
|
canvas.drawArc(rectF, -90f, sweepAngle, false, progressPaint) // Draw the progress arc.
|
||||||
|
|
||||||
|
// Draw the icon in the center.
|
||||||
|
icon?.let {
|
||||||
|
val iconSize = if (overlayShowOverlayMinimalStyle) 100 else 80
|
||||||
|
val iconX = (width - iconSize) / 2
|
||||||
|
val iconY = (height / 2) - if (overlayShowOverlayMinimalStyle) 50 else 80
|
||||||
|
it.setBounds(iconX, iconY, iconX + iconSize, iconY + iconSize)
|
||||||
|
it.draw(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not a minimal style mode, draw the text inside the ring.
|
||||||
|
if (!overlayShowOverlayMinimalStyle) {
|
||||||
|
canvas.drawText(displayText, width / 2f, height / 2f + 60f, textPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom view for rendering a rectangular progress bar with icons and text.
|
||||||
|
*/
|
||||||
|
@SuppressLint("ViewConstructor")
|
||||||
|
class HorizontalProgressView(
|
||||||
|
context: Context,
|
||||||
|
overlayBackgroundOpacity: Int,
|
||||||
|
overlayShowOverlayMinimalStyle: Boolean,
|
||||||
|
overlayProgressColor: Int,
|
||||||
|
overlayFillBackgroundPaint: Int,
|
||||||
|
overlayTextColor: Int,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : AbstractProgressView(
|
||||||
|
context,
|
||||||
|
overlayBackgroundOpacity,
|
||||||
|
overlayShowOverlayMinimalStyle,
|
||||||
|
overlayProgressColor,
|
||||||
|
overlayFillBackgroundPaint,
|
||||||
|
overlayTextColor,
|
||||||
|
attrs,
|
||||||
|
defStyleAttr
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val iconSize = 60f
|
||||||
|
private val padding = 40f
|
||||||
|
|
||||||
|
init {
|
||||||
|
textPaint.textSize = 36f // Override default text size for horizontal view
|
||||||
|
progressPaint.strokeWidth = 0f
|
||||||
|
progressPaint.strokeCap = Paint.Cap.BUTT
|
||||||
|
progressPaint.style = Paint.Style.FILL
|
||||||
|
fillBackgroundPaint.style = Paint.Style.FILL
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
|
||||||
|
val width = width.toFloat()
|
||||||
|
val height = height.toFloat()
|
||||||
|
|
||||||
|
// Radius for rounded corners
|
||||||
|
val cornerRadius = min(width, height) / 2
|
||||||
|
|
||||||
|
// Calculate the total width for the elements
|
||||||
|
val minimalElementWidth = 5 * padding + iconSize
|
||||||
|
|
||||||
|
// Calculate the starting point (X) to center the elements
|
||||||
|
val minimalStartX = (width - minimalElementWidth) / 2
|
||||||
|
|
||||||
|
// Draw the background
|
||||||
|
if (!overlayShowOverlayMinimalStyle) {
|
||||||
|
canvas.drawRoundRect(0f, 0f, width, height, cornerRadius, cornerRadius, backgroundPaint)
|
||||||
|
} else {
|
||||||
|
canvas.drawRoundRect(
|
||||||
|
minimalStartX,
|
||||||
|
0f,
|
||||||
|
minimalStartX + minimalElementWidth,
|
||||||
|
height,
|
||||||
|
cornerRadius,
|
||||||
|
cornerRadius,
|
||||||
|
backgroundPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!overlayShowOverlayMinimalStyle) {
|
||||||
|
// Draw the fill background
|
||||||
|
val startX = 2 * padding + iconSize
|
||||||
|
val endX = width - 4 * padding
|
||||||
|
val fillWidth = endX - startX
|
||||||
|
|
||||||
|
canvas.drawRoundRect(
|
||||||
|
startX,
|
||||||
|
height / 2 - 5f,
|
||||||
|
endX,
|
||||||
|
height / 2 + 5f,
|
||||||
|
10f, 10f,
|
||||||
|
fillBackgroundPaint
|
||||||
|
)
|
||||||
|
|
||||||
|
// Draw the progress
|
||||||
|
val progressWidth = (progress.toFloat() / maxProgress) * fillWidth
|
||||||
|
canvas.drawRoundRect(
|
||||||
|
startX,
|
||||||
|
height / 2 - 5f,
|
||||||
|
startX + progressWidth,
|
||||||
|
height / 2 + 5f,
|
||||||
|
10f, 10f,
|
||||||
|
progressPaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the icon
|
||||||
|
icon?.let {
|
||||||
|
val iconX = if (!overlayShowOverlayMinimalStyle) {
|
||||||
|
padding
|
||||||
|
} else {
|
||||||
|
padding + minimalStartX
|
||||||
|
}
|
||||||
|
val iconY = height / 2 - iconSize / 2
|
||||||
|
it.setBounds(
|
||||||
|
iconX.toInt(),
|
||||||
|
iconY.toInt(),
|
||||||
|
(iconX + iconSize).toInt(),
|
||||||
|
(iconY + iconSize).toInt()
|
||||||
|
)
|
||||||
|
it.draw(canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the text on the right
|
||||||
|
val textX = if (!overlayShowOverlayMinimalStyle) {
|
||||||
|
width - 2 * padding
|
||||||
|
} else {
|
||||||
|
minimalStartX + minimalElementWidth - 2 * padding
|
||||||
|
}
|
||||||
|
val textY = height / 2 + textPaint.textSize / 3
|
||||||
|
|
||||||
|
// Draw the text
|
||||||
|
canvas.drawText(displayText, textX, textY, textPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,8 +2,27 @@ package app.revanced.extension.youtube.utils;
|
|||||||
|
|
||||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.PorterDuffColorFilter;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.graphics.drawable.StateListDrawable;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||||
import app.revanced.extension.shared.settings.FloatSetting;
|
import app.revanced.extension.shared.settings.FloatSetting;
|
||||||
import app.revanced.extension.shared.settings.IntegerSetting;
|
import app.revanced.extension.shared.settings.IntegerSetting;
|
||||||
@ -12,14 +31,20 @@ import app.revanced.extension.shared.utils.PackageUtils;
|
|||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
public class ExtendedUtils extends PackageUtils {
|
public class ExtendedUtils extends PackageUtils {
|
||||||
|
|
||||||
|
private static boolean isVersionOrGreater(String version) {
|
||||||
|
return getAppVersionName().compareTo(version) >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public static final boolean IS_19_17_OR_GREATER = getAppVersionName().compareTo("19.17.00") >= 0;
|
public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00");
|
||||||
public static final boolean IS_19_20_OR_GREATER = getAppVersionName().compareTo("19.20.00") >= 0;
|
public static final boolean IS_19_20_OR_GREATER = isVersionOrGreater("19.20.00");
|
||||||
public static final boolean IS_19_21_OR_GREATER = getAppVersionName().compareTo("19.21.00") >= 0;
|
public static final boolean IS_19_21_OR_GREATER = isVersionOrGreater("19.21.00");
|
||||||
public static final boolean IS_19_26_OR_GREATER = getAppVersionName().compareTo("19.26.00") >= 0;
|
public static final boolean IS_19_26_OR_GREATER = isVersionOrGreater("19.26.00");
|
||||||
public static final boolean IS_19_28_OR_GREATER = getAppVersionName().compareTo("19.28.00") >= 0;
|
public static final boolean IS_19_28_OR_GREATER = isVersionOrGreater("19.28.00");
|
||||||
public static final boolean IS_19_29_OR_GREATER = getAppVersionName().compareTo("19.29.00") >= 0;
|
public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00");
|
||||||
public static final boolean IS_19_34_OR_GREATER = getAppVersionName().compareTo("19.34.00") >= 0;
|
public static final boolean IS_19_34_OR_GREATER = isVersionOrGreater("19.34.00");
|
||||||
|
public static final boolean IS_20_09_OR_GREATER = isVersionOrGreater("20.09.00");
|
||||||
|
|
||||||
public static int validateValue(IntegerSetting settings, int min, int max, String message) {
|
public static int validateValue(IntegerSetting settings, int min, int max, String message) {
|
||||||
int value = settings.get();
|
int value = settings.get();
|
||||||
@ -114,4 +139,88 @@ public class ExtendedUtils extends PackageUtils {
|
|||||||
}
|
}
|
||||||
return additionalSettingsEnabled;
|
return additionalSettingsEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void showBottomSheetDialog(Context mContext, ScrollView mScrollView,
|
||||||
|
Map<LinearLayout, Runnable> actionsMap) {
|
||||||
|
runOnMainThreadDelayed(() -> {
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
|
||||||
|
builder.setView(mScrollView);
|
||||||
|
|
||||||
|
AlertDialog dialog = builder.create();
|
||||||
|
dialog.show();
|
||||||
|
|
||||||
|
actionsMap.forEach((view, action) ->
|
||||||
|
view.setOnClickListener(v -> {
|
||||||
|
action.run();
|
||||||
|
dialog.dismiss();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
actionsMap.clear();
|
||||||
|
|
||||||
|
Window window = dialog.getWindow();
|
||||||
|
if (window == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// round corners
|
||||||
|
GradientDrawable dialogBackground = new GradientDrawable();
|
||||||
|
dialogBackground.setCornerRadius(32);
|
||||||
|
window.setBackgroundDrawable(dialogBackground);
|
||||||
|
|
||||||
|
// fit screen width
|
||||||
|
int dialogWidth = (int) (mContext.getResources().getDisplayMetrics().widthPixels * 0.95);
|
||||||
|
window.setLayout(dialogWidth, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
|
||||||
|
// move dialog to bottom
|
||||||
|
WindowManager.LayoutParams layoutParams = window.getAttributes();
|
||||||
|
layoutParams.gravity = Gravity.BOTTOM;
|
||||||
|
|
||||||
|
// adjust the vertical offset
|
||||||
|
layoutParams.y = dpToPx(5);
|
||||||
|
|
||||||
|
window.setAttributes(layoutParams);
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LinearLayout createItemLayout(Context mContext, String title, int iconId) {
|
||||||
|
// Item Layout
|
||||||
|
LinearLayout itemLayout = new LinearLayout(mContext);
|
||||||
|
itemLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
itemLayout.setPadding(dpToPx(16), dpToPx(12), dpToPx(16), dpToPx(12));
|
||||||
|
itemLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
itemLayout.setClickable(true);
|
||||||
|
itemLayout.setFocusable(true);
|
||||||
|
|
||||||
|
// Create a StateListDrawable for the background
|
||||||
|
StateListDrawable background = new StateListDrawable();
|
||||||
|
ColorDrawable pressedDrawable = new ColorDrawable(ThemeUtils.getPressedElementColor());
|
||||||
|
ColorDrawable defaultDrawable = new ColorDrawable(ThemeUtils.getBackgroundColor());
|
||||||
|
background.addState(new int[]{android.R.attr.state_pressed}, pressedDrawable);
|
||||||
|
background.addState(new int[]{}, defaultDrawable);
|
||||||
|
itemLayout.setBackground(background);
|
||||||
|
|
||||||
|
// Icon
|
||||||
|
ColorFilter cf = new PorterDuffColorFilter(ThemeUtils.getForegroundColor(), PorterDuff.Mode.SRC_ATOP);
|
||||||
|
ImageView iconView = new ImageView(mContext);
|
||||||
|
iconView.setImageResource(iconId);
|
||||||
|
iconView.setColorFilter(cf);
|
||||||
|
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(dpToPx(24), dpToPx(24));
|
||||||
|
iconParams.setMarginEnd(dpToPx(16));
|
||||||
|
iconView.setLayoutParams(iconParams);
|
||||||
|
itemLayout.addView(iconView);
|
||||||
|
|
||||||
|
// Text container
|
||||||
|
LinearLayout textContainer = new LinearLayout(mContext);
|
||||||
|
textContainer.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
TextView titleView = new TextView(mContext);
|
||||||
|
titleView.setText(title);
|
||||||
|
titleView.setTextSize(16);
|
||||||
|
titleView.setTextColor(ThemeUtils.getForegroundColor());
|
||||||
|
textContainer.addView(titleView);
|
||||||
|
|
||||||
|
itemLayout.addView(textContainer);
|
||||||
|
|
||||||
|
return itemLayout;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -63,7 +63,7 @@ public class VideoUtils extends IntentUtils {
|
|||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getVideoScheme(String videoId, boolean isShorts) {
|
public static String getVideoScheme(String videoId, boolean isShorts) {
|
||||||
return String.format(
|
return String.format(
|
||||||
Locale.ENGLISH,
|
Locale.ENGLISH,
|
||||||
isShorts ? VIDEO_SCHEME_INTENT_FORMAT : VIDEO_SCHEME_LINK_FORMAT,
|
isShorts ? VIDEO_SCHEME_INTENT_FORMAT : VIDEO_SCHEME_LINK_FORMAT,
|
||||||
@ -128,6 +128,22 @@ public class VideoUtils extends IntentUtils {
|
|||||||
launchView(getChannelUrl(channelId), getContext().getPackageName());
|
launchView(getChannelUrl(channelId), getContext().getPackageName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void openPlaylist(@NonNull String playlistId) {
|
||||||
|
openPlaylist(playlistId, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void openPlaylist(@NonNull String playlistId, @NonNull String videoId) {
|
||||||
|
final StringBuilder sb = new StringBuilder();
|
||||||
|
if (videoId.isEmpty()) {
|
||||||
|
sb.append(getPlaylistUrl(playlistId));
|
||||||
|
} else {
|
||||||
|
sb.append(getVideoScheme(videoId, false));
|
||||||
|
sb.append("&list=");
|
||||||
|
sb.append(playlistId);
|
||||||
|
}
|
||||||
|
launchView(sb.toString(), getContext().getPackageName());
|
||||||
|
}
|
||||||
|
|
||||||
public static void openVideo() {
|
public static void openVideo() {
|
||||||
openVideo(VideoInformation.getVideoId());
|
openVideo(VideoInformation.getVideoId());
|
||||||
}
|
}
|
||||||
@ -177,8 +193,8 @@ public class VideoUtils extends IntentUtils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void showPlaybackSpeedDialog(@NonNull Context context) {
|
public static void showPlaybackSpeedDialog(@NonNull Context context) {
|
||||||
final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedListEntries();
|
final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedEntries();
|
||||||
final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedListEntryValues();
|
final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedEntryValues();
|
||||||
|
|
||||||
final float playbackSpeed = VideoInformation.getPlaybackSpeed();
|
final float playbackSpeed = VideoInformation.getPlaybackSpeed();
|
||||||
final int index = Arrays.binarySearch(playbackSpeedEntryValues, String.valueOf(playbackSpeed));
|
final int index = Arrays.binarySearch(playbackSpeedEntryValues, String.valueOf(playbackSpeed));
|
||||||
@ -186,6 +202,7 @@ public class VideoUtils extends IntentUtils {
|
|||||||
new AlertDialog.Builder(context)
|
new AlertDialog.Builder(context)
|
||||||
.setSingleChoiceItems(playbackSpeedEntries, index, (mDialog, mIndex) -> {
|
.setSingleChoiceItems(playbackSpeedEntries, index, (mDialog, mIndex) -> {
|
||||||
final float selectedPlaybackSpeed = Float.parseFloat(playbackSpeedEntryValues[mIndex] + "f");
|
final float selectedPlaybackSpeed = Float.parseFloat(playbackSpeedEntryValues[mIndex] + "f");
|
||||||
|
VideoInformation.setPlaybackSpeed(selectedPlaybackSpeed);
|
||||||
VideoInformation.overridePlaybackSpeed(selectedPlaybackSpeed);
|
VideoInformation.overridePlaybackSpeed(selectedPlaybackSpeed);
|
||||||
userSelectedPlaybackSpeed(selectedPlaybackSpeed);
|
userSelectedPlaybackSpeed(selectedPlaybackSpeed);
|
||||||
mDialog.dismiss();
|
mDialog.dismiss();
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
package com.google.android.apps.youtube.app.settings.videoquality;
|
package com.google.android.apps.youtube.app.settings.videoquality;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.res.Resources;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.util.TypedValue;
|
import android.util.TypedValue;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -25,8 +27,8 @@ import app.revanced.extension.youtube.utils.ThemeUtils;
|
|||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public class VideoQualitySettingsActivity extends Activity {
|
public class VideoQualitySettingsActivity extends Activity {
|
||||||
|
|
||||||
private static final String rvxSettingsLabel = ResourceUtils.getString("revanced_extended_settings_title");
|
private static String rvxSettingsLabel;
|
||||||
private static final String searchLabel = ResourceUtils.getString("revanced_extended_settings_search_title");
|
private static String searchLabel;
|
||||||
private static WeakReference<SearchView> searchViewRef = new WeakReference<>(null);
|
private static WeakReference<SearchView> searchViewRef = new WeakReference<>(null);
|
||||||
private static WeakReference<ImageView> closeButtonRef = new WeakReference<>(null);
|
private static WeakReference<ImageView> closeButtonRef = new WeakReference<>(null);
|
||||||
private ReVancedPreferenceFragment fragment;
|
private ReVancedPreferenceFragment fragment;
|
||||||
@ -71,6 +73,10 @@ public class VideoQualitySettingsActivity extends Activity {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set label
|
||||||
|
rvxSettingsLabel = getString("revanced_extended_settings_title");
|
||||||
|
searchLabel = getString("revanced_extended_settings_search_title");
|
||||||
|
|
||||||
// Set toolbar
|
// Set toolbar
|
||||||
setToolbar();
|
setToolbar();
|
||||||
|
|
||||||
@ -85,6 +91,14 @@ public class VideoQualitySettingsActivity extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DiscouragedApi")
|
||||||
|
private String getString(String str) {
|
||||||
|
Context baseContext = getBaseContext();
|
||||||
|
Resources resources = baseContext.getResources();
|
||||||
|
int identifier = resources.getIdentifier(str, "string", baseContext.getPackageName());
|
||||||
|
return resources.getString(identifier);
|
||||||
|
}
|
||||||
|
|
||||||
private void filterPreferences(String query) {
|
private void filterPreferences(String query) {
|
||||||
if (fragment == null) return;
|
if (fragment == null) return;
|
||||||
fragment.filterPreferences(query);
|
fragment.filterPreferences(query);
|
||||||
|
@ -4,5 +4,5 @@ org.gradle.parallel = true
|
|||||||
android.useAndroidX = true
|
android.useAndroidX = true
|
||||||
kotlin.code.style = official
|
kotlin.code.style = official
|
||||||
kotlin.jvm.target.validation.mode = IGNORE
|
kotlin.jvm.target.validation.mode = IGNORE
|
||||||
version = 5.5.1
|
version = 5.6.1
|
||||||
|
|
||||||
|
@ -6,12 +6,14 @@ smali = "3.0.5"
|
|||||||
gson = "2.12.1"
|
gson = "2.12.1"
|
||||||
agp = "8.2.2"
|
agp = "8.2.2"
|
||||||
annotation = "1.9.1"
|
annotation = "1.9.1"
|
||||||
|
collections4 = "4.5.0-M3"
|
||||||
lang3 = "3.17.0"
|
lang3 = "3.17.0"
|
||||||
preference = "1.2.1"
|
preference = "1.2.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||||
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
|
annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
|
||||||
|
collections4 = { module = "org.apache.commons:commons-collections4", version.ref = "collections4" }
|
||||||
lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "lang3" }
|
lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "lang3" }
|
||||||
preference = { module = "androidx.preference:preference", version.ref = "preference" }
|
preference = { module = "androidx.preference:preference", version.ref = "preference" }
|
||||||
|
|
||||||
|
724
patches.json
724
patches.json
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,10 @@ public final class app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWi
|
|||||||
public static final fun getSpoofWifiPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getSpoofWifiPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/all/misc/display/edgetoedge/EdgeToEdgeDisplayPatchKt {
|
||||||
|
public static final fun getEdgeToEdgeDisplayPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract interface class app/revanced/patches/all/misc/transformation/IMethodCall {
|
public abstract interface class app/revanced/patches/all/misc/transformation/IMethodCall {
|
||||||
public abstract fun getDefinedClassName ()Ljava/lang/String;
|
public abstract fun getDefinedClassName ()Ljava/lang/String;
|
||||||
public abstract fun getMethodName ()Ljava/lang/String;
|
public abstract fun getMethodName ()Ljava/lang/String;
|
||||||
@ -253,6 +257,7 @@ public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdP
|
|||||||
public static final fun getBottomSheetRecyclerView ()J
|
public static final fun getBottomSheetRecyclerView ()J
|
||||||
public static final fun getButtonContainer ()J
|
public static final fun getButtonContainer ()J
|
||||||
public static final fun getButtonIconPaddingMedium ()J
|
public static final fun getButtonIconPaddingMedium ()J
|
||||||
|
public static final fun getChannelHandle ()J
|
||||||
public static final fun getChipCloud ()J
|
public static final fun getChipCloud ()J
|
||||||
public static final fun getColorGrey ()J
|
public static final fun getColorGrey ()J
|
||||||
public static final fun getDarkBackground ()J
|
public static final fun getDarkBackground ()J
|
||||||
@ -281,6 +286,7 @@ public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdP
|
|||||||
public static final fun getPrivacyTosFooter ()J
|
public static final fun getPrivacyTosFooter ()J
|
||||||
public static final fun getQualityAuto ()J
|
public static final fun getQualityAuto ()J
|
||||||
public static final fun getRemixGenericButtonSize ()J
|
public static final fun getRemixGenericButtonSize ()J
|
||||||
|
public static final fun getSearchButton ()J
|
||||||
public static final fun getSlidingDialogAnimation ()J
|
public static final fun getSlidingDialogAnimation ()J
|
||||||
public static final fun getTapBloomView ()J
|
public static final fun getTapBloomView ()J
|
||||||
public static final fun getText1 ()J
|
public static final fun getText1 ()J
|
||||||
@ -452,10 +458,6 @@ public final class app/revanced/patches/reddit/utils/extension/SharedExtensionPa
|
|||||||
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
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 final class app/revanced/patches/reddit/utils/settings/SettingsPatchKt {
|
||||||
public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
public static final fun is_2024_26_or_greater ()Z
|
public static final fun is_2024_26_or_greater ()Z
|
||||||
@ -550,10 +552,9 @@ public final class app/revanced/patches/shared/mapping/ResourceElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/shared/mapping/ResourceMappingPatchKt {
|
public final class app/revanced/patches/shared/mapping/ResourceMappingPatchKt {
|
||||||
public static final fun get (Ljava/util/List;Lapp/revanced/patches/shared/mapping/ResourceType;Ljava/lang/String;)J
|
public static final fun getResourceId (Lapp/revanced/patches/shared/mapping/ResourceType;Ljava/lang/String;)J
|
||||||
public static final fun get (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)J
|
public static final fun getResourceId (Ljava/lang/String;Ljava/lang/String;)J
|
||||||
public static final fun getResourceMappingPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
public static final fun getResourceMappingPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
public static final fun getResourceMappings ()Ljava/util/List;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/shared/mapping/ResourceType : java/lang/Enum {
|
public final class app/revanced/patches/shared/mapping/ResourceType : java/lang/Enum {
|
||||||
@ -715,6 +716,10 @@ public final class app/revanced/patches/youtube/general/toolbar/ToolBarComponent
|
|||||||
public static final fun getToolBarComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getToolBarComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/youtube/general/updates/LayoutUpdatesPatchKt {
|
||||||
|
public static final fun getLayoutUpdatesPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatchKt {
|
public final class app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatchKt {
|
||||||
public static final fun getShortsActionButtonsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
public static final fun getShortsActionButtonsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
}
|
}
|
||||||
@ -866,6 +871,7 @@ public final class app/revanced/patches/youtube/player/seekbar/SeekbarComponents
|
|||||||
|
|
||||||
public final class app/revanced/patches/youtube/shorts/components/FingerprintsKt {
|
public final class app/revanced/patches/youtube/shorts/components/FingerprintsKt {
|
||||||
public static final fun indexOfAddLiveHeaderElementsContainerInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
|
public static final fun indexOfAddLiveHeaderElementsContainerInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
|
||||||
|
public static final fun indexOfDismissInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/youtube/shorts/components/ShortsComponentPatchKt {
|
public final class app/revanced/patches/youtube/shorts/components/ShortsComponentPatchKt {
|
||||||
@ -1013,6 +1019,10 @@ public final class app/revanced/patches/youtube/utils/playertype/PlayerTypeHookP
|
|||||||
public static final fun getPlayerTypeHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getPlayerTypeHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/youtube/utils/playlist/PlaylistPatchKt {
|
||||||
|
public static final fun getPlaylistPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPatchKt {
|
public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPatchKt {
|
||||||
public static final fun getVersionCheckPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
public static final fun getVersionCheckPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
public static final fun is_18_31_or_greater ()Z
|
public static final fun is_18_31_or_greater ()Z
|
||||||
@ -1020,12 +1030,16 @@ public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPa
|
|||||||
public static final fun is_18_39_or_greater ()Z
|
public static final fun is_18_39_or_greater ()Z
|
||||||
public static final fun is_18_42_or_greater ()Z
|
public static final fun is_18_42_or_greater ()Z
|
||||||
public static final fun is_18_49_or_greater ()Z
|
public static final fun is_18_49_or_greater ()Z
|
||||||
|
public static final fun is_19_01_or_greater ()Z
|
||||||
public static final fun is_19_02_or_greater ()Z
|
public static final fun is_19_02_or_greater ()Z
|
||||||
public static final fun is_19_04_or_greater ()Z
|
public static final fun is_19_04_or_greater ()Z
|
||||||
|
public static final fun is_19_05_or_greater ()Z
|
||||||
public static final fun is_19_09_or_greater ()Z
|
public static final fun is_19_09_or_greater ()Z
|
||||||
|
public static final fun is_19_11_or_greater ()Z
|
||||||
public static final fun is_19_15_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_16_or_greater ()Z
|
||||||
public static final fun is_19_17_or_greater ()Z
|
public static final fun is_19_17_or_greater ()Z
|
||||||
|
public static final fun is_19_18_or_greater ()Z
|
||||||
public static final fun is_19_23_or_greater ()Z
|
public static final fun is_19_23_or_greater ()Z
|
||||||
public static final fun is_19_25_or_greater ()Z
|
public static final fun is_19_25_or_greater ()Z
|
||||||
public static final fun is_19_26_or_greater ()Z
|
public static final fun is_19_26_or_greater ()Z
|
||||||
@ -1040,10 +1054,13 @@ public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPa
|
|||||||
public static final fun is_19_44_or_greater ()Z
|
public static final fun is_19_44_or_greater ()Z
|
||||||
public static final fun is_19_46_or_greater ()Z
|
public static final fun is_19_46_or_greater ()Z
|
||||||
public static final fun is_19_49_or_greater ()Z
|
public static final fun is_19_49_or_greater ()Z
|
||||||
|
public static final fun is_19_50_or_greater ()Z
|
||||||
public static final fun is_20_02_or_greater ()Z
|
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_03_or_greater ()Z
|
||||||
public static final fun is_20_05_or_greater ()Z
|
public static final fun is_20_05_or_greater ()Z
|
||||||
|
public static final fun is_20_09_or_greater ()Z
|
||||||
public static final fun is_20_10_or_greater ()Z
|
public static final fun is_20_10_or_greater ()Z
|
||||||
|
public static final fun is_20_12_or_greater ()Z
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/youtube/utils/recyclerview/RecyclerViewTreeObserverPatchKt {
|
public final class app/revanced/patches/youtube/utils/recyclerview/RecyclerViewTreeObserverPatchKt {
|
||||||
@ -1145,7 +1162,6 @@ public final class app/revanced/patches/youtube/utils/resourceid/SharedResourceI
|
|||||||
public static final fun getRelatedChipCloudMargin ()J
|
public static final fun getRelatedChipCloudMargin ()J
|
||||||
public static final fun getRightComment ()J
|
public static final fun getRightComment ()J
|
||||||
public static final fun getScrimOverlay ()J
|
public static final fun getScrimOverlay ()J
|
||||||
public static final fun getScrubbing ()J
|
|
||||||
public static final fun getSeekEasyHorizontalTouchOffsetToStartScrubbing ()J
|
public static final fun getSeekEasyHorizontalTouchOffsetToStartScrubbing ()J
|
||||||
public static final fun getSeekUndoEduOverlayStub ()J
|
public static final fun getSeekUndoEduOverlayStub ()J
|
||||||
public static final fun getSettingsFragment ()J
|
public static final fun getSettingsFragment ()J
|
||||||
@ -1160,13 +1176,17 @@ public final class app/revanced/patches/youtube/utils/resourceid/SharedResourceI
|
|||||||
public static final fun getTotalTime ()J
|
public static final fun getTotalTime ()J
|
||||||
public static final fun getTouchArea ()J
|
public static final fun getTouchArea ()J
|
||||||
public static final fun getVarispeedUnavailableTitle ()J
|
public static final fun getVarispeedUnavailableTitle ()J
|
||||||
|
public static final fun getVerticalTouchOffsetToEnterFineScrubbing ()J
|
||||||
|
public static final fun getVerticalTouchOffsetToStartFineScrubbing ()J
|
||||||
public static final fun getVideoQualityBottomSheet ()J
|
public static final fun getVideoQualityBottomSheet ()J
|
||||||
public static final fun getVideoQualityUnavailableAnnouncement ()J
|
public static final fun getVideoQualityUnavailableAnnouncement ()J
|
||||||
public static final fun getVideoZoomSnapIndicator ()J
|
public static final fun getVideoZoomSnapIndicator ()J
|
||||||
public static final fun getVoiceSearch ()J
|
public static final fun getVoiceSearch ()J
|
||||||
public static final fun getYouTubeControlsOverlaySubtitleButton ()J
|
public static final fun getYouTubeControlsOverlaySubtitleButton ()J
|
||||||
public static final fun getYouTubeLogo ()J
|
public static final fun getYouTubeLogo ()J
|
||||||
|
public static final fun getYtCallToAction ()J
|
||||||
public static final fun getYtFillBell ()J
|
public static final fun getYtFillBell ()J
|
||||||
|
public static final fun getYtOutlineLibrary ()J
|
||||||
public static final fun getYtOutlineMoonZ ()J
|
public static final fun getYtOutlineMoonZ ()J
|
||||||
public static final fun getYtOutlinePictureInPictureWhite ()J
|
public static final fun getYtOutlinePictureInPictureWhite ()J
|
||||||
public static final fun getYtOutlineVideoCamera ()J
|
public static final fun getYtOutlineVideoCamera ()J
|
||||||
|
@ -152,7 +152,10 @@ private enum class MethodCall(
|
|||||||
RegisterNetworkCallback1(
|
RegisterNetworkCallback1(
|
||||||
"Landroid/net/ConnectivityManager;",
|
"Landroid/net/ConnectivityManager;",
|
||||||
"registerNetworkCallback",
|
"registerNetworkCallback",
|
||||||
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"),
|
arrayOf(
|
||||||
|
"Landroid/net/NetworkRequest;",
|
||||||
|
"Landroid/net/ConnectivityManager\$NetworkCallback;"
|
||||||
|
),
|
||||||
"V",
|
"V",
|
||||||
),
|
),
|
||||||
RegisterNetworkCallback2(
|
RegisterNetworkCallback2(
|
||||||
@ -174,13 +177,20 @@ private enum class MethodCall(
|
|||||||
RequestNetwork1(
|
RequestNetwork1(
|
||||||
"Landroid/net/ConnectivityManager;",
|
"Landroid/net/ConnectivityManager;",
|
||||||
"requestNetwork",
|
"requestNetwork",
|
||||||
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"),
|
arrayOf(
|
||||||
|
"Landroid/net/NetworkRequest;",
|
||||||
|
"Landroid/net/ConnectivityManager\$NetworkCallback;"
|
||||||
|
),
|
||||||
"V",
|
"V",
|
||||||
),
|
),
|
||||||
RequestNetwork2(
|
RequestNetwork2(
|
||||||
"Landroid/net/ConnectivityManager;",
|
"Landroid/net/ConnectivityManager;",
|
||||||
"requestNetwork",
|
"requestNetwork",
|
||||||
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;", "I"),
|
arrayOf(
|
||||||
|
"Landroid/net/NetworkRequest;",
|
||||||
|
"Landroid/net/ConnectivityManager\$NetworkCallback;",
|
||||||
|
"I"
|
||||||
|
),
|
||||||
"V",
|
"V",
|
||||||
),
|
),
|
||||||
RequestNetwork3(
|
RequestNetwork3(
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
package app.revanced.patches.all.misc.display.edgetoedge
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.resourcePatch
|
||||||
|
import app.revanced.util.Utils.printWarn
|
||||||
|
import app.revanced.util.getNode
|
||||||
|
import org.w3c.dom.Element
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
val edgeToEdgeDisplayPatch = resourcePatch(
|
||||||
|
name = "Disable edge-to-edge display",
|
||||||
|
description = "Disable forced edge-to-edge display on Android 15+ by changing the app's target SDK version. " +
|
||||||
|
"This patch does not work if the app is installed by mounting.",
|
||||||
|
use = false,
|
||||||
|
) {
|
||||||
|
execute {
|
||||||
|
document("AndroidManifest.xml").use { document ->
|
||||||
|
// Ideally, the patch should only be applied when targetSdkVersion is 35 or greater.
|
||||||
|
// Since ApkTool does not add targetSdkVersion to AndroidManifest, there is no way to check targetSdkVersion.
|
||||||
|
// Instead, it checks compileSdkVersion and prints a warning.
|
||||||
|
try {
|
||||||
|
val manifestElement = document.getNode("manifest") as Element
|
||||||
|
val compileSdkVersion =
|
||||||
|
Integer.parseInt(manifestElement.getAttribute("android:compileSdkVersion"))
|
||||||
|
if (compileSdkVersion < 35) {
|
||||||
|
printWarn("This app may not be forcing edge to edge display (compileSdkVersion: $compileSdkVersion)")
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
printWarn("Failed to check compileSdkVersion")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change targetSdkVersion to 34.
|
||||||
|
document.getElementsByTagName("manifest").item(0).also {
|
||||||
|
it.appendChild(it.ownerDocument.createElement("uses-sdk").also { element ->
|
||||||
|
element.setAttribute("android:targetSdkVersion", "34")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,23 +4,27 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
|||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||||
|
import app.revanced.patcher.patch.PatchException
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||||
import app.revanced.patches.music.utils.extension.Constants.ACCOUNT_CLASS_DESCRIPTOR
|
import app.revanced.patches.music.utils.extension.Constants.ACCOUNT_CLASS_DESCRIPTOR
|
||||||
import app.revanced.patches.music.utils.patch.PatchList.HIDE_ACCOUNT_COMPONENTS
|
import app.revanced.patches.music.utils.patch.PatchList.HIDE_ACCOUNT_COMPONENTS
|
||||||
|
import app.revanced.patches.music.utils.resourceid.channelHandle
|
||||||
import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
|
import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
|
||||||
import app.revanced.patches.music.utils.settings.CategoryType
|
import app.revanced.patches.music.utils.settings.CategoryType
|
||||||
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
|
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
|
||||||
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
|
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
|
||||||
import app.revanced.patches.music.utils.settings.addSwitchPreference
|
import app.revanced.patches.music.utils.settings.addSwitchPreference
|
||||||
import app.revanced.patches.music.utils.settings.settingsPatch
|
import app.revanced.patches.music.utils.settings.settingsPatch
|
||||||
import app.revanced.util.fingerprint.matchOrThrow
|
|
||||||
import app.revanced.util.fingerprint.methodOrThrow
|
import app.revanced.util.fingerprint.methodOrThrow
|
||||||
import app.revanced.util.getReference
|
import app.revanced.util.getReference
|
||||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||||
|
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
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.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.iface.reference.MethodReference
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@ -84,17 +88,50 @@ val accountComponentsPatch = bytecodePatch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// account switcher
|
// account switcher
|
||||||
namesInactiveAccountThumbnailSizeFingerprint.matchOrThrow().let {
|
val textViewField = with(
|
||||||
it.method.apply {
|
channelHandleFingerprint
|
||||||
val targetIndex = it.patternMatch!!.startIndex
|
.methodOrThrow(namesInactiveAccountThumbnailSizeFingerprint)
|
||||||
val targetRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA
|
) {
|
||||||
|
val literalIndex = indexOfFirstLiteralInstructionOrThrow(channelHandle)
|
||||||
|
getInstruction(
|
||||||
|
indexOfFirstInstructionOrThrow(literalIndex) {
|
||||||
|
opcode == Opcode.IPUT_OBJECT &&
|
||||||
|
getReference<FieldReference>()?.type == "Landroid/widget/TextView;"
|
||||||
|
},
|
||||||
|
).getReference<FieldReference>()
|
||||||
|
}
|
||||||
|
|
||||||
addInstructions(
|
namesInactiveAccountThumbnailSizeFingerprint.methodOrThrow().apply {
|
||||||
targetIndex, """
|
var hook = false
|
||||||
invoke-static {v$targetRegister}, $ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Z)Z
|
|
||||||
move-result v$targetRegister
|
implementation!!.instructions
|
||||||
"""
|
.withIndex()
|
||||||
)
|
.filter { (_, instruction) ->
|
||||||
|
val reference =
|
||||||
|
(instruction as? ReferenceInstruction)?.reference
|
||||||
|
instruction.opcode == Opcode.IGET_OBJECT &&
|
||||||
|
reference is FieldReference &&
|
||||||
|
reference == textViewField
|
||||||
|
}
|
||||||
|
.map { (index, _) -> index }
|
||||||
|
.forEach { index ->
|
||||||
|
val insertIndex = index - 1
|
||||||
|
if (!hook && getInstruction(insertIndex).opcode == Opcode.IF_NEZ) {
|
||||||
|
val insertRegister =
|
||||||
|
getInstruction<OneRegisterInstruction>(insertIndex).registerA
|
||||||
|
|
||||||
|
addInstructions(
|
||||||
|
insertIndex, """
|
||||||
|
invoke-static {v$insertRegister}, $ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Z)Z
|
||||||
|
move-result v$insertRegister
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
hook = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hook) {
|
||||||
|
throw PatchException("Could not find TextUtils.isEmpty() index")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
package app.revanced.patches.music.account.components
|
package app.revanced.patches.music.account.components
|
||||||
|
|
||||||
import app.revanced.patches.music.utils.resourceid.accountSwitcherAccessibility
|
import app.revanced.patches.music.utils.resourceid.accountSwitcherAccessibility
|
||||||
|
import app.revanced.patches.music.utils.resourceid.channelHandle
|
||||||
import app.revanced.patches.music.utils.resourceid.menuEntry
|
import app.revanced.patches.music.utils.resourceid.menuEntry
|
||||||
import app.revanced.patches.music.utils.resourceid.namesInactiveAccountThumbnailSize
|
import app.revanced.patches.music.utils.resourceid.namesInactiveAccountThumbnailSize
|
||||||
import app.revanced.patches.music.utils.resourceid.tosFooter
|
import app.revanced.patches.music.utils.resourceid.tosFooter
|
||||||
import app.revanced.util.fingerprint.legacyFingerprint
|
import app.revanced.util.fingerprint.legacyFingerprint
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
|
||||||
|
|
||||||
internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
|
internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
|
||||||
name = "accountSwitcherAccessibilityLabelFingerprint",
|
name = "accountSwitcherAccessibilityLabelFingerprint",
|
||||||
@ -14,6 +14,12 @@ internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
|
|||||||
literals = listOf(accountSwitcherAccessibility)
|
literals = listOf(accountSwitcherAccessibility)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal val channelHandleFingerprint = legacyFingerprint(
|
||||||
|
name = "channelHandleFingerprint",
|
||||||
|
returnType = "V",
|
||||||
|
literals = listOf(channelHandle),
|
||||||
|
)
|
||||||
|
|
||||||
internal val menuEntryFingerprint = legacyFingerprint(
|
internal val menuEntryFingerprint = legacyFingerprint(
|
||||||
name = "menuEntryFingerprint",
|
name = "menuEntryFingerprint",
|
||||||
returnType = "V",
|
returnType = "V",
|
||||||
@ -24,19 +30,6 @@ internal val namesInactiveAccountThumbnailSizeFingerprint = legacyFingerprint(
|
|||||||
name = "namesInactiveAccountThumbnailSizeFingerprint",
|
name = "namesInactiveAccountThumbnailSizeFingerprint",
|
||||||
returnType = "V",
|
returnType = "V",
|
||||||
parameters = listOf("L", "Ljava/lang/Object;"),
|
parameters = listOf("L", "Ljava/lang/Object;"),
|
||||||
opcodes = listOf(
|
|
||||||
Opcode.IF_NEZ,
|
|
||||||
Opcode.IGET_OBJECT,
|
|
||||||
Opcode.INVOKE_VIRTUAL,
|
|
||||||
Opcode.IGET_OBJECT,
|
|
||||||
Opcode.INVOKE_VIRTUAL,
|
|
||||||
Opcode.GOTO,
|
|
||||||
Opcode.IGET_OBJECT,
|
|
||||||
Opcode.INVOKE_VIRTUAL,
|
|
||||||
Opcode.INVOKE_VIRTUAL,
|
|
||||||
Opcode.MOVE_RESULT_OBJECT,
|
|
||||||
Opcode.IF_EQZ
|
|
||||||
),
|
|
||||||
literals = listOf(namesInactiveAccountThumbnailSize)
|
literals = listOf(namesInactiveAccountThumbnailSize)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -115,7 +115,8 @@ val adsPatch = bytecodePatch(
|
|||||||
.methodOrThrow(getPremiumDialogParentFingerprint)
|
.methodOrThrow(getPremiumDialogParentFingerprint)
|
||||||
.apply {
|
.apply {
|
||||||
val setContentViewIndex = indexOfSetContentViewInstruction(this)
|
val setContentViewIndex = indexOfSetContentViewInstruction(this)
|
||||||
val dialogInstruction = getInstruction<FiveRegisterInstruction>(setContentViewIndex)
|
val dialogInstruction =
|
||||||
|
getInstruction<FiveRegisterInstruction>(setContentViewIndex)
|
||||||
val dialogRegister = dialogInstruction.registerC
|
val dialogRegister = dialogInstruction.registerC
|
||||||
val viewRegister = dialogInstruction.registerD
|
val viewRegister = dialogInstruction.registerD
|
||||||
|
|
||||||
|
@ -97,8 +97,6 @@ internal val showDialogCommandFingerprint = legacyFingerprint(
|
|||||||
name = "showDialogCommandFingerprint",
|
name = "showDialogCommandFingerprint",
|
||||||
returnType = "V",
|
returnType = "V",
|
||||||
opcodes = listOf(
|
opcodes = listOf(
|
||||||
Opcode.IF_EQ,
|
|
||||||
Opcode.IGET_OBJECT,
|
|
||||||
Opcode.INVOKE_VIRTUAL,
|
Opcode.INVOKE_VIRTUAL,
|
||||||
Opcode.IGET, // get dialog code
|
Opcode.IGET, // get dialog code
|
||||||
),
|
),
|
||||||
|
@ -5,6 +5,7 @@ import app.revanced.patches.music.utils.resourceid.historyMenuItem
|
|||||||
import app.revanced.patches.music.utils.resourceid.musicTasteBuilderShelf
|
import app.revanced.patches.music.utils.resourceid.musicTasteBuilderShelf
|
||||||
import app.revanced.patches.music.utils.resourceid.offlineSettingsMenuItem
|
import app.revanced.patches.music.utils.resourceid.offlineSettingsMenuItem
|
||||||
import app.revanced.patches.music.utils.resourceid.playerOverlayChip
|
import app.revanced.patches.music.utils.resourceid.playerOverlayChip
|
||||||
|
import app.revanced.patches.music.utils.resourceid.searchButton
|
||||||
import app.revanced.patches.music.utils.resourceid.toolTipContentView
|
import app.revanced.patches.music.utils.resourceid.toolTipContentView
|
||||||
import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView
|
import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView
|
||||||
import app.revanced.util.fingerprint.legacyFingerprint
|
import app.revanced.util.fingerprint.legacyFingerprint
|
||||||
@ -118,6 +119,17 @@ internal val preferenceScreenFingerprint = legacyFingerprint(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
internal val searchActionViewFingerprint = legacyFingerprint(
|
||||||
|
name = "searchActionViewFingerprint",
|
||||||
|
returnType = "Landroid/view/View;",
|
||||||
|
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
|
||||||
|
parameters = emptyList(),
|
||||||
|
literals = listOf(searchButton),
|
||||||
|
customFingerprint = { _, classDef ->
|
||||||
|
classDef.type.endsWith("/SearchActionProvider;")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
internal val searchBarFingerprint = legacyFingerprint(
|
internal val searchBarFingerprint = legacyFingerprint(
|
||||||
name = "searchBarFingerprint",
|
name = "searchBarFingerprint",
|
||||||
returnType = "V",
|
returnType = "V",
|
||||||
|
@ -20,6 +20,7 @@ import app.revanced.patches.music.utils.playservice.is_8_05_or_greater
|
|||||||
import app.revanced.patches.music.utils.playservice.versionCheckPatch
|
import app.revanced.patches.music.utils.playservice.versionCheckPatch
|
||||||
import app.revanced.patches.music.utils.resourceid.musicTasteBuilderShelf
|
import app.revanced.patches.music.utils.resourceid.musicTasteBuilderShelf
|
||||||
import app.revanced.patches.music.utils.resourceid.playerOverlayChip
|
import app.revanced.patches.music.utils.resourceid.playerOverlayChip
|
||||||
|
import app.revanced.patches.music.utils.resourceid.searchButton
|
||||||
import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
|
import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
|
||||||
import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView
|
import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView
|
||||||
import app.revanced.patches.music.utils.settings.CategoryType
|
import app.revanced.patches.music.utils.settings.CategoryType
|
||||||
@ -198,6 +199,23 @@ val layoutComponentsPatch = bytecodePatch(
|
|||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
|
// region patch for hide search button
|
||||||
|
|
||||||
|
searchActionViewFingerprint.methodOrThrow().apply {
|
||||||
|
val constIndex =
|
||||||
|
indexOfFirstLiteralInstructionOrThrow(searchButton)
|
||||||
|
val targetIndex =
|
||||||
|
indexOfFirstInstructionOrThrow(constIndex, Opcode.MOVE_RESULT_OBJECT)
|
||||||
|
val targetRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA
|
||||||
|
|
||||||
|
addInstruction(
|
||||||
|
targetIndex + 1,
|
||||||
|
"invoke-static {v$targetRegister}, $GENERAL_CLASS_DESCRIPTOR->hideSearchButton(Landroid/view/View;)V"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
// region patch for hide sound search button
|
// region patch for hide sound search button
|
||||||
|
|
||||||
if (is_6_48_or_greater) {
|
if (is_6_48_or_greater) {
|
||||||
@ -353,6 +371,11 @@ val layoutComponentsPatch = bytecodePatch(
|
|||||||
"revanced_hide_samples_shelf",
|
"revanced_hide_samples_shelf",
|
||||||
"false"
|
"false"
|
||||||
)
|
)
|
||||||
|
addSwitchPreference(
|
||||||
|
CategoryType.GENERAL,
|
||||||
|
"revanced_hide_search_button",
|
||||||
|
"false"
|
||||||
|
)
|
||||||
if (is_6_48_or_greater) {
|
if (is_6_48_or_greater) {
|
||||||
addSwitchPreference(
|
addSwitchPreference(
|
||||||
CategoryType.GENERAL,
|
CategoryType.GENERAL,
|
||||||
|
@ -32,25 +32,32 @@ private val spoofAppVersionBytecodePatch = bytecodePatch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
if (!is_6_43_or_greater || is_7_25_or_greater) {
|
if (!is_6_43_or_greater) {
|
||||||
return@execute
|
return@execute
|
||||||
}
|
}
|
||||||
if (is_7_17_or_greater) {
|
var defaultVersionString = "6.42.55"
|
||||||
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
|
|
||||||
name == "SpoofAppVersionDefaultString"
|
if (is_7_17_or_greater && !is_7_25_or_greater) {
|
||||||
}.replaceInstruction(
|
defaultVersionString = "7.16.53"
|
||||||
0,
|
defaultValue = "true"
|
||||||
"const-string v0, \"7.16.53\""
|
|
||||||
)
|
|
||||||
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
|
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
|
||||||
name == "SpoofAppVersionDefaultBoolean"
|
name == "SpoofAppVersionDefaultBoolean"
|
||||||
}.replaceInstruction(
|
}.replaceInstruction(
|
||||||
0,
|
0,
|
||||||
"const/4 v0, 0x1"
|
"const/4 v0, 0x1"
|
||||||
)
|
)
|
||||||
|
|
||||||
defaultValue = "true"
|
|
||||||
}
|
}
|
||||||
|
if (is_7_25_or_greater) {
|
||||||
|
defaultVersionString = "7.17.52"
|
||||||
|
}
|
||||||
|
|
||||||
|
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
|
||||||
|
name == "SpoofAppVersionDefaultString"
|
||||||
|
}.replaceInstruction(
|
||||||
|
0,
|
||||||
|
"const-string v0, \"$defaultVersionString\""
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,6 +70,9 @@ val spoofAppVersionPatch = resourcePatch(
|
|||||||
YOUTUBE_MUSIC_PACKAGE_NAME(
|
YOUTUBE_MUSIC_PACKAGE_NAME(
|
||||||
"6.51.53",
|
"6.51.53",
|
||||||
"7.16.53",
|
"7.16.53",
|
||||||
|
"7.25.53",
|
||||||
|
"8.05.51",
|
||||||
|
"8.10.52",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -73,13 +83,19 @@ val spoofAppVersionPatch = resourcePatch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
if (!is_6_43_or_greater || is_7_25_or_greater) {
|
if (!is_6_43_or_greater) {
|
||||||
printWarn("\"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 6.43.53 ~ 7.24.51.")
|
printWarn("\"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 6.51.53 or later.")
|
||||||
return@execute
|
return@execute
|
||||||
}
|
}
|
||||||
if (is_7_17_or_greater) {
|
if (!is_7_17_or_greater) {
|
||||||
|
appendAppVersion("6.42.55")
|
||||||
|
}
|
||||||
|
if (is_7_17_or_greater && !is_7_25_or_greater) {
|
||||||
appendAppVersion("7.16.53")
|
appendAppVersion("7.16.53")
|
||||||
}
|
}
|
||||||
|
if (is_7_25_or_greater) {
|
||||||
|
appendAppVersion("7.17.52")
|
||||||
|
}
|
||||||
|
|
||||||
addSwitchPreference(
|
addSwitchPreference(
|
||||||
CategoryType.GENERAL,
|
CategoryType.GENERAL,
|
||||||
|
@ -206,7 +206,8 @@ val changeHeaderPatch = resourcePatch(
|
|||||||
printWarn(warnings)
|
printWarn(warnings)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isLegacyLogoExists = get("res").resolve("drawable-xxhdpi").resolve("ytm_logo.png").exists()
|
val isLegacyLogoExists =
|
||||||
|
get("res").resolve("drawable-xxhdpi").resolve("ytm_logo.png").exists()
|
||||||
if (is_7_27_or_greater && isLegacyLogoExists) {
|
if (is_7_27_or_greater && isLegacyLogoExists) {
|
||||||
document("res/layout/signin_fragment.xml").use { document ->
|
document("res/layout/signin_fragment.xml").use { document ->
|
||||||
document.doRecursively node@{ node ->
|
document.doRecursively node@{ node ->
|
||||||
|
@ -24,7 +24,7 @@ import app.revanced.util.indexOfFirstInstructionOrThrow
|
|||||||
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
||||||
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
|
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
|
||||||
import com.android.tools.smali.dexlib2.Opcode
|
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.reference.MethodReference
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
|
||||||
private const val EXTENSION_METHOD_DESCRIPTOR =
|
private const val EXTENSION_METHOD_DESCRIPTOR =
|
||||||
@ -41,7 +41,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
|
|||||||
"7.16.53",
|
"7.16.53",
|
||||||
"7.25.53",
|
"7.25.53",
|
||||||
"8.05.51",
|
"8.05.51",
|
||||||
"8.10.51",
|
"8.12.53",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
|
|||||||
return@execute
|
return@execute
|
||||||
} else if (!is_7_20_or_greater) {
|
} else if (!is_7_20_or_greater) {
|
||||||
cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall(
|
cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall(
|
||||||
45635386L,
|
CAIRO_SPLASH_ANIMATION_FEATURE_FLAG,
|
||||||
EXTENSION_METHOD_DESCRIPTOR
|
EXTENSION_METHOD_DESCRIPTOR
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@ -69,18 +69,13 @@ val cairoSplashAnimationPatch = bytecodePatch(
|
|||||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||||
getReference<MethodReference>()?.name == "setContentView"
|
getReference<MethodReference>()?.name == "setContentView"
|
||||||
} + 1
|
} + 1
|
||||||
val viewStubFindViewByIdIndex = indexOfFirstInstructionOrThrow(literalIndex) {
|
val freeIndex = indexOfFirstInstructionOrThrow(insertIndex, Opcode.CONST)
|
||||||
val reference = getReference<MethodReference>()
|
|
||||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
|
||||||
reference?.name == "findViewById" &&
|
|
||||||
reference.definingClass != "Landroid/view/View;"
|
|
||||||
}
|
|
||||||
val freeRegister =
|
val freeRegister =
|
||||||
getInstruction<FiveRegisterInstruction>(viewStubFindViewByIdIndex).registerD
|
getInstruction<OneRegisterInstruction>(freeIndex).registerA
|
||||||
val jumpIndex = indexOfFirstInstructionReversedOrThrow(
|
val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex) {
|
||||||
viewStubFindViewByIdIndex,
|
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||||
Opcode.IGET_OBJECT
|
getReference<MethodReference>()?.parameterTypes?.firstOrNull() == "Ljava/lang/Runnable;"
|
||||||
)
|
} + 1
|
||||||
|
|
||||||
addInstructionsWithLabels(
|
addInstructionsWithLabels(
|
||||||
insertIndex, """
|
insertIndex, """
|
||||||
|
@ -5,6 +5,8 @@ import app.revanced.patches.music.utils.resourceid.mainActivityLaunchAnimation
|
|||||||
import app.revanced.util.fingerprint.legacyFingerprint
|
import app.revanced.util.fingerprint.legacyFingerprint
|
||||||
import app.revanced.util.indexOfFirstLiteralInstruction
|
import app.revanced.util.indexOfFirstLiteralInstruction
|
||||||
|
|
||||||
|
internal const val CAIRO_SPLASH_ANIMATION_FEATURE_FLAG = 45635386L
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This fingerprint is compatible with YouTube Music v7.06.53+
|
* This fingerprint is compatible with YouTube Music v7.06.53+
|
||||||
*/
|
*/
|
||||||
@ -20,7 +22,7 @@ internal val cairoSplashAnimationConfigFingerprint = legacyFingerprint(
|
|||||||
if (is_7_20_or_greater) {
|
if (is_7_20_or_greater) {
|
||||||
method.indexOfFirstLiteralInstruction(mainActivityLaunchAnimation) >= 0
|
method.indexOfFirstLiteralInstruction(mainActivityLaunchAnimation) >= 0
|
||||||
} else {
|
} else {
|
||||||
method.indexOfFirstLiteralInstruction(45635386) >= 0
|
method.indexOfFirstLiteralInstruction(CAIRO_SPLASH_ANIMATION_FEATURE_FLAG) >= 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
package app.revanced.patches.music.misc.watchhistory
|
package app.revanced.patches.music.misc.watchhistory
|
||||||
|
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import app.revanced.patches.shared.trackingurlhook.hookWatchHistory
|
|
||||||
import app.revanced.patches.shared.trackingurlhook.trackingUrlHookPatch
|
|
||||||
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
|
||||||
import app.revanced.patches.music.utils.patch.PatchList.WATCH_HISTORY
|
import app.revanced.patches.music.utils.patch.PatchList.WATCH_HISTORY
|
||||||
import app.revanced.patches.music.utils.settings.CategoryType
|
import app.revanced.patches.music.utils.settings.CategoryType
|
||||||
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
|
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
|
||||||
import app.revanced.patches.music.utils.settings.settingsPatch
|
import app.revanced.patches.music.utils.settings.settingsPatch
|
||||||
|
import app.revanced.patches.shared.trackingurlhook.hookWatchHistory
|
||||||
|
import app.revanced.patches.shared.trackingurlhook.trackingUrlHookPatch
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val watchHistoryPatch = bytecodePatch(
|
val watchHistoryPatch = bytecodePatch(
|
||||||
|
@ -124,14 +124,17 @@ val navigationBarComponentsPatch = bytecodePatch(
|
|||||||
opcode == Opcode.IGET_OBJECT &&
|
opcode == Opcode.IGET_OBJECT &&
|
||||||
getReference<FieldReference>()?.type == "Ljava/lang/String;"
|
getReference<FieldReference>()?.type == "Ljava/lang/String;"
|
||||||
}
|
}
|
||||||
val browseIdReference = getInstruction<ReferenceInstruction>(browseIdIndex).reference as FieldReference
|
val browseIdReference =
|
||||||
|
getInstruction<ReferenceInstruction>(browseIdIndex).reference as FieldReference
|
||||||
val fieldName = browseIdReference.name
|
val fieldName = browseIdReference.name
|
||||||
val componentIndex = indexOfFirstInstructionOrThrow(stringIndex) {
|
val componentIndex = indexOfFirstInstructionOrThrow(stringIndex) {
|
||||||
opcode == Opcode.IGET_OBJECT &&
|
opcode == Opcode.IGET_OBJECT &&
|
||||||
getReference<FieldReference>()?.toString() == browseIdReference.toString()
|
getReference<FieldReference>()?.toString() == browseIdReference.toString()
|
||||||
}
|
}
|
||||||
val browseIdRegister = getInstruction<TwoRegisterInstruction>(componentIndex).registerA
|
val browseIdRegister =
|
||||||
val componentRegister = getInstruction<TwoRegisterInstruction>(componentIndex).registerB
|
getInstruction<TwoRegisterInstruction>(componentIndex).registerA
|
||||||
|
val componentRegister =
|
||||||
|
getInstruction<TwoRegisterInstruction>(componentIndex).registerB
|
||||||
|
|
||||||
val enumIndex = it.patternMatch!!.startIndex + 3
|
val enumIndex = it.patternMatch!!.startIndex + 3
|
||||||
val enumRegister = getInstruction<OneRegisterInstruction>(enumIndex).registerA
|
val enumRegister = getInstruction<OneRegisterInstruction>(enumIndex).registerA
|
||||||
|
@ -54,7 +54,7 @@ internal val engagementPanelHeightFingerprint = legacyFingerprint(
|
|||||||
parameters = emptyList(),
|
parameters = emptyList(),
|
||||||
customFingerprint = { method, _ ->
|
customFingerprint = { method, _ ->
|
||||||
AccessFlags.FINAL.isSet(method.accessFlags) &&
|
AccessFlags.FINAL.isSet(method.accessFlags) &&
|
||||||
method.containsLiteralInstruction(1) &&
|
method.containsLiteralInstruction(1) &&
|
||||||
method.indexOfFirstInstruction {
|
method.indexOfFirstInstruction {
|
||||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||||
getReference<MethodReference>()?.name == "booleanValue"
|
getReference<MethodReference>()?.name == "booleanValue"
|
||||||
|
@ -746,15 +746,22 @@ val playerComponentsPatch = bytecodePatch(
|
|||||||
val freeRegister =
|
val freeRegister =
|
||||||
getInstruction<FiveRegisterInstruction>(bottomSheetBehaviorIndex).registerD
|
getInstruction<FiveRegisterInstruction>(bottomSheetBehaviorIndex).registerD
|
||||||
|
|
||||||
|
val getFieldIndex = bottomSheetBehaviorIndex - 2
|
||||||
|
val getFieldReference =
|
||||||
|
getInstruction<ReferenceInstruction>(getFieldIndex).reference
|
||||||
|
val getFieldInstruction = getInstruction<TwoRegisterInstruction>(getFieldIndex)
|
||||||
|
|
||||||
addInstructionsWithLabels(
|
addInstructionsWithLabels(
|
||||||
bottomSheetBehaviorIndex - 2,
|
getFieldIndex + 1,
|
||||||
"""
|
"""
|
||||||
invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer()Z
|
invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer()Z
|
||||||
move-result v$freeRegister
|
move-result v$freeRegister
|
||||||
if-nez v$freeRegister, :dismiss
|
if-nez v$freeRegister, :dismiss
|
||||||
|
iget-object v${getFieldInstruction.registerA}, v${getFieldInstruction.registerB}, $getFieldReference
|
||||||
""",
|
""",
|
||||||
ExternalLabel("dismiss", getInstruction(bottomSheetBehaviorIndex + 1))
|
ExternalLabel("dismiss", getInstruction(bottomSheetBehaviorIndex + 1))
|
||||||
)
|
)
|
||||||
|
removeInstruction(getFieldIndex)
|
||||||
} ?: throw PatchException("Could not find targetMethod")
|
} ?: throw PatchException("Could not find targetMethod")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ internal object Constants {
|
|||||||
"7.16.53", // This is the latest version that supports the 'Spoof app version' patch.
|
"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.
|
"7.25.53", // This is the last supported version for 2024.
|
||||||
"8.05.51", // This was the latest version supported by the previous 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.
|
"8.12.53", // This is the latest version supported by the RVX patch.
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -1,14 +1,8 @@
|
|||||||
package app.revanced.patches.music.utils.extension
|
package app.revanced.patches.music.utils.extension
|
||||||
|
|
||||||
import app.revanced.patches.music.utils.extension.hooks.applicationInitHook
|
import app.revanced.patches.music.utils.extension.hooks.applicationInitHook
|
||||||
import app.revanced.patches.music.utils.extension.hooks.mainActivityBaseContextHook
|
|
||||||
import app.revanced.patches.shared.extension.hooks.cronetEngineContextHook
|
|
||||||
import app.revanced.patches.shared.extension.hooks.firebaseInitProviderContextHook
|
|
||||||
import app.revanced.patches.shared.extension.sharedExtensionPatch
|
import app.revanced.patches.shared.extension.sharedExtensionPatch
|
||||||
|
|
||||||
val sharedExtensionPatch = sharedExtensionPatch(
|
val sharedExtensionPatch = sharedExtensionPatch(
|
||||||
applicationInitHook,
|
applicationInitHook,
|
||||||
cronetEngineContextHook,
|
|
||||||
firebaseInitProviderContextHook,
|
|
||||||
mainActivityBaseContextHook,
|
|
||||||
)
|
)
|
||||||
|
@ -1,32 +0,0 @@
|
|||||||
package app.revanced.patches.music.utils.extension.hooks
|
|
||||||
|
|
||||||
import app.revanced.patches.shared.extension.extensionHook
|
|
||||||
import app.revanced.util.getReference
|
|
||||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
|
||||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
|
||||||
|
|
||||||
private var attachBaseContextIndex = -1
|
|
||||||
|
|
||||||
internal val mainActivityBaseContextHook = extensionHook(
|
|
||||||
insertIndexResolver = { method ->
|
|
||||||
attachBaseContextIndex = method.indexOfFirstInstructionOrThrow {
|
|
||||||
getReference<MethodReference>()?.name == "attachBaseContext"
|
|
||||||
}
|
|
||||||
|
|
||||||
attachBaseContextIndex + 1
|
|
||||||
},
|
|
||||||
contextRegisterResolver = { method ->
|
|
||||||
val overrideInstruction =
|
|
||||||
method.implementation!!.instructions.elementAt(attachBaseContextIndex)
|
|
||||||
as FiveRegisterInstruction
|
|
||||||
"v${overrideInstruction.registerD}"
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
returns("V")
|
|
||||||
parameters("Landroid/content/Context;")
|
|
||||||
custom { method, classDef ->
|
|
||||||
classDef.type == "Lcom/google/android/apps/youtube/music/activities/MusicActivity;" &&
|
|
||||||
method.name == "attachBaseContext"
|
|
||||||
}
|
|
||||||
}
|
|
@ -22,7 +22,6 @@ import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
|
|||||||
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
|
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
|
||||||
import app.revanced.patches.music.utils.settings.addSwitchPreference
|
import app.revanced.patches.music.utils.settings.addSwitchPreference
|
||||||
import app.revanced.patches.music.utils.settings.settingsPatch
|
import app.revanced.patches.music.utils.settings.settingsPatch
|
||||||
import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
|
|
||||||
import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint
|
import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint
|
||||||
import app.revanced.patches.shared.customspeed.customPlaybackSpeedPatch
|
import app.revanced.patches.shared.customspeed.customPlaybackSpeedPatch
|
||||||
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
|
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
|
||||||
@ -31,6 +30,7 @@ import app.revanced.patches.shared.indexOfBrandInstruction
|
|||||||
import app.revanced.patches.shared.indexOfManufacturerInstruction
|
import app.revanced.patches.shared.indexOfManufacturerInstruction
|
||||||
import app.revanced.patches.shared.indexOfModelInstruction
|
import app.revanced.patches.shared.indexOfModelInstruction
|
||||||
import app.revanced.patches.shared.indexOfReleaseInstruction
|
import app.revanced.patches.shared.indexOfReleaseInstruction
|
||||||
|
import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
|
||||||
import app.revanced.util.findMethodOrThrow
|
import app.revanced.util.findMethodOrThrow
|
||||||
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
|
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
|
||||||
import app.revanced.util.fingerprint.matchOrThrow
|
import app.revanced.util.fingerprint.matchOrThrow
|
||||||
@ -63,9 +63,11 @@ private const val CLIENT_INFO_CLASS_DESCRIPTOR =
|
|||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val spoofClientPatch = bytecodePatch(
|
val spoofClientPatch = bytecodePatch(
|
||||||
SPOOF_CLIENT.title,
|
// Removed from the patch list to avoid user confusion:
|
||||||
SPOOF_CLIENT.summary,
|
// https://github.com/inotia00/ReVanced_Extended/issues/2832#issuecomment-2745941171
|
||||||
false,
|
// SPOOF_CLIENT.title,
|
||||||
|
// SPOOF_CLIENT.summary,
|
||||||
|
// false,
|
||||||
) {
|
) {
|
||||||
compatibleWith(COMPATIBLE_PACKAGE)
|
compatibleWith(COMPATIBLE_PACKAGE)
|
||||||
|
|
||||||
|
@ -9,9 +9,8 @@ import app.revanced.patches.shared.mapping.ResourceType.ID
|
|||||||
import app.revanced.patches.shared.mapping.ResourceType.LAYOUT
|
import app.revanced.patches.shared.mapping.ResourceType.LAYOUT
|
||||||
import app.revanced.patches.shared.mapping.ResourceType.STRING
|
import app.revanced.patches.shared.mapping.ResourceType.STRING
|
||||||
import app.revanced.patches.shared.mapping.ResourceType.STYLE
|
import app.revanced.patches.shared.mapping.ResourceType.STYLE
|
||||||
import app.revanced.patches.shared.mapping.get
|
import app.revanced.patches.shared.mapping.getResourceId
|
||||||
import app.revanced.patches.shared.mapping.resourceMappingPatch
|
import app.revanced.patches.shared.mapping.resourceMappingPatch
|
||||||
import app.revanced.patches.shared.mapping.resourceMappings
|
|
||||||
|
|
||||||
var accountSwitcherAccessibility = -1L
|
var accountSwitcherAccessibility = -1L
|
||||||
private set
|
private set
|
||||||
@ -25,6 +24,8 @@ var buttonContainer = -1L
|
|||||||
private set
|
private set
|
||||||
var buttonIconPaddingMedium = -1L
|
var buttonIconPaddingMedium = -1L
|
||||||
private set
|
private set
|
||||||
|
var channelHandle = -1L
|
||||||
|
private set
|
||||||
var chipCloud = -1L
|
var chipCloud = -1L
|
||||||
private set
|
private set
|
||||||
var colorGrey = -1L
|
var colorGrey = -1L
|
||||||
@ -83,6 +84,8 @@ var qualityAuto = -1L
|
|||||||
private set
|
private set
|
||||||
var remixGenericButtonSize = -1L
|
var remixGenericButtonSize = -1L
|
||||||
private set
|
private set
|
||||||
|
var searchButton = -1L
|
||||||
|
private set
|
||||||
var slidingDialogAnimation = -1L
|
var slidingDialogAnimation = -1L
|
||||||
private set
|
private set
|
||||||
var tapBloomView = -1L
|
var tapBloomView = -1L
|
||||||
@ -124,213 +127,61 @@ internal val sharedResourceIdPatch = resourcePatch(
|
|||||||
dependsOn(resourceMappingPatch)
|
dependsOn(resourceMappingPatch)
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
accountSwitcherAccessibility = resourceMappings[
|
accountSwitcherAccessibility = getResourceId(STRING, "account_switcher_accessibility_label")
|
||||||
STRING,
|
actionBarLogo = getResourceId(DRAWABLE, "action_bar_logo")
|
||||||
"account_switcher_accessibility_label",
|
actionBarLogoRingo2 = getResourceId(DRAWABLE, "action_bar_logo_ringo2")
|
||||||
]
|
bottomSheetRecyclerView = getResourceId(LAYOUT, "bottom_sheet_recycler_view")
|
||||||
actionBarLogo = resourceMappings[
|
buttonContainer = getResourceId(ID, "button_container")
|
||||||
DRAWABLE,
|
buttonIconPaddingMedium = getResourceId(DIMEN, "button_icon_padding_medium")
|
||||||
"action_bar_logo",
|
channelHandle = getResourceId(ID, "channel_handle")
|
||||||
]
|
chipCloud = getResourceId(LAYOUT, "chip_cloud")
|
||||||
actionBarLogoRingo2 = resourceMappings[
|
colorGrey = getResourceId(COLOR, "ytm_color_grey_12")
|
||||||
DRAWABLE,
|
darkBackground = getResourceId(ID, "dark_background")
|
||||||
"action_bar_logo_ringo2",
|
designBottomSheetDialog = getResourceId(LAYOUT, "design_bottom_sheet_dialog")
|
||||||
]
|
elementsContainer = getResourceId(ID, "elements_container")
|
||||||
bottomSheetRecyclerView = resourceMappings[
|
endButtonsContainer = getResourceId(ID, "end_buttons_container")
|
||||||
LAYOUT,
|
floatingLayout = getResourceId(ID, "floating_layout")
|
||||||
"bottom_sheet_recycler_view"
|
historyMenuItem = getResourceId(ID, "history_menu_item")
|
||||||
]
|
inlineTimeBarAdBreakMarkerColor =
|
||||||
buttonContainer = resourceMappings[
|
getResourceId(COLOR, "inline_time_bar_ad_break_marker_color")
|
||||||
ID,
|
inlineTimeBarProgressColor = getResourceId(COLOR, "inline_time_bar_progress_color")
|
||||||
"button_container"
|
interstitialsContainer = getResourceId(ID, "interstitials_container")
|
||||||
]
|
isTablet = getResourceId(BOOL, "is_tablet")
|
||||||
buttonIconPaddingMedium = resourceMappings[
|
likeDislikeContainer = getResourceId(ID, "like_dislike_container")
|
||||||
DIMEN,
|
mainActivityLaunchAnimation = getResourceId(LAYOUT, "main_activity_launch_animation")
|
||||||
"button_icon_padding_medium"
|
menuEntry = getResourceId(LAYOUT, "menu_entry")
|
||||||
]
|
miniPlayerDefaultText = getResourceId(STRING, "mini_player_default_text")
|
||||||
chipCloud = resourceMappings[
|
miniPlayerMdxPlaying = getResourceId(STRING, "mini_player_mdx_playing")
|
||||||
LAYOUT,
|
miniPlayerPlayPauseReplayButton = getResourceId(ID, "mini_player_play_pause_replay_button")
|
||||||
"chip_cloud"
|
miniPlayerViewPager = getResourceId(ID, "mini_player_view_pager")
|
||||||
]
|
modernDialogBackground = getResourceId(DRAWABLE, "modern_dialog_background")
|
||||||
colorGrey = resourceMappings[
|
musicNotifierShelf = getResourceId(LAYOUT, "music_notifier_shelf")
|
||||||
COLOR,
|
musicTasteBuilderShelf = getResourceId(LAYOUT, "music_tastebuilder_shelf")
|
||||||
"ytm_color_grey_12"
|
namesInactiveAccountThumbnailSize =
|
||||||
]
|
getResourceId(DIMEN, "names_inactive_account_thumbnail_size")
|
||||||
darkBackground = resourceMappings[
|
offlineSettingsMenuItem = getResourceId(ID, "offline_settings_menu_item")
|
||||||
ID,
|
playerOverlayChip = getResourceId(ID, "player_overlay_chip")
|
||||||
"dark_background"
|
playerViewPager = getResourceId(ID, "player_view_pager")
|
||||||
]
|
privacyTosFooter = getResourceId(ID, "privacy_tos_footer")
|
||||||
designBottomSheetDialog = resourceMappings[
|
qualityAuto = getResourceId(STRING, "quality_auto")
|
||||||
LAYOUT,
|
remixGenericButtonSize = getResourceId(DIMEN, "remix_generic_button_size")
|
||||||
"design_bottom_sheet_dialog"
|
searchButton = getResourceId(LAYOUT, "search_button")
|
||||||
]
|
slidingDialogAnimation = getResourceId(STYLE, "SlidingDialogAnimation")
|
||||||
elementsContainer = resourceMappings[
|
tapBloomView = getResourceId(ID, "tap_bloom_view")
|
||||||
ID,
|
text1 = getResourceId(ID, "text1")
|
||||||
"elements_container"
|
toolTipContentView = getResourceId(LAYOUT, "tooltip_content_view")
|
||||||
]
|
topEnd = getResourceId(ID, "TOP_END")
|
||||||
endButtonsContainer = resourceMappings[
|
topStart = getResourceId(ID, "TOP_START")
|
||||||
ID,
|
topBarMenuItemImageView = getResourceId(ID, "top_bar_menu_item_image_view")
|
||||||
"end_buttons_container"
|
tosFooter = getResourceId(ID, "tos_footer")
|
||||||
]
|
touchOutside = getResourceId(ID, "touch_outside")
|
||||||
floatingLayout = resourceMappings[
|
trimSilenceSwitch = getResourceId(ID, "trim_silence_switch")
|
||||||
ID,
|
varispeedUnavailableTitle = getResourceId(STRING, "varispeed_unavailable_title")
|
||||||
"floating_layout"
|
ytFillSamples = getResourceId(DRAWABLE, "yt_fill_samples_vd_theme_24")
|
||||||
]
|
ytFillYouTubeMusic = getResourceId(DRAWABLE, "yt_fill_youtube_music_vd_theme_24")
|
||||||
historyMenuItem = resourceMappings[
|
ytOutlineSamples = getResourceId(DRAWABLE, "yt_outline_samples_vd_theme_24")
|
||||||
ID,
|
ytOutlineYouTubeMusic = getResourceId(DRAWABLE, "yt_outline_youtube_music_vd_theme_24")
|
||||||
"history_menu_item"
|
ytmLogo = getResourceId(DRAWABLE, "ytm_logo")
|
||||||
]
|
ytmLogoRingo2 = getResourceId(DRAWABLE, "ytm_logo_ringo2")
|
||||||
inlineTimeBarAdBreakMarkerColor = resourceMappings[
|
|
||||||
COLOR,
|
|
||||||
"inline_time_bar_ad_break_marker_color"
|
|
||||||
]
|
|
||||||
inlineTimeBarProgressColor = resourceMappings[
|
|
||||||
COLOR,
|
|
||||||
"inline_time_bar_progress_color"
|
|
||||||
]
|
|
||||||
interstitialsContainer = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"interstitials_container"
|
|
||||||
]
|
|
||||||
isTablet = resourceMappings[
|
|
||||||
BOOL,
|
|
||||||
"is_tablet"
|
|
||||||
]
|
|
||||||
likeDislikeContainer = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"like_dislike_container"
|
|
||||||
]
|
|
||||||
mainActivityLaunchAnimation = resourceMappings[
|
|
||||||
LAYOUT,
|
|
||||||
"main_activity_launch_animation"
|
|
||||||
]
|
|
||||||
menuEntry = resourceMappings[
|
|
||||||
LAYOUT,
|
|
||||||
"menu_entry"
|
|
||||||
]
|
|
||||||
miniPlayerDefaultText = resourceMappings[
|
|
||||||
STRING,
|
|
||||||
"mini_player_default_text"
|
|
||||||
]
|
|
||||||
miniPlayerMdxPlaying = resourceMappings[
|
|
||||||
STRING,
|
|
||||||
"mini_player_mdx_playing"
|
|
||||||
]
|
|
||||||
miniPlayerPlayPauseReplayButton = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"mini_player_play_pause_replay_button"
|
|
||||||
]
|
|
||||||
miniPlayerViewPager = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"mini_player_view_pager"
|
|
||||||
]
|
|
||||||
modernDialogBackground = resourceMappings[
|
|
||||||
DRAWABLE,
|
|
||||||
"modern_dialog_background"
|
|
||||||
]
|
|
||||||
musicNotifierShelf = resourceMappings[
|
|
||||||
LAYOUT,
|
|
||||||
"music_notifier_shelf"
|
|
||||||
]
|
|
||||||
musicTasteBuilderShelf = resourceMappings[
|
|
||||||
LAYOUT,
|
|
||||||
"music_tastebuilder_shelf"
|
|
||||||
]
|
|
||||||
namesInactiveAccountThumbnailSize = resourceMappings[
|
|
||||||
DIMEN,
|
|
||||||
"names_inactive_account_thumbnail_size"
|
|
||||||
]
|
|
||||||
offlineSettingsMenuItem = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"offline_settings_menu_item"
|
|
||||||
]
|
|
||||||
playerOverlayChip = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"player_overlay_chip"
|
|
||||||
]
|
|
||||||
playerViewPager = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"player_view_pager"
|
|
||||||
]
|
|
||||||
privacyTosFooter = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"privacy_tos_footer"
|
|
||||||
]
|
|
||||||
qualityAuto = resourceMappings[
|
|
||||||
STRING,
|
|
||||||
"quality_auto"
|
|
||||||
]
|
|
||||||
remixGenericButtonSize = resourceMappings[
|
|
||||||
DIMEN,
|
|
||||||
"remix_generic_button_size"
|
|
||||||
]
|
|
||||||
slidingDialogAnimation = resourceMappings[
|
|
||||||
STYLE,
|
|
||||||
"SlidingDialogAnimation"
|
|
||||||
]
|
|
||||||
tapBloomView = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"tap_bloom_view"
|
|
||||||
]
|
|
||||||
text1 = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"text1"
|
|
||||||
]
|
|
||||||
toolTipContentView = resourceMappings[
|
|
||||||
LAYOUT,
|
|
||||||
"tooltip_content_view"
|
|
||||||
]
|
|
||||||
topEnd = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"TOP_END"
|
|
||||||
]
|
|
||||||
topStart = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"TOP_START"
|
|
||||||
]
|
|
||||||
topBarMenuItemImageView = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"top_bar_menu_item_image_view"
|
|
||||||
]
|
|
||||||
tosFooter = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"tos_footer"
|
|
||||||
]
|
|
||||||
touchOutside = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"touch_outside"
|
|
||||||
]
|
|
||||||
trimSilenceSwitch = resourceMappings[
|
|
||||||
ID,
|
|
||||||
"trim_silence_switch"
|
|
||||||
]
|
|
||||||
varispeedUnavailableTitle = resourceMappings[
|
|
||||||
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",
|
|
||||||
]
|
|
||||||
ytmLogoRingo2 = resourceMappings[
|
|
||||||
DRAWABLE,
|
|
||||||
"ytm_logo_ringo2",
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -25,7 +25,8 @@ val videoTypeHookPatch = bytecodePatch(
|
|||||||
|
|
||||||
videoTypeFingerprint.methodOrThrow(videoTypeParentFingerprint).apply {
|
videoTypeFingerprint.methodOrThrow(videoTypeParentFingerprint).apply {
|
||||||
val getEnumIndex = indexOfGetEnumInstruction(this)
|
val getEnumIndex = indexOfGetEnumInstruction(this)
|
||||||
val enumClass = (getInstruction<ReferenceInstruction>(getEnumIndex).reference as MethodReference).definingClass
|
val enumClass =
|
||||||
|
(getInstruction<ReferenceInstruction>(getEnumIndex).reference as MethodReference).definingClass
|
||||||
val referenceIndex = indexOfFirstInstructionOrThrow(getEnumIndex) {
|
val referenceIndex = indexOfFirstInstructionOrThrow(getEnumIndex) {
|
||||||
opcode == Opcode.SGET_OBJECT &&
|
opcode == Opcode.SGET_OBJECT &&
|
||||||
getReference<FieldReference>()?.type == enumClass
|
getReference<FieldReference>()?.type == enumClass
|
||||||
|
@ -71,7 +71,8 @@ val playerResponseMethodHookPatch = bytecodePatch(
|
|||||||
val beforeVideoIdHooks =
|
val beforeVideoIdHooks =
|
||||||
hooks.filterIsInstance<Hook.PlayerParameterBeforeVideoId>().asReversed()
|
hooks.filterIsInstance<Hook.PlayerParameterBeforeVideoId>().asReversed()
|
||||||
val videoIdHooks = hooks.filterIsInstance<Hook.VideoId>().asReversed()
|
val videoIdHooks = hooks.filterIsInstance<Hook.VideoId>().asReversed()
|
||||||
val videoIdAndPlaylistIdHooks = hooks.filterIsInstance<Hook.VideoIdAndPlaylistId>().asReversed()
|
val videoIdAndPlaylistIdHooks =
|
||||||
|
hooks.filterIsInstance<Hook.VideoIdAndPlaylistId>().asReversed()
|
||||||
val afterVideoIdHooks = hooks.filterIsInstance<Hook.PlayerParameter>().asReversed()
|
val afterVideoIdHooks = hooks.filterIsInstance<Hook.PlayerParameter>().asReversed()
|
||||||
|
|
||||||
// Add the hooks in this specific order as they insert instructions at the beginning of the method.
|
// Add the hooks in this specific order as they insert instructions at the beginning of the method.
|
||||||
|
@ -27,14 +27,6 @@ import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
|||||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||||
"$PATCHES_PATH/GeneralAdsPatch;"
|
"$PATCHES_PATH/GeneralAdsPatch;"
|
||||||
|
|
||||||
private val isCommentAdsMethod: Method.() -> Boolean = {
|
|
||||||
parameterTypes.size == 1 &&
|
|
||||||
parameterTypes.first().startsWith("Lcom/reddit/ads/conversation/") &&
|
|
||||||
accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL &&
|
|
||||||
returnType == "V" &&
|
|
||||||
indexOfFirstStringInstruction("ad") >= 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val adsPatch = bytecodePatch(
|
val adsPatch = bytecodePatch(
|
||||||
HIDE_ADS.title,
|
HIDE_ADS.title,
|
||||||
@ -94,11 +86,20 @@ val adsPatch = bytecodePatch(
|
|||||||
if (is_2025_06_or_greater) {
|
if (is_2025_06_or_greater) {
|
||||||
listOf(
|
listOf(
|
||||||
commentAdCommentScreenAdViewFingerprint,
|
commentAdCommentScreenAdViewFingerprint,
|
||||||
commentAdDetailListHeaderViewFingerprint
|
commentAdDetailListHeaderViewFingerprint,
|
||||||
|
commentsViewModelFingerprint
|
||||||
).forEach { fingerprint ->
|
).forEach { fingerprint ->
|
||||||
fingerprint.methodOrThrow().hook()
|
fingerprint.methodOrThrow().hook()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
val isCommentAdsMethod: Method.() -> Boolean = {
|
||||||
|
parameterTypes.size == 1 &&
|
||||||
|
parameterTypes.first().startsWith("Lcom/reddit/ads/conversation/") &&
|
||||||
|
accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL &&
|
||||||
|
returnType == "V" &&
|
||||||
|
indexOfFirstStringInstruction("ad") >= 0
|
||||||
|
}
|
||||||
|
|
||||||
classes.forEach { classDef ->
|
classes.forEach { classDef ->
|
||||||
classDef.methods.forEach { method ->
|
classDef.methods.forEach { method ->
|
||||||
if (method.isCommentAdsMethod()) {
|
if (method.isCommentAdsMethod()) {
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user