mirror of
https://github.com/inotia00/revanced-patches.git
synced 2025-04-29 22:24:31 +02:00
Compare commits
107 Commits
v5.5.1
...
revanced-e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
42cc8201f5 | ||
![]() |
15d0fafcf8 | ||
![]() |
db10e410d3 | ||
![]() |
8f23d76f37 | ||
![]() |
7168e49121 | ||
![]() |
2b52380294 | ||
![]() |
0cc693961a | ||
![]() |
bbf863c630 | ||
![]() |
783e366242 | ||
![]() |
4a19a960c5 | ||
![]() |
e55fd4eb74 | ||
![]() |
64af7fd8b6 | ||
![]() |
457dbfec6c | ||
![]() |
22b98336d5 | ||
![]() |
7e4a71b385 | ||
![]() |
bb5964ce98 | ||
![]() |
15db05c636 | ||
![]() |
3f9edca15d | ||
![]() |
1eca8c854c | ||
![]() |
692b4f2c53 | ||
![]() |
5162dccecc | ||
![]() |
56b713b0db | ||
![]() |
f761bc45ec | ||
![]() |
bccd6dc5df | ||
![]() |
09a8eb7114 | ||
![]() |
b439ef3ee7 | ||
![]() |
7f85e802c2 | ||
![]() |
bb1498df76 | ||
![]() |
1c58c6a36e | ||
![]() |
26bf2f4b82 | ||
![]() |
a037b25c13 | ||
![]() |
b72bb71e30 | ||
![]() |
28ff781786 | ||
![]() |
f249e88ce8 | ||
![]() |
036e3dad11 | ||
![]() |
9342e1f7d2 | ||
![]() |
f19dd54026 | ||
![]() |
edccd61e6b | ||
![]() |
35e6c26823 | ||
![]() |
88d59d05b9 | ||
![]() |
aac38dc8af | ||
![]() |
1dd7eda606 | ||
![]() |
05195caa5a | ||
![]() |
82158deaf2 | ||
![]() |
21be41c2a9 | ||
![]() |
56d2f7c4ad | ||
![]() |
aca0591575 | ||
![]() |
274e10aabc | ||
![]() |
4330b7f6df | ||
![]() |
4feff6b150 | ||
![]() |
fbf19ee78b | ||
![]() |
e157e9447d | ||
![]() |
79f933dad4 | ||
![]() |
b8f3917b55 | ||
![]() |
758e8ac568 | ||
![]() |
b8b61fdf51 | ||
![]() |
34e482b03e | ||
![]() |
481a310d06 | ||
![]() |
6818df4507 | ||
![]() |
af26cd58a8 | ||
![]() |
3e8c748f48 | ||
![]() |
f0b1155d20 | ||
![]() |
bf9ba0b1ef | ||
![]() |
82ceb8aa76 | ||
![]() |
4f911d9a55 | ||
![]() |
a235c84e19 | ||
![]() |
98d362ad93 | ||
![]() |
8169ccacc2 | ||
![]() |
f3abc04812 | ||
![]() |
5446847f5f | ||
![]() |
29ba8f7a7d | ||
![]() |
0639b559b1 | ||
![]() |
dcb72cc803 | ||
![]() |
d4ad05d4ba | ||
![]() |
451a14a74d | ||
![]() |
99fa969857 | ||
![]() |
efead108f9 | ||
![]() |
9d37e31a24 | ||
![]() |
a764e3aea3 | ||
![]() |
0bfabbc384 | ||
![]() |
cf90f7b94e | ||
![]() |
df41f035b6 | ||
![]() |
5c743c299a | ||
![]() |
6ac6fcf953 | ||
![]() |
123082b676 | ||
![]() |
4bed9f346d | ||
![]() |
9046d2d959 | ||
![]() |
227afa532a | ||
![]() |
0c5c3a87be | ||
![]() |
c22391926b | ||
![]() |
a235454d80 | ||
![]() |
43a8073a8f | ||
![]() |
e7e011667e | ||
![]() |
7609b904d4 | ||
![]() |
8666d14297 | ||
![]() |
529e0163cc | ||
![]() |
fd32e4a854 | ||
![]() |
aebeb8b9d3 | ||
![]() |
b99ec2cd8d | ||
![]() |
8ec5f19bdb | ||
![]() |
6dfd88c75e | ||
![]() |
98f2e99061 | ||
![]() |
c3d05fd01d | ||
![]() |
872db64da1 | ||
![]() |
84b122f628 | ||
![]() |
c85816bbe9 | ||
![]() |
fec13ad15b |
257
README.md
257
README.md
@ -11,72 +11,73 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
|
||||
|
||||
| 💊 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 |
|
||||
| `Ambient mode control` | Adds options to disable Ambient mode and to bypass Ambient mode restrictions. | 18.29.38 ~ 19.44.39 |
|
||||
| `Bypass URL redirects` | Adds an option to bypass URL redirects and open the original URL directly. | 18.29.38 ~ 19.44.39 |
|
||||
| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 18.29.38 ~ 19.44.39 |
|
||||
| `Change form factor` | Adds an option to change the UI appearance to a phone, tablet, or automotive device. | 18.29.38 ~ 19.44.39 |
|
||||
| `Change live ring click action` | Adds an option to open the channel instead of the live stream when clicking on the live ring. | 18.29.38 ~ 19.44.39 |
|
||||
| `Change player flyout menu toggles` | Adds an option to use text toggles instead of switch toggles within the additional settings menu. | 18.29.38 ~ 19.44.39 |
|
||||
| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 18.29.38 ~ 19.44.39 |
|
||||
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 18.29.38 ~ 19.44.39 |
|
||||
| `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 branding icon for YouTube` | Changes the YouTube app icon to the icon 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. | 18.29.38 ~ 19.44.39 |
|
||||
| `Custom double tap length` | Adds Double-tap to seek values that are specified in patch options. | 18.29.38 ~ 19.44.39 |
|
||||
| `Custom header for YouTube` | Applies a custom header in the top left corner within the app. | 18.29.38 ~ 19.44.39 |
|
||||
| `Description components` | Adds options to hide and disable description components. | 18.29.38 ~ 19.44.39 |
|
||||
| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 18.29.38 ~ 19.44.39 |
|
||||
| `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 captions` | Adds an option to disable captions from being automatically enabled. | 18.29.38 ~ 19.44.39 |
|
||||
| `Disable haptic feedback` | Adds options to disable haptic feedback when swiping in the video player. | 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. | 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. | 18.29.38 ~ 19.44.39 |
|
||||
| `Disable splash animation` | Adds an option to disable the splash animation on app startup. | 18.29.38 ~ 19.44.39 |
|
||||
| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 18.29.38 ~ 19.44.39 |
|
||||
| `Enable debug logging` | Adds an option to enable debug logging. | 18.29.38 ~ 19.44.39 |
|
||||
| `Enable gradient loading screen` | Adds an option to enable the gradient loading screen. | 18.29.38 ~ 19.44.39 |
|
||||
| `Force hide player buttons background` | Removes, at compile time, the dark background surrounding the video player controls. | 18.29.38 ~ 19.44.39 |
|
||||
| `Fullscreen components` | Adds options to hide or change components related to fullscreen. | 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. | 18.29.38 ~ 19.44.39 |
|
||||
| `Hide Shorts dimming` | Removes, at compile time, the dimming effect at the top and bottom of Shorts 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?'. | 18.29.38 ~ 19.44.39 |
|
||||
| `Hide action buttons` | Adds options to hide action buttons under videos. | 18.29.38 ~ 19.44.39 |
|
||||
| `Hide ads` | Adds options to hide ads. | 18.29.38 ~ 19.44.39 |
|
||||
| `Hide comments components` | Adds options to hide components related to comments. | 18.29.38 ~ 19.44.39 |
|
||||
| `Hide feed components` | Adds options to hide components related to feeds. | 18.29.38 ~ 19.44.39 |
|
||||
| `Hide feed flyout menu` | Adds the ability to hide feed flyout menu components using a custom filter. | 18.29.38 ~ 19.44.39 |
|
||||
| `Hide layout components` | Adds options to hide general layout components. | 18.29.38 ~ 19.44.39 |
|
||||
| `Hide player buttons` | Adds options to hide buttons in the video player. | 18.29.38 ~ 19.44.39 |
|
||||
| `Hide player flyout menu` | Adds options to hide player flyout menu components. | 18.29.38 ~ 19.44.39 |
|
||||
| `Hide shortcuts` | Remove, at compile time, the app shortcuts that appears when the app icon is long pressed. | 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. | 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. | 18.29.38 ~ 19.44.39 |
|
||||
| `MaterialYou` | Applies the MaterialYou theme for Android 12+ devices. | 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. | 18.29.38 ~ 19.44.39 |
|
||||
| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 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. | 18.29.38 ~ 19.44.39 |
|
||||
| `Overlay buttons` | Adds options to display useful overlay buttons in the video player. | 18.29.38 ~ 19.44.39 |
|
||||
| `Player components` | Adds options to hide or change components related to the video player. | 18.29.38 ~ 19.44.39 |
|
||||
| `Remove background playback restrictions` | Removes restrictions on background playback, including for music and kids videos. | 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. | 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. | 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. | 18.29.38 ~ 19.44.39 |
|
||||
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 18.29.38 ~ 19.44.39 |
|
||||
| `Seekbar components` | Adds options to hide or change components related to the seekbar. | 18.29.38 ~ 19.44.39 |
|
||||
| `Settings for YouTube` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 18.29.38 ~ 19.44.39 |
|
||||
| `Shorts components` | Adds options to hide or change components related to YouTube Shorts. | 18.29.38 ~ 19.44.39 |
|
||||
| `Snack bar components` | Adds options to hide or change components related to the snack bar. | 18.29.38 ~ 19.44.39 |
|
||||
| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content. | 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. | 18.29.38 ~ 19.44.39 |
|
||||
| `Spoof streaming data` | Adds options to spoof the streaming data to allow playback. | 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. | 18.29.38 ~ 19.44.39 |
|
||||
| `Theme` | Changes the app's themes to the values specified in patch options. | 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. | 18.29.38 ~ 19.44.39 |
|
||||
| `Translations for YouTube` | Add translations or remove string resources. | 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. | 18.29.38 ~ 19.44.39 |
|
||||
| `Visual preferences icons for YouTube` | Adds icons to specific preferences in the settings. | 18.29.38 ~ 19.44.39 |
|
||||
| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 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. | 19.05.36 ~ 19.47.53 |
|
||||
| `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. | 19.05.36 ~ 19.47.53 |
|
||||
| `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. | 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. | 19.05.36 ~ 19.47.53 |
|
||||
| `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. | 19.05.36 ~ 19.47.53 |
|
||||
| `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. | 19.05.36 ~ 19.47.53 |
|
||||
| `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. | 19.05.36 ~ 19.47.53 |
|
||||
| `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. | 19.05.36 ~ 19.47.53 |
|
||||
| `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. | 19.05.36 ~ 19.47.53 |
|
||||
| `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. | 19.05.36 ~ 19.47.53 |
|
||||
| `Disable layout updates` | Adds an option to disable layout updates by server. | 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. | 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. | 19.05.36 ~ 19.47.53 |
|
||||
| `Disable splash animation` | Adds an option to disable the splash animation on app startup. | 19.05.36 ~ 19.47.53 |
|
||||
| `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 debug logging` | Adds an option to enable debug logging. | 19.05.36 ~ 19.47.53 |
|
||||
| `Enable gradient loading screen` | Adds an option to enable the gradient loading screen. | 19.05.36 ~ 19.47.53 |
|
||||
| `Force hide player buttons background` | Removes, at compile time, the dark background surrounding the video player controls. | 19.05.36 ~ 19.47.53 |
|
||||
| `Fullscreen components` | Adds options to hide or change components related to fullscreen. | 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. | 19.05.36 ~ 19.47.53 |
|
||||
| `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 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 action buttons` | Adds options to hide action buttons under videos. | 19.05.36 ~ 19.47.53 |
|
||||
| `Hide ads` | Adds options to hide ads. | 19.05.36 ~ 19.47.53 |
|
||||
| `Hide comments components` | Adds options to hide components related to comments. | 19.05.36 ~ 19.47.53 |
|
||||
| `Hide feed components` | Adds options to hide components related to feeds. | 19.05.36 ~ 19.47.53 |
|
||||
| `Hide feed flyout menu` | Adds the ability to hide feed flyout menu components using a custom filter. | 19.05.36 ~ 19.47.53 |
|
||||
| `Hide layout components` | Adds options to hide general layout components. | 19.05.36 ~ 19.47.53 |
|
||||
| `Hide player buttons` | Adds options to hide buttons in the video player. | 19.05.36 ~ 19.47.53 |
|
||||
| `Hide player flyout menu` | Adds options to hide player flyout menu components. | 19.05.36 ~ 19.47.53 |
|
||||
| `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 YouTube Music actions` | Adds support for opening music in RVX Music using the in-app YouTube Music button. | 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. | 19.05.36 ~ 19.47.53 |
|
||||
| `MaterialYou` | Applies the MaterialYou theme for Android 12+ devices. | 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. | 19.05.36 ~ 19.47.53 |
|
||||
| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 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. | 19.05.36 ~ 19.47.53 |
|
||||
| `Overlay buttons` | Adds options to display useful overlay buttons in the video player. | 19.05.36 ~ 19.47.53 |
|
||||
| `Player components` | Adds options to hide or change components related to the video player. | 19.05.36 ~ 19.47.53 |
|
||||
| `Remove background playback restrictions` | Removes restrictions on background playback, including for music and kids videos. | 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. | 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. | 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. | 19.05.36 ~ 19.47.53 |
|
||||
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 19.05.36 ~ 19.47.53 |
|
||||
| `Seekbar components` | Adds options to hide or change components related to the seekbar. | 19.05.36 ~ 19.47.53 |
|
||||
| `Settings for YouTube` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 19.05.36 ~ 19.47.53 |
|
||||
| `Shorts components` | Adds options to hide or change components related to YouTube Shorts. | 19.05.36 ~ 19.47.53 |
|
||||
| `Snack bar components` | Adds options to hide or change components related to the snack bar. | 19.05.36 ~ 19.47.53 |
|
||||
| `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 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 |
|
||||
| `Spoof streaming data` | Adds options to spoof the streaming data to allow playback. | 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. | 19.05.36 ~ 19.47.53 |
|
||||
| `Theme` | Changes the app's themes to the values specified in patch options. | 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. | 19.05.36 ~ 19.47.53 |
|
||||
| `Translations for YouTube` | Add translations or remove string resources. | 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. | 19.05.36 ~ 19.47.53 |
|
||||
| `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>
|
||||
|
||||
### [📦 `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 |
|
||||
|:--------:|:--------------:|:-----------------:|
|
||||
| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 8.10.51 |
|
||||
| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 8.10.51 |
|
||||
| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 8.10.51 |
|
||||
| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 6.20.51 ~ 8.10.51 |
|
||||
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 8.10.51 |
|
||||
| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 8.10.51 |
|
||||
| `Custom branding name for YouTube Music` | Changes the YouTube Music app name to the name specified in patch options. | 6.20.51 ~ 8.10.51 |
|
||||
| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 8.10.51 |
|
||||
| `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 6.20.51 ~ 8.10.51 |
|
||||
| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.10.51 |
|
||||
| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 8.10.51 |
|
||||
| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 6.20.51 ~ 8.10.51 |
|
||||
| `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 8.10.51 |
|
||||
| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 8.10.51 |
|
||||
| `Disable music video in album` | Adds option to redirect music videos from albums for non-premium users. | 6.20.51 ~ 8.10.51 |
|
||||
| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 6.20.51 ~ 8.10.51 |
|
||||
| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 8.10.51 |
|
||||
| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 8.10.51 |
|
||||
| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 8.10.51 |
|
||||
| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 6.20.51 ~ 8.10.51 |
|
||||
| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.10.51 |
|
||||
| `Hide action bar components` | Adds options to hide action bar components and replace the offline download button with an external download button. | 6.20.51 ~ 8.10.51 |
|
||||
| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.10.51 |
|
||||
| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 8.10.51 |
|
||||
| `Hide overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 8.10.51 |
|
||||
| `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 8.10.51 |
|
||||
| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 6.20.51 ~ 8.10.51 |
|
||||
| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.10.51 |
|
||||
| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 8.10.51 |
|
||||
| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 6.20.51 ~ 8.10.51 |
|
||||
| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 6.20.51 ~ 8.10.51 |
|
||||
| `Return YouTube Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 8.10.51 |
|
||||
| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 8.10.51 |
|
||||
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 6.20.51 ~ 8.10.51 |
|
||||
| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 6.20.51 ~ 8.10.51 |
|
||||
| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 8.10.51 |
|
||||
| `Spoof app version` | Adds options to spoof the YouTube Music client version. This can be used to restore old UI elements and features. | 6.51.53 ~ 7.16.53 |
|
||||
| `Spoof client` | Adds options to spoof the client to allow playback. | 6.20.51 ~ 8.10.51 |
|
||||
| `Spoof player parameter` | Adds options to spoof player parameter to allow playback. | 6.20.51 ~ 8.10.51 |
|
||||
| `Translations for YouTube Music` | Add translations or remove string resources. | 6.20.51 ~ 8.10.51 |
|
||||
| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 6.20.51 ~ 8.10.51 |
|
||||
| `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 8.10.51 |
|
||||
| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 6.20.51 ~ 8.10.51 |
|
||||
| `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.12.53 |
|
||||
| `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.12.53 |
|
||||
| `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.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.12.53 |
|
||||
| `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.12.53 |
|
||||
| `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.12.53 |
|
||||
| `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.12.53 |
|
||||
| `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.12.53 |
|
||||
| `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.12.53 |
|
||||
| `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.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.12.53 |
|
||||
| `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.12.53 |
|
||||
| `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.12.53 |
|
||||
| `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.12.53 |
|
||||
| `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.12.53 |
|
||||
| `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.12.53 |
|
||||
| `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.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.12.53 |
|
||||
| `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.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.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 ~ 8.10.52 |
|
||||
| `Spoof player parameter` | Adds options to spoof player parameter to allow playback. | 6.20.51 ~ 8.12.53 |
|
||||
| `Translations for YouTube Music` | Add translations or remove string resources. | 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.12.53 |
|
||||
| `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 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.12.53 |
|
||||
</details>
|
||||
|
||||
### [📦 `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 |
|
||||
|:--------:|:--------------:|:-----------------:|
|
||||
| `Change package name` | Changes the package name for Reddit 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.05.1 |
|
||||
| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 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.05.1 |
|
||||
| `Hide ads` | Adds options to hide ads. | 2024.17.0 ~ 2025.05.1 |
|
||||
| `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | 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.05.1 |
|
||||
| `Open links directly` | Adds an option to skip over redirection URLs in external links. | 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.05.1 |
|
||||
| `Premium icon` | Unlocks premium app icons. | 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.05.1 |
|
||||
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 2024.17.0 ~ 2025.05.1 |
|
||||
| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 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.1 |
|
||||
| `Custom branding name for Reddit` | Changes the Reddit app name to the name specified in patch options. | 2024.17.0 ~ 2025.12.1 |
|
||||
| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2024.17.0 ~ 2025.12.1 |
|
||||
| `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2024.17.0 ~ 2025.12.1 |
|
||||
| `Hide ads` | Adds options to hide ads. | 2024.17.0 ~ 2025.12.1 |
|
||||
| `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | 2024.17.0 ~ 2025.12.1 |
|
||||
| `Hide recommended communities shelf` | Adds an option to hide the recommended communities shelves in subreddits. | 2024.17.0 ~ 2025.12.1 |
|
||||
| `Open links directly` | Adds an option to skip over redirection URLs in external links. | 2024.17.0 ~ 2025.12.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.1 |
|
||||
| `Premium icon` | Unlocks premium app icons. | 2024.17.0 ~ 2025.12.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.1 |
|
||||
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 2024.17.0 ~ 2025.12.1 |
|
||||
| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2024.17.0 ~ 2025.12.1 |
|
||||
</details>
|
||||
|
||||
|
||||
@ -165,13 +165,11 @@ Example:
|
||||
"use":true,
|
||||
"compatiblePackages": {
|
||||
"com.google.android.youtube": [
|
||||
"18.29.38",
|
||||
"18.33.40",
|
||||
"18.38.44",
|
||||
"18.48.39",
|
||||
"19.05.36",
|
||||
"19.16.39",
|
||||
"19.44.39"
|
||||
"19.43.41",
|
||||
"19.44.39",
|
||||
"19.47.53"
|
||||
]
|
||||
},
|
||||
"options": []
|
||||
@ -189,7 +187,7 @@ Example:
|
||||
"7.16.53",
|
||||
"7.25.53",
|
||||
"8.05.51",
|
||||
"8.10.51"
|
||||
"8.12.53"
|
||||
]
|
||||
},
|
||||
"options": []
|
||||
@ -201,7 +199,8 @@ Example:
|
||||
"compatiblePackages": {
|
||||
"com.reddit.frontpage": [
|
||||
"2024.17.0",
|
||||
"2025.05.1"
|
||||
"2025.05.1",
|
||||
"2025.12.1"
|
||||
]
|
||||
},
|
||||
"options": []
|
||||
|
@ -26,6 +26,7 @@ android {
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
compileOnly(libs.preference)
|
||||
implementation(libs.collections4)
|
||||
implementation(libs.lang3)
|
||||
|
||||
compileOnly(project(":extensions:shared:stub"))
|
||||
|
@ -108,13 +108,13 @@ public class FlyoutPatch {
|
||||
if (REPLACE_FLYOUT_MENU_DISMISS_QUEUE.get() &&
|
||||
textView.getParent() instanceof ViewGroup clickAbleArea) {
|
||||
runOnMainThreadDelayed(() -> {
|
||||
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()));
|
||||
clickAbleArea.setOnClickListener(view -> {
|
||||
clickView(touchOutSideViewRef.get());
|
||||
VideoUtils.openInYouTube();
|
||||
});
|
||||
}, 0L
|
||||
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()));
|
||||
clickAbleArea.setOnClickListener(view -> {
|
||||
clickView(touchOutSideViewRef.get());
|
||||
VideoUtils.openInYouTube();
|
||||
});
|
||||
}, 0L
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -126,14 +126,14 @@ public class FlyoutPatch {
|
||||
textView.getParent() instanceof ViewGroup clickAbleArea
|
||||
) {
|
||||
runOnMainThreadDelayed(() -> {
|
||||
textView.setText(str("playback_rate_title"));
|
||||
imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
|
||||
imageView.setColorFilter(cf);
|
||||
clickAbleArea.setOnClickListener(view -> {
|
||||
clickView(touchOutSideViewRef.get());
|
||||
VideoUtils.showPlaybackSpeedFlyoutMenu();
|
||||
});
|
||||
}, 0L
|
||||
textView.setText(str("playback_rate_title"));
|
||||
imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
|
||||
imageView.setColorFilter(cf);
|
||||
clickAbleArea.setOnClickListener(view -> {
|
||||
clickView(touchOutSideViewRef.get());
|
||||
VideoUtils.showPlaybackSpeedFlyoutMenu();
|
||||
});
|
||||
}, 0L
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -9,13 +9,11 @@ import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||
|
||||
/**
|
||||
* @noinspection ALL
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
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() {
|
||||
return Settings.HIDE_SOUND_SEARCH_BUTTON.get();
|
||||
}
|
||||
@ -123,7 +128,7 @@ public class GeneralPatch {
|
||||
* <p>
|
||||
* 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.
|
||||
* https://stackoverflow.com/a/4604145
|
||||
* <a href="https://stackoverflow.com/a/4604145">Reference</a>
|
||||
* <p>
|
||||
* That's why {@link AlertDialog#show()} is absolutely necessary.
|
||||
* Instead, use two tricks to hide Alertdialog.
|
||||
|
@ -7,15 +7,12 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import app.revanced.extension.music.patches.misc.requests.PlaylistRequest;
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
import app.revanced.extension.music.shared.VideoInformation;
|
||||
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.Utils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AlbumMusicVideoPatch {
|
||||
@ -40,7 +37,7 @@ public class AlbumMusicVideoPatch {
|
||||
|
||||
private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK";
|
||||
|
||||
private static final AtomicBoolean isVideoLaunched = new AtomicBoolean(false);
|
||||
private static volatile boolean isVideoLaunched = false;
|
||||
|
||||
@NonNull
|
||||
private static volatile String playerResponseVideoId = "";
|
||||
@ -100,14 +97,6 @@ public class AlbumMusicVideoPatch {
|
||||
if (request == null) {
|
||||
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();
|
||||
if (songId.isEmpty()) {
|
||||
Logger.printDebug(() -> "Official song not found, videoId: " + videoId);
|
||||
@ -149,17 +138,16 @@ public class AlbumMusicVideoPatch {
|
||||
|
||||
private static void openMusic(@NonNull String songId) {
|
||||
try {
|
||||
isVideoLaunched.compareAndSet(false, true);
|
||||
|
||||
// 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
|
||||
VideoUtils.runOnMainThreadDelayed(() -> {
|
||||
isVideoLaunched = true;
|
||||
playerResponseVideoId = songId;
|
||||
currentVideoId = songId;
|
||||
VideoUtils.openInYouTubeMusic(songId);
|
||||
}, 1000);
|
||||
VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched = false, 3000);
|
||||
}, 1500);
|
||||
|
||||
VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 2500);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "openMusic failure", ex);
|
||||
}
|
||||
@ -191,7 +179,7 @@ public class AlbumMusicVideoPatch {
|
||||
* Injection point.
|
||||
*/
|
||||
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 java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
|
@ -2,8 +2,10 @@ package app.revanced.extension.music.patches.misc.requests
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.GuardedBy
|
||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient
|
||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
|
||||
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||
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.settings.AppLanguage
|
||||
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" }
|
||||
|
||||
try {
|
||||
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
|
||||
PlayerRoutes.GET_PLAYLIST_PAGE,
|
||||
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||
GET_PLAYLIST_PAGE,
|
||||
clientType
|
||||
)
|
||||
|
||||
/**
|
||||
* For some reason, the tracks in Top Songs have the playlistId of the album:
|
||||
* [ReVanced_Extended#2835](https://github.com/inotia00/ReVanced_Extended/issues/2835)
|
||||
@ -152,7 +155,7 @@ class PlaylistRequest private constructor(
|
||||
* So we can work around this by setting the language to English when sending the request.
|
||||
*/
|
||||
val requestBody =
|
||||
PlayerRoutes.createApplicationRequestBody(
|
||||
createApplicationRequestBody(
|
||||
clientType = clientType,
|
||||
videoId = videoId,
|
||||
playlistId = playlistId,
|
||||
|
@ -3,6 +3,7 @@ package app.revanced.extension.music.settings;
|
||||
import static java.lang.Boolean.FALSE;
|
||||
import static java.lang.Boolean.TRUE;
|
||||
import static app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
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_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_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_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);
|
||||
@ -240,7 +242,10 @@ public class Settings extends BaseSettings {
|
||||
// region Migration
|
||||
|
||||
// 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");
|
||||
SPOOF_APP_VERSION_TARGET.resetToDefault();
|
||||
}
|
||||
|
@ -2,7 +2,10 @@ package app.revanced.extension.reddit.patches;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@ -34,6 +37,35 @@ public class RemoveSubRedditDialogPatch {
|
||||
return Settings.REMOVE_NSFW_DIALOG.get() || hasBeenVisited;
|
||||
}
|
||||
|
||||
public static void dismissNSFWDialog(Object customDialog) {
|
||||
if (Settings.REMOVE_NSFW_DIALOG.get() &&
|
||||
customDialog instanceof Dialog dialog) {
|
||||
Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
WindowManager.LayoutParams params = window.getAttributes();
|
||||
params.height = 0;
|
||||
params.width = 0;
|
||||
|
||||
// Change the size of dialog to 0.
|
||||
window.setAttributes(params);
|
||||
|
||||
// Disable dialog's background dim.
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
|
||||
|
||||
// Hide DecorView.
|
||||
View decorView = window.getDecorView();
|
||||
decorView.setVisibility(View.GONE);
|
||||
|
||||
// Dismiss dialog.
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean removeNSFWDialog() {
|
||||
return Settings.REMOVE_NSFW_DIALOG.get();
|
||||
}
|
||||
|
||||
public static boolean spoofLoggedInStatus(boolean isLoggedIn) {
|
||||
return !Settings.REMOVE_NOTIFICATION_DIALOG.get() && isLoggedIn;
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import app.revanced.extension.reddit.settings.Settings;
|
||||
@SuppressWarnings("unused")
|
||||
public class ScreenshotPopupPatch {
|
||||
|
||||
public static boolean disableScreenshotPopup() {
|
||||
return Settings.DISABLE_SCREENSHOT_POPUP.get();
|
||||
public static Boolean disableScreenshotPopup(Boolean original) {
|
||||
return Settings.DISABLE_SCREENSHOT_POPUP.get() ? Boolean.FALSE : original;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
// 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_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);
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app.revanced.extension.shared.patches.client
|
||||
package app.revanced.extension.shared.innertube.client
|
||||
|
||||
import android.os.Build
|
||||
import app.revanced.extension.shared.patches.PatchStatus
|
||||
import app.revanced.extension.shared.settings.BaseSettings
|
||||
import app.revanced.extension.shared.utils.PackageUtils
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
@ -212,8 +213,15 @@ object YouTubeAppClient {
|
||||
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> {
|
||||
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)) {
|
||||
val clientToUse: Array<ClientType?> = arrayOfNulls(availableClientTypes.size)
|
||||
@ -230,7 +238,7 @@ object YouTubeAppClient {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Suppress("DEPRECATION", "unused")
|
||||
enum class ClientType(
|
||||
/**
|
||||
* [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.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@ -363,7 +367,7 @@ object YouTubeAppClient {
|
||||
else
|
||||
"iOS TV"
|
||||
),
|
||||
IOS(
|
||||
IOS_DEPRECATED(
|
||||
id = 5,
|
||||
deviceMake = DEVICE_MAKE_IOS,
|
||||
deviceModel = DEVICE_MODEL_IOS,
|
||||
@ -372,7 +376,6 @@ object YouTubeAppClient {
|
||||
userAgent = USER_AGENT_IOS,
|
||||
clientVersion = CLIENT_VERSION_IOS,
|
||||
supportsCookies = false,
|
||||
requirePoToken = true,
|
||||
clientName = "IOS",
|
||||
friendlyName = if (forceAVC())
|
||||
"iOS Force AVC"
|
||||
@ -381,12 +384,20 @@ object YouTubeAppClient {
|
||||
);
|
||||
|
||||
companion object {
|
||||
val CLIENT_ORDER_TO_USE_YOUTUBE: Array<ClientType> = arrayOf(
|
||||
val CLIENT_ORDER_TO_USE: Array<ClientType> = arrayOf(
|
||||
ANDROID_VR_NO_AUTH,
|
||||
ANDROID_UNPLUGGED,
|
||||
ANDROID_CREATOR,
|
||||
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,
|
||||
)
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
package app.revanced.extension.shared.patches.client;
|
||||
package app.revanced.extension.shared.innertube.client;
|
||||
|
||||
import android.os.Build;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class MusicAppClient {
|
||||
public class YouTubeMusicAppClient {
|
||||
|
||||
// Response to the '/next' request is 'Please update to continue using the app':
|
||||
// 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 OS_NAME_IOS_MUSIC = "iOS";
|
||||
|
||||
private MusicAppClient() {
|
||||
private YouTubeMusicAppClient() {
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
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 =
|
||||
"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.
|
||||
*/
|
||||
@JvmField
|
||||
val clientVersion: String
|
||||
val clientVersion: String,
|
||||
) {
|
||||
MWEB(
|
||||
id = 2,
|
||||
clientVersion = "2.20241202.07.00"
|
||||
clientVersion = "2.20241202.07.00",
|
||||
),
|
||||
WEB_REMIX(
|
||||
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
|
||||
*
|
||||
* @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) {
|
||||
if (!HIDE_FULLSCREEN_ADS) {
|
||||
|
@ -11,4 +11,8 @@ public class PatchStatus {
|
||||
// Replace this with true If the Spoof streaming data patch succeeds in YouTube.
|
||||
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.TRUE;
|
||||
|
||||
import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -2,8 +2,8 @@ package app.revanced.extension.shared.patches;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class WatchHistoryPatch {
|
||||
|
@ -1,11 +1,11 @@
|
||||
package app.revanced.extension.shared.patches.spoof;
|
||||
|
||||
import app.revanced.extension.shared.patches.client.MusicAppClient.ClientType;
|
||||
import app.revanced.extension.music.settings.Settings;
|
||||
import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient.ClientType;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
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.
|
||||
|
@ -10,21 +10,21 @@ import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
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.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.ResourceUtils;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
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 =
|
||||
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.
|
||||
@ -69,17 +69,27 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||
* Skip response encryption in OnesiePlayerRequest.
|
||||
*/
|
||||
public static boolean skipResponseEncryption(boolean original) {
|
||||
if (SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) {
|
||||
return false;
|
||||
if (!SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) {
|
||||
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.
|
||||
*/
|
||||
public static void fetchStreams(String url, Map<String, String> requestHeaders) {
|
||||
public static void fetchStreams(String url, Map<String, String> requestHeader) {
|
||||
if (SPOOF_STREAMING_DATA) {
|
||||
String id = Utils.getVideoIdFromRequest(url);
|
||||
if (id == null) {
|
||||
@ -89,7 +99,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||
return;
|
||||
}
|
||||
|
||||
StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN);
|
||||
StreamingDataRequest.fetchRequest(id, requestHeader);
|
||||
}
|
||||
}
|
||||
|
||||
@ -210,6 +220,18 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
|
||||
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 {
|
||||
@Override
|
||||
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
|
||||
|
||||
import androidx.annotation.GuardedBy
|
||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient
|
||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA
|
||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody
|
||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute
|
||||
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.getInnerTubeResponseConnectionFromRoute
|
||||
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_STREAMING_DATA
|
||||
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 java.io.BufferedInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
@ -32,21 +32,19 @@ import java.util.concurrent.TimeoutException
|
||||
* did use its own client streams.
|
||||
*/
|
||||
class StreamingDataRequest private constructor(
|
||||
videoId: String, playerHeaders: Map<String, String>,
|
||||
visitorId: String, botGuardPoToken: String
|
||||
videoId: String,
|
||||
requestHeader: Map<String, String>,
|
||||
) {
|
||||
private val videoId: String
|
||||
private val future: Future<ByteBuffer?>
|
||||
|
||||
init {
|
||||
Objects.requireNonNull(playerHeaders)
|
||||
Objects.requireNonNull(requestHeader)
|
||||
this.videoId = videoId
|
||||
this.future = Utils.submitOnBackgroundThread {
|
||||
fetch(
|
||||
videoId,
|
||||
playerHeaders,
|
||||
visitorId,
|
||||
botGuardPoToken
|
||||
requestHeader,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -86,33 +84,16 @@ class StreamingDataRequest private constructor(
|
||||
|
||||
companion object {
|
||||
private const val AUTHORIZATION_HEADER = "Authorization"
|
||||
private const val VISITOR_ID_HEADER = "X-Goog-Visitor-Id"
|
||||
private val REQUEST_HEADER_KEYS = arrayOf(
|
||||
AUTHORIZATION_HEADER, // Available only to logged-in users.
|
||||
"X-GOOG-API-FORMAT-VERSION",
|
||||
VISITOR_ID_HEADER
|
||||
)
|
||||
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
|
||||
|
||||
private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType =
|
||||
BaseSettings.SPOOF_STREAMING_DATA_TYPE.get()
|
||||
|
||||
private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> =
|
||||
YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE)
|
||||
|
||||
private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean =
|
||||
SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH
|
||||
|
||||
private var lastSpoofedClientType: YouTubeAppClient.ClientType? = 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
|
||||
private var lastSpoofedClientFriendlyName: String? = null
|
||||
|
||||
@GuardedBy("itself")
|
||||
val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap(
|
||||
@ -126,22 +107,24 @@ class StreamingDataRequest private constructor(
|
||||
|
||||
@JvmStatic
|
||||
val lastSpoofedClientName: String
|
||||
get() = lastSpoofedClientType
|
||||
?.friendlyName
|
||||
?: "Unknown"
|
||||
get() {
|
||||
return if (lastSpoofedClientFriendlyName != null) {
|
||||
lastSpoofedClientFriendlyName!!
|
||||
} else {
|
||||
"Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun fetchRequest(
|
||||
videoId: String, fetchHeaders: Map<String, String>,
|
||||
visitorId: String, botGuardPoToken: String
|
||||
videoId: String,
|
||||
fetchHeaders: Map<String, String>,
|
||||
) {
|
||||
// Always fetch, even if there is an existing request for the same video.
|
||||
cache[videoId] =
|
||||
StreamingDataRequest(
|
||||
videoId,
|
||||
fetchHeaders,
|
||||
visitorId,
|
||||
botGuardPoToken
|
||||
fetchHeaders
|
||||
)
|
||||
}
|
||||
|
||||
@ -150,71 +133,40 @@ class StreamingDataRequest private constructor(
|
||||
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)
|
||||
}
|
||||
|
||||
private fun send(
|
||||
clientType: YouTubeAppClient.ClientType,
|
||||
videoId: String,
|
||||
playerHeaders: Map<String, String>,
|
||||
visitorId: String,
|
||||
botGuardPoToken: String
|
||||
requestHeader: Map<String, String>,
|
||||
): HttpURLConnection? {
|
||||
Objects.requireNonNull(clientType)
|
||||
Objects.requireNonNull(videoId)
|
||||
Objects.requireNonNull(playerHeaders)
|
||||
Objects.requireNonNull(requestHeader)
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" }
|
||||
|
||||
try {
|
||||
val connection =
|
||||
getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType)
|
||||
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
|
||||
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
|
||||
|
||||
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,
|
||||
getInnerTubeResponseConnectionFromRoute(
|
||||
GET_STREAMING_DATA,
|
||||
clientType,
|
||||
requestHeader
|
||||
)
|
||||
Logger.printDebug { "Set poToken (botGuardPoToken):\n$botGuardPoToken" }
|
||||
} else {
|
||||
requestBody =
|
||||
createApplicationRequestBody(
|
||||
clientType = clientType,
|
||||
videoId = videoId,
|
||||
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
|
||||
)
|
||||
}
|
||||
|
||||
val requestBody = createApplicationRequestBody(
|
||||
clientType = clientType,
|
||||
videoId = videoId,
|
||||
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
|
||||
)
|
||||
|
||||
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||
connection.outputStream.write(requestBody)
|
||||
@ -243,15 +195,15 @@ class StreamingDataRequest private constructor(
|
||||
}
|
||||
|
||||
private fun fetch(
|
||||
videoId: String, playerHeaders: Map<String, String>,
|
||||
visitorId: String, botGuardPoToken: String
|
||||
videoId: String,
|
||||
requestHeader: Map<String, String>,
|
||||
): ByteBuffer? {
|
||||
lastSpoofedClientType = null
|
||||
lastSpoofedClientFriendlyName = null
|
||||
|
||||
// Retry with different client if empty response body is received.
|
||||
for (clientType in CLIENT_ORDER_TO_USE) {
|
||||
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" }
|
||||
continue
|
||||
@ -259,9 +211,7 @@ class StreamingDataRequest private constructor(
|
||||
send(
|
||||
clientType,
|
||||
videoId,
|
||||
playerHeaders,
|
||||
visitorId,
|
||||
botGuardPoToken
|
||||
requestHeader,
|
||||
)?.let { connection ->
|
||||
try {
|
||||
// gzip encoding doesn't response with content length (-1),
|
||||
@ -271,14 +221,14 @@ class StreamingDataRequest private constructor(
|
||||
} else {
|
||||
BufferedInputStream(connection.inputStream).use { inputStream ->
|
||||
ByteArrayOutputStream().use { stream ->
|
||||
val buffer = ByteArray(2048)
|
||||
val buffer = ByteArray(4096)
|
||||
var bytesRead: Int
|
||||
while ((inputStream.read(buffer)
|
||||
.also { bytesRead = it }) >= 0
|
||||
) {
|
||||
stream.write(buffer, 0, bytesRead)
|
||||
}
|
||||
lastSpoofedClientType = clientType
|
||||
lastSpoofedClientFriendlyName = clientType.friendlyName
|
||||
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
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,10 @@ package app.revanced.extension.shared.settings;
|
||||
import static java.lang.Boolean.FALSE;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
@ -31,7 +31,7 @@ public class BaseSettings {
|
||||
* 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 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.
|
||||
@ -43,11 +43,9 @@ public class BaseSettings {
|
||||
"revanced_spoof_streaming_data_ios_force_avc_user_dialog_message");
|
||||
public static final BooleanSetting SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION = new BooleanSetting("revanced_spoof_streaming_data_skip_response_encryption", TRUE, true);
|
||||
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
|
||||
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.
|
||||
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 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);
|
||||
public static final EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_VR, true);
|
||||
|
||||
/**
|
||||
* These settings are used by YouTube and YouTube Music.
|
||||
|
@ -22,16 +22,16 @@ import android.widget.ListView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.StringRef;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
/**
|
||||
* Indicates that if a preference changes,
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Nullable
|
||||
@ -52,7 +56,14 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||
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) {
|
||||
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'.
|
||||
// Updating here can can cause a recursive call back into this same method.
|
||||
updatePreference(pref, setting, true, settingImportInProgress);
|
||||
// Update any other preference availability that may now be different.
|
||||
updateUIAvailability();
|
||||
updatingPreference = false;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex);
|
||||
}
|
||||
@ -103,36 +117,39 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
final var context = getActivity();
|
||||
showingUserDialogMessage = true;
|
||||
assert setting.userDialogMessage != null;
|
||||
new AlertDialog.Builder(context)
|
||||
.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);
|
||||
final StringRef userDialogMessage = setting.userDialogMessage;
|
||||
if (context != null && userDialogMessage != null) {
|
||||
showingUserDialogMessage = true;
|
||||
|
||||
// Update availability of other preferences that may be changed.
|
||||
updateUIAvailability();
|
||||
new AlertDialog.Builder(context)
|
||||
.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) {
|
||||
showRestartDialog(context);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||
// Restore whatever the setting was before the change.
|
||||
updatePreference(pref, setting, true, true);
|
||||
})
|
||||
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
// Update availability of other preferences that may be changed.
|
||||
updateUIAvailability();
|
||||
|
||||
if (setting.rebootApp) {
|
||||
showRestartDialog(context);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||
// Restore whatever the setting was before the change.
|
||||
updatePreference(pref, setting, true, true);
|
||||
})
|
||||
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all Preferences values and their availability using the current values in {@link Setting}.
|
||||
*/
|
||||
protected void updateUIToSettingValues() {
|
||||
updatePreferenceScreen(getPreferenceScreen(), true,true);
|
||||
updatePreferenceScreen(getPreferenceScreen(), true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -146,14 +163,16 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
* @return If the preference is currently set to the default value of the Setting.
|
||||
*/
|
||||
protected boolean prefIsSetToDefault(Preference pref, Setting<?> setting) {
|
||||
Object defaultValue = setting.defaultValue;
|
||||
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) {
|
||||
return editPreference.getText().equals(setting.defaultValue.toString());
|
||||
return editPreference.getText().equals(defaultValueString);
|
||||
}
|
||||
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 "
|
||||
@ -227,7 +246,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
/**
|
||||
* Updates a UI Preference with the {@link Setting} that backs it.
|
||||
*
|
||||
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference
|
||||
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference
|
||||
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
|
||||
* If false, then apply {@link Setting} <- Preference.
|
||||
*/
|
||||
@ -258,18 +277,19 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
listPreference.setSummary(objectStringValue);
|
||||
}
|
||||
|
||||
public static void showRestartDialog(@NonNull final Context context) {
|
||||
public static void showRestartDialog(@NonNull Context context) {
|
||||
if (restartDialogMessage == null) {
|
||||
restartDialogMessage = str("revanced_extended_restart_message");
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
|
@ -10,6 +10,8 @@ import android.util.AttributeSet;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
@ -19,6 +21,12 @@ import app.revanced.extension.shared.utils.Utils;
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ResettableEditTextPreference extends EditTextPreference {
|
||||
|
||||
/**
|
||||
* Setting to reset.
|
||||
*/
|
||||
@Nullable
|
||||
private Setting<?> setting;
|
||||
|
||||
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
@ -35,6 +43,10 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public void setSetting(@Nullable Setting<?> setting) {
|
||||
this.setting = setting;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
@ -44,7 +56,12 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
||||
if (title != null) {
|
||||
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) {
|
||||
builder.setNeutralButton(str("revanced_extended_settings_reset"), null);
|
||||
}
|
||||
@ -65,8 +82,7 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
||||
}
|
||||
button.setOnClickListener(v -> {
|
||||
try {
|
||||
Setting<?> setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey()));
|
||||
String defaultStringValue = setting.defaultValue.toString();
|
||||
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
|
||||
EditText editText = getEditText();
|
||||
editText.setText(defaultStringValue);
|
||||
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) {
|
||||
try {
|
||||
return getContext().getPackageManager().getApplicationInfo(packageName, 0).enabled;
|
||||
} catch (PackageManager.NameNotFoundException ignored) {
|
||||
ApplicationInfo applicationInfo = getApplicationInfo(packageName);
|
||||
if (applicationInfo != null) {
|
||||
return applicationInfo.enabled;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -47,6 +57,16 @@ public class PackageUtils extends 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
|
||||
private static PackageInfo getPackageInfo() {
|
||||
try {
|
||||
|
@ -60,6 +60,7 @@ public class Utils {
|
||||
private static WeakReference<Activity> activityRef = new WeakReference<>(null);
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private static volatile Context context;
|
||||
private static Locale contextLocale;
|
||||
|
||||
protected Utils() {
|
||||
} // utility class
|
||||
@ -308,34 +309,51 @@ public class Utils {
|
||||
* @return Context with locale applied.
|
||||
*/
|
||||
public static Context getLocalizedContext(Context mContext) {
|
||||
Activity mActivity = activityRef.get();
|
||||
if (mActivity == null) {
|
||||
return mContext;
|
||||
}
|
||||
if (mContext == null) {
|
||||
return null;
|
||||
try {
|
||||
Activity mActivity = activityRef.get();
|
||||
if (mActivity != null && mContext != null) {
|
||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
||||
|
||||
// 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.
|
||||
Locale applicationLocale = language == AppLanguage.DEFAULT
|
||||
? mActivity.getResources().getConfiguration().locale
|
||||
: language.getLocale();
|
||||
|
||||
// Locale of Context.
|
||||
Locale contextLocale = mContext.getResources().getConfiguration().locale;
|
||||
|
||||
// If they are identical, no need to override them.
|
||||
if (applicationLocale == contextLocale) {
|
||||
return mContext;
|
||||
public static void resetLocalizedContext() {
|
||||
try {
|
||||
if (contextLocale != null) {
|
||||
Locale.setDefault(contextLocale);
|
||||
Context mContext = getContext();
|
||||
if (mContext != null) {
|
||||
Configuration config = mContext.getResources().getConfiguration();
|
||||
config.setLocale(contextLocale);
|
||||
setContext(mContext.createConfigurationContext(config));
|
||||
}
|
||||
}
|
||||
} 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) {
|
||||
@ -353,14 +371,6 @@ public class Utils {
|
||||
// Must initially set context to check the app language.
|
||||
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) {
|
||||
@ -538,14 +548,6 @@ public class Utils {
|
||||
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) {
|
||||
if (context == null) {
|
||||
return dp;
|
||||
@ -608,10 +610,10 @@ public class Utils {
|
||||
* <br>
|
||||
* 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.
|
||||
*<br>
|
||||
* <br>
|
||||
* 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.
|
||||
*<br>
|
||||
* <br>
|
||||
* For all other situations it's better to not use this method and
|
||||
* 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.Filter;
|
||||
import app.revanced.extension.shared.patches.components.StringFilterGroup;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@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.youtube.settings.Settings
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import kotlin.Boolean
|
||||
|
||||
@Suppress("unused")
|
||||
object ChangeStartPagePatch {
|
||||
@ -44,7 +43,7 @@ object ChangeStartPagePatch {
|
||||
}
|
||||
appLaunched = true
|
||||
|
||||
Logger.printDebug{ "Changing browseId to $browseId" }
|
||||
Logger.printDebug { "Changing browseId to $browseId" }
|
||||
return browseId
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,34 @@
|
||||
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.youtube.patches.utils.PlaylistPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class DownloadActionsPatch extends VideoUtils {
|
||||
public final class DownloadActionsPatch {
|
||||
|
||||
private static final BooleanSetting overrideVideoDownloadButton =
|
||||
Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON;
|
||||
private static final boolean OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON =
|
||||
Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON.get();
|
||||
|
||||
private static final BooleanSetting overridePlaylistDownloadButton =
|
||||
Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON;
|
||||
private static final boolean OVERRIDE_VIDEO_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.
|
||||
@ -23,17 +39,21 @@ public final class DownloadActionsPatch extends VideoUtils {
|
||||
* <p>
|
||||
* 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 {
|
||||
if (!overrideVideoDownloadButton.get()) {
|
||||
return false;
|
||||
}
|
||||
if (videoId == null || videoId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
launchVideoExternalDownloader(videoId);
|
||||
if (OVERRIDE_VIDEO_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(videoId)) {
|
||||
if (OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER) {
|
||||
if (map != null && map.get(ELEMENTS_SENDER_VIEW) instanceof View view) {
|
||||
PlaylistPatch.setContext(view.getContext());
|
||||
}
|
||||
PlaylistPatch.prepareDialogBuilder(videoId);
|
||||
} else {
|
||||
launchVideoExternalDownloader(videoId);
|
||||
}
|
||||
|
||||
return true;
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "inAppVideoDownloadButtonOnClick failure", ex);
|
||||
}
|
||||
@ -49,15 +69,10 @@ public final class DownloadActionsPatch extends VideoUtils {
|
||||
*/
|
||||
public static String inAppPlaylistDownloadButtonOnClick(String playlistId) {
|
||||
try {
|
||||
if (!overridePlaylistDownloadButton.get()) {
|
||||
return playlistId;
|
||||
if (OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(playlistId)) {
|
||||
launchPlaylistExternalDownloader(playlistId);
|
||||
return "";
|
||||
}
|
||||
if (playlistId == null || playlistId.isEmpty()) {
|
||||
return playlistId;
|
||||
}
|
||||
launchPlaylistExternalDownloader(playlistId);
|
||||
|
||||
return "";
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "inAppPlaylistDownloadButtonOnClick failure", ex);
|
||||
}
|
||||
@ -73,15 +88,10 @@ public final class DownloadActionsPatch extends VideoUtils {
|
||||
*/
|
||||
public static boolean inAppPlaylistDownloadMenuOnClick(String playlistId) {
|
||||
try {
|
||||
if (!overridePlaylistDownloadButton.get()) {
|
||||
return false;
|
||||
if (OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(playlistId)) {
|
||||
launchPlaylistExternalDownloader(playlistId);
|
||||
return true;
|
||||
}
|
||||
if (playlistId == null || playlistId.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
launchPlaylistExternalDownloader(playlistId);
|
||||
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "inAppPlaylistDownloadMenuOnClick failure", ex);
|
||||
}
|
||||
@ -92,7 +102,7 @@ public final class DownloadActionsPatch extends VideoUtils {
|
||||
* Injection point.
|
||||
*/
|
||||
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.Objects;
|
||||
|
||||
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.settings.Settings;
|
||||
@ -105,6 +106,34 @@ public class GeneralPatch {
|
||||
|
||||
// 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
|
||||
|
||||
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) {
|
||||
return Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get() || original;
|
||||
}
|
||||
|
@ -43,8 +43,8 @@ public final class OpenChannelOfLiveAvatarPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor
|
||||
* @param newlyLoadedVideoId id of the current video
|
||||
* @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor
|
||||
* @param newlyLoadedVideoId id of the current video
|
||||
*/
|
||||
public static void fetchChannelId(@NonNull Map<Object, Object> playbackStartDescriptorMap, String newlyLoadedVideoId) {
|
||||
try {
|
||||
|
@ -2,8 +2,10 @@ package app.revanced.extension.youtube.patches.general.requests
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.annotation.GuardedBy
|
||||
import app.revanced.extension.shared.patches.client.YouTubeWebClient
|
||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
|
||||
import app.revanced.extension.shared.innertube.client.YouTubeWebClient
|
||||
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.utils.Logger
|
||||
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" }
|
||||
|
||||
try {
|
||||
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
|
||||
PlayerRoutes.GET_VIDEO_DETAILS,
|
||||
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||
GET_VIDEO_DETAILS,
|
||||
clientType
|
||||
)
|
||||
val requestBody =
|
||||
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
|
||||
val requestBody = createWebInnertubeBody(clientType, videoId)
|
||||
|
||||
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||
connection.outputStream.write(requestBody)
|
||||
|
@ -15,7 +15,16 @@ public class BackgroundPlaybackPatch {
|
||||
*/
|
||||
public static boolean isBackgroundPlaybackAllowed(boolean original) {
|
||||
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 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.shared.VideoInformation;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ -19,7 +21,14 @@ public class ExternalDownload extends BottomControlButton {
|
||||
bottomControlsViewGroup,
|
||||
"external_download_button",
|
||||
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
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package app.revanced.extension.youtube.patches.player;
|
||||
|
||||
import static app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton.REMIX;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.ArrayUtils;
|
||||
@ -8,8 +10,6 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
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.utils.Logger;
|
||||
import app.revanced.extension.shared.utils.Utils;
|
||||
@ -106,8 +106,8 @@ public class ActionButtonsPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param list Type list of litho components
|
||||
* @param identifier Identifier of litho components
|
||||
* @param list Type list of litho components
|
||||
* @param identifier Identifier of litho components
|
||||
*/
|
||||
public static List<Object> hideActionButtonByIndex(@Nullable List<Object> list, @Nullable String identifier) {
|
||||
try {
|
||||
|
@ -24,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
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.ResourceUtils;
|
||||
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.PlayerType;
|
||||
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.utils.VideoUtils;
|
||||
|
||||
@ -185,8 +183,8 @@ public class PlayerPatch {
|
||||
// The type of descriptionView can be either ViewGroup or TextView. (A/B tests)
|
||||
// If the type of descriptionView is TextView, longer delay is required.
|
||||
final long delayMillis = descriptionView instanceof TextView
|
||||
? 500
|
||||
: 100;
|
||||
? 750
|
||||
: 200;
|
||||
|
||||
Utils.runOnMainThreadDelayed(() -> Utils.clickView(descriptionView), delayMillis);
|
||||
}
|
||||
@ -441,7 +439,7 @@ public class PlayerPatch {
|
||||
if (isLiveChatOrPlaylistPanel) {
|
||||
return true;
|
||||
}
|
||||
return isAutoPopupPanel && ShortsPlayerState.getCurrent().isClosed();
|
||||
return isAutoPopupPanel && !RootView.isShortsActive();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -471,8 +469,8 @@ public class PlayerPatch {
|
||||
* Used in YouTube 20.05.46+.
|
||||
*/
|
||||
public static void disableAutoPlayerPopupPanels(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||
if (Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get() && newVideoStarted.compareAndSet(false, true)) {
|
||||
Utils.runOnMainThreadDelayed(() -> newVideoStarted.compareAndSet(true, false), 3000L);
|
||||
}
|
||||
@ -518,6 +516,12 @@ public class PlayerPatch {
|
||||
return SPEED_OVERLAY_VALUE;
|
||||
}
|
||||
|
||||
public static float speedOverlayRelativeValue(float original) {
|
||||
return SPEED_OVERLAY_VALUE != 2.0f
|
||||
? 0f
|
||||
: original;
|
||||
}
|
||||
|
||||
public static boolean hideChannelWatermark(boolean original) {
|
||||
return !Settings.HIDE_CHANNEL_WATERMARK.get() && original;
|
||||
}
|
||||
@ -540,6 +544,10 @@ public class PlayerPatch {
|
||||
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) {
|
||||
return !Settings.HIDE_INFO_CARDS.get() && original;
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ public class SeekbarColorPatch {
|
||||
/**
|
||||
* 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.
|
||||
|
@ -1,8 +1,10 @@
|
||||
package app.revanced.extension.youtube.patches.player.requests
|
||||
|
||||
import androidx.annotation.GuardedBy
|
||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient
|
||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
|
||||
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||
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.utils.Logger
|
||||
import app.revanced.extension.shared.utils.Utils
|
||||
@ -20,10 +22,10 @@ import java.util.concurrent.TimeoutException
|
||||
|
||||
class ActionButtonRequest private constructor(
|
||||
private val videoId: String,
|
||||
private val playerHeaders: Map<String, String>,
|
||||
private val requestHeader: Map<String, String>,
|
||||
) {
|
||||
private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread {
|
||||
fetch(videoId, playerHeaders)
|
||||
fetch(videoId, requestHeader)
|
||||
}
|
||||
|
||||
val array: Array<ActionButton>
|
||||
@ -52,14 +54,6 @@ class ActionButtonRequest private constructor(
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@GuardedBy("itself")
|
||||
@ -73,11 +67,11 @@ class ActionButtonRequest private constructor(
|
||||
})
|
||||
|
||||
@JvmStatic
|
||||
fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map<String, String>) {
|
||||
fun fetchRequestIfNeeded(videoId: String, requestHeader: Map<String, String>) {
|
||||
Objects.requireNonNull(videoId)
|
||||
synchronized(cache) {
|
||||
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)
|
||||
}
|
||||
|
||||
private val REQUEST_HEADER_KEYS = arrayOf(
|
||||
"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? {
|
||||
private fun sendRequest(videoId: String, requestHeader: Map<String, String>): JSONObject? {
|
||||
Objects.requireNonNull(videoId)
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
// '/next' request does not require PoToken.
|
||||
// '/next' endpoint does not require PoToken.
|
||||
val clientType = YouTubeAppClient.ClientType.ANDROID
|
||||
val clientTypeName = clientType.name
|
||||
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
|
||||
|
||||
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,
|
||||
// Set the [Authorization] field to property to get the correct action buttons.
|
||||
for (key in REQUEST_HEADER_KEYS) {
|
||||
var value = playerHeaders[key]
|
||||
if (value != null) {
|
||||
connection.setRequestProperty(key, value)
|
||||
}
|
||||
}
|
||||
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||
GET_VIDEO_ACTION_BUTTON,
|
||||
clientType,
|
||||
requestHeader,
|
||||
)
|
||||
|
||||
val requestBody =
|
||||
PlayerRoutes.createApplicationRequestBody(
|
||||
clientType = clientType,
|
||||
videoId = videoId
|
||||
)
|
||||
val requestBody = createApplicationRequestBody(
|
||||
clientType = clientType,
|
||||
videoId = videoId
|
||||
)
|
||||
|
||||
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||
connection.outputStream.write(requestBody)
|
||||
@ -214,8 +193,11 @@ class ActionButtonRequest private constructor(
|
||||
return emptyArray()
|
||||
}
|
||||
|
||||
private fun fetch(videoId: String, playerHeaders: Map<String, String>): Array<ActionButton> {
|
||||
val json = sendRequest(videoId, playerHeaders)
|
||||
private fun fetch(
|
||||
videoId: String,
|
||||
requestHeader: Map<String, String>
|
||||
): Array<ActionButton> {
|
||||
val json = sendRequest(videoId, requestHeader)
|
||||
if (json != null) {
|
||||
return parseResponse(json)
|
||||
}
|
||||
|
@ -1,25 +1,15 @@
|
||||
package app.revanced.extension.youtube.patches.shorts;
|
||||
|
||||
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.shared.RootView.isShortsActive;
|
||||
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
|
||||
|
||||
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.Drawable;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.graphics.drawable.StateListDrawable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
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.youtube.patches.components.ShortsCustomActionsFilter;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
import app.revanced.extension.youtube.utils.ThemeUtils;
|
||||
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||
import app.revanced.extension.youtube.utils.VideoUtils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ -66,7 +55,7 @@ public final class CustomActionsPatch {
|
||||
if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (ShortsPlayerState.getCurrent().isClosed()) {
|
||||
if (!isShortsActive()) {
|
||||
return;
|
||||
}
|
||||
if (!isMoreButton(enumString)) {
|
||||
@ -90,105 +79,28 @@ public final class CustomActionsPatch {
|
||||
}), 0);
|
||||
}
|
||||
|
||||
private static void showMoreButtonDialog(Context context) {
|
||||
ScrollView scrollView = new ScrollView(context);
|
||||
LinearLayout container = new LinearLayout(context);
|
||||
private static void showMoreButtonDialog(Context mContext) {
|
||||
ScrollView mScrollView = new ScrollView(mContext);
|
||||
LinearLayout mLinearLayout = new LinearLayout(mContext);
|
||||
mLinearLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
mLinearLayout.setPadding(0, 0, 0, 0);
|
||||
|
||||
container.setOrientation(LinearLayout.VERTICAL);
|
||||
container.setPadding(0, 0, 0, 0);
|
||||
|
||||
Map<LinearLayout, Runnable> toolbarMap = new LinkedHashMap<>(arrSize);
|
||||
Map<LinearLayout, Runnable> actionsMap = new LinkedHashMap<>(arrSize);
|
||||
|
||||
for (CustomAction customAction : CustomAction.values()) {
|
||||
if (customAction.settings.get()) {
|
||||
String title = customAction.getLabel();
|
||||
int iconId = customAction.getDrawableId();
|
||||
Runnable action = customAction.getOnClickAction();
|
||||
LinearLayout itemLayout = createItemLayout(context, title, iconId);
|
||||
toolbarMap.putIfAbsent(itemLayout, action);
|
||||
container.addView(itemLayout);
|
||||
LinearLayout itemLayout = ExtendedUtils.createItemLayout(mContext, title, iconId);
|
||||
actionsMap.putIfAbsent(itemLayout, action);
|
||||
mLinearLayout.addView(itemLayout);
|
||||
}
|
||||
}
|
||||
|
||||
scrollView.addView(container);
|
||||
mScrollView.addView(mLinearLayout);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
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;
|
||||
ExtendedUtils.showBottomSheetDialog(mContext, mScrollView, actionsMap);
|
||||
}
|
||||
|
||||
private static boolean isMoreButton(String enumString) {
|
||||
@ -206,7 +118,7 @@ public final class CustomActionsPatch {
|
||||
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (ShortsPlayerState.getCurrent().isClosed()) {
|
||||
if (!isShortsActive()) {
|
||||
return;
|
||||
}
|
||||
if (bottomSheetMenuObject == null) {
|
||||
@ -224,7 +136,7 @@ public final class CustomActionsPatch {
|
||||
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (ShortsPlayerState.getCurrent().isClosed()) {
|
||||
if (!isShortsActive()) {
|
||||
return;
|
||||
}
|
||||
for (CustomAction customAction : CustomAction.values()) {
|
||||
@ -243,6 +155,34 @@ public final class CustomActionsPatch {
|
||||
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.
|
||||
*/
|
||||
@ -252,7 +192,7 @@ public final class CustomActionsPatch {
|
||||
}
|
||||
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
||||
try {
|
||||
if (ShortsPlayerState.getCurrent().isClosed()) {
|
||||
if (!isShortsActive()) {
|
||||
return;
|
||||
}
|
||||
contextRef = new WeakReference<>(recyclerView.getContext());
|
||||
@ -267,8 +207,9 @@ public final class CustomActionsPatch {
|
||||
if (recyclerView.getChildAt(childCount - i - 1) instanceof ViewGroup parentViewGroup) {
|
||||
childCount = recyclerView.getChildCount();
|
||||
if (childCount > 3 && parentViewGroup.getChildAt(1) instanceof TextView textView) {
|
||||
String menuTitle = textView.getText().toString();
|
||||
for (CustomAction customAction : CustomAction.values()) {
|
||||
if (customAction.getLabel().equals(textView.getText().toString())) {
|
||||
if (customAction.getLabel().equals(menuTitle)) {
|
||||
View.OnClickListener onClick = customAction.getOnClickListener();
|
||||
View.OnLongClickListener onLongClick = customAction.getOnLongClickListener();
|
||||
recyclerViewRef = new WeakReference<>(recyclerView);
|
||||
@ -384,6 +325,11 @@ public final class CustomActionsPatch {
|
||||
true
|
||||
)
|
||||
),
|
||||
SPEED_DIALOG(
|
||||
Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG,
|
||||
"yt_outline_play_arrow_half_circle_black_24",
|
||||
() -> VideoUtils.showPlaybackSpeedDialog(contextRef.get())
|
||||
),
|
||||
REPEAT_STATE(
|
||||
Settings.SHORTS_CUSTOM_ACTIONS_REPEAT_STATE,
|
||||
"yt_outline_arrow_repeat_1_black_24",
|
||||
|
@ -2,6 +2,8 @@ package app.revanced.extension.youtube.patches.shorts;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Objects;
|
||||
|
||||
@ -29,10 +31,15 @@ public class ShortsRepeatStatePatch {
|
||||
|
||||
static void setYTEnumValue(Enum<?> ytBehavior) {
|
||||
for (ShortsLoopBehavior rvBehavior : values()) {
|
||||
if (ytBehavior.name().endsWith(rvBehavior.name())) {
|
||||
rvBehavior.ytEnumValue = ytBehavior;
|
||||
|
||||
Logger.printDebug(() -> rvBehavior + " set to YT enum: " + ytBehavior.name());
|
||||
String ytName = ytBehavior.name();
|
||||
if (ytName.endsWith(rvBehavior.name())) {
|
||||
if (rvBehavior.ytEnumValue != null) {
|
||||
Logger.printException(() -> "Conflicting behavior names: " + rvBehavior
|
||||
+ " ytBehavior: " + ytName);
|
||||
} else {
|
||||
rvBehavior.ytEnumValue = ytBehavior;
|
||||
Logger.printDebug(() -> rvBehavior + " set to YT enum: " + ytName);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -77,25 +84,39 @@ public class ShortsRepeatStatePatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static Enum<?> changeShortsRepeatBehavior(Enum<?> original) {
|
||||
@Nullable
|
||||
public static Enum<?> changeShortsRepeatBehavior(@Nullable Enum<?> original) {
|
||||
try {
|
||||
final ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER &&
|
||||
ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER &&
|
||||
isAppInBackgroundPiPMode()
|
||||
? Settings.CHANGE_SHORTS_BACKGROUND_REPEAT_STATE.get()
|
||||
: Settings.CHANGE_SHORTS_REPEAT_STATE.get();
|
||||
Enum<?> overrideBehavior = behavior.ytEnumValue;
|
||||
|
||||
if (behavior != ShortsLoopBehavior.UNKNOWN && behavior.ytEnumValue != null) {
|
||||
Logger.printDebug(() -> behavior.ytEnumValue == original
|
||||
? "Changing Shorts repeat behavior from: " + original.name() + " to: " + behavior.ytEnumValue
|
||||
: "Behavior setting is same as original. Using original: " + original.name()
|
||||
);
|
||||
if (behavior != ShortsLoopBehavior.UNKNOWN && overrideBehavior != null) {
|
||||
Logger.printDebug(() -> {
|
||||
String name = original == null ? "unknown (null)" : 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) {
|
||||
Logger.printException(() -> "changeShortsRepeatState failure", ex);
|
||||
Logger.printException(() -> "changeShortsRepeatBehavior failure", ex);
|
||||
}
|
||||
|
||||
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 app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
@ -59,4 +60,20 @@ public class SwipeControlsPatch {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,6 +9,11 @@ public class PatchStatus {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
public static boolean OldSeekbarThumbnailsDefaultBoolean() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean OldSplashAnimation() {
|
||||
// Replace this with true if the Restore old splash animation (Custom branding icon) succeeds
|
||||
return false;
|
||||
@ -40,23 +45,22 @@ public class PatchStatus {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String SpoofAppVersionDefaultString() {
|
||||
return "18.17.43";
|
||||
}
|
||||
|
||||
public static boolean ToolBarComponents() {
|
||||
// Replace this with true if the Toolbar components patch succeeds
|
||||
return false;
|
||||
}
|
||||
|
||||
public static long PatchedTime() {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
public static String SpoofAppVersionDefaultString() {
|
||||
return "18.17.43";
|
||||
}
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
public static String RVXMusicPackageName() {
|
||||
return "com.google.android.apps.youtube.music";
|
||||
}
|
||||
|
||||
// Modified by a patch. Do not touch.
|
||||
public static boolean OldSeekbarThumbnailsDefaultBoolean() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,20 +1,24 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
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.ShortsPlayerState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PlaybackSpeedWhilePlayingPatch {
|
||||
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
||||
|
||||
public static boolean playbackSpeedChanged(float playbackSpeed) {
|
||||
PlayerType playerType = PlayerType.getCurrent();
|
||||
if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED &&
|
||||
playerType.isMaximizedOrFullscreenOrPiP()) {
|
||||
if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED) {
|
||||
if (PlayerType.getCurrent().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;
|
||||
|
@ -0,0 +1,534 @@
|
||||
package app.revanced.extension.youtube.patches.utils;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.shared.utils.Utils.runOnMainThreadDelayed;
|
||||
import static app.revanced.extension.youtube.utils.VideoUtils.dismissPlayer;
|
||||
import static app.revanced.extension.youtube.utils.VideoUtils.launchVideoExternalDownloader;
|
||||
import static app.revanced.extension.youtube.utils.VideoUtils.openPlaylist;
|
||||
|
||||
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.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.shared.VideoInformation;
|
||||
import app.revanced.extension.youtube.utils.AuthUtils;
|
||||
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||
import kotlin.Pair;
|
||||
|
||||
// TODO: Implement sync queue and clean up code.
|
||||
@SuppressWarnings({"unused", "StaticFieldLeak"})
|
||||
public class PlaylistPatch extends AuthUtils {
|
||||
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 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);
|
||||
Logger.printDebug(() -> "Video removed by YouTube flyout menu: " + videoId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setPivotBar(PivotBar view) {
|
||||
if (QUEUE_MANAGER) {
|
||||
mContext = view.getContext();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
boolean canReload = PlayerType.getCurrent().isMaximizedOrFullscreen() &&
|
||||
lastVideoIds.get(VideoInformation.getVideoId()) != null;
|
||||
if (playlistId.isEmpty() || lastVideoIds.get(currentVideoId) == null) {
|
||||
if (canReload) {
|
||||
customActionsEntries = QueueManager.addToQueueWithReloadEntries;
|
||||
} else {
|
||||
customActionsEntries = QueueManager.addToQueueEntries;
|
||||
}
|
||||
} else {
|
||||
if (canReload) {
|
||||
customActionsEntries = QueueManager.removeFromQueueWithReloadEntries;
|
||||
} else {
|
||||
customActionsEntries = 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, boolean reload) {
|
||||
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, reload);
|
||||
}
|
||||
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 ("".equals(fetchedSetVideoId)) {
|
||||
lastVideoIds.remove(currentVideoId, setVideoId);
|
||||
EditPlaylistRequest.clearVideoId(currentVideoId);
|
||||
showToast(fetchSucceededRemove);
|
||||
if (openPlaylist) {
|
||||
openQueue(currentVideoId, openVideo, reload);
|
||||
}
|
||||
return;
|
||||
}
|
||||
showToast(fetchFailedRemove);
|
||||
} else { // Add to queue.
|
||||
if (fetchedSetVideoId != null && !fetchedSetVideoId.isEmpty()) {
|
||||
lastVideoIds.putIfAbsent(currentVideoId, fetchedSetVideoId);
|
||||
EditPlaylistRequest.clearVideoId(currentVideoId);
|
||||
showToast(fetchSucceededAdd);
|
||||
Logger.printDebug(() -> "Video successfully added, setVideoId: " + fetchedSetVideoId);
|
||||
if (openPlaylist) {
|
||||
openQueue(currentVideoId, openVideo, reload);
|
||||
}
|
||||
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, false);
|
||||
}
|
||||
|
||||
private static void openQueue(String currentVideoId, boolean openVideo, boolean reload) {
|
||||
String currentPlaylistId = playlistId;
|
||||
if (currentPlaylistId.isEmpty()) {
|
||||
handleCheckError(checkFailedQueue);
|
||||
return;
|
||||
}
|
||||
if (openVideo) {
|
||||
if (StringUtils.isEmpty(currentVideoId)) {
|
||||
handleCheckError(checkFailedVideoId);
|
||||
return;
|
||||
}
|
||||
// Open a video from a playlist
|
||||
if (reload) {
|
||||
// Since the Queue is not automatically synced, a 'reload' action has been added as a workaround.
|
||||
// The 'reload' action simply closes the video and reopens it.
|
||||
// It is important to close the video, otherwise the Queue will not be updated.
|
||||
dismissPlayer();
|
||||
openPlaylist(currentPlaylistId, VideoInformation.getVideoId(), true);
|
||||
} else {
|
||||
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, 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, 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, false)
|
||||
),
|
||||
ADD_TO_QUEUE_AND_RELOAD_VIDEO(
|
||||
"revanced_queue_manager_add_to_queue_and_reload_video",
|
||||
"yt_outline_arrow_circle_black_24",
|
||||
() -> fetchQueue(false, true, true, true)
|
||||
),
|
||||
REMOVE_FROM_QUEUE(
|
||||
"revanced_queue_manager_remove_from_queue",
|
||||
"yt_outline_trash_can_black_24",
|
||||
() -> fetchQueue(true, false, 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, false)
|
||||
),
|
||||
REMOVE_FROM_QUEUE_AND_RELOAD_VIDEO(
|
||||
"revanced_queue_manager_remove_from_queue_and_reload_video",
|
||||
"yt_outline_arrow_circle_black_24",
|
||||
() -> fetchQueue(true, true, true, true)
|
||||
),
|
||||
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[] addToQueueWithReloadEntries = {
|
||||
ADD_TO_QUEUE,
|
||||
ADD_TO_QUEUE_AND_OPEN_QUEUE,
|
||||
ADD_TO_QUEUE_AND_PLAY_VIDEO,
|
||||
ADD_TO_QUEUE_AND_RELOAD_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[] removeFromQueueWithReloadEntries = {
|
||||
REMOVE_FROM_QUEUE,
|
||||
REMOVE_FROM_QUEUE_AND_OPEN_QUEUE,
|
||||
REMOVE_FROM_QUEUE_AND_RELOAD_VIDEO,
|
||||
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,224 @@
|
||||
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.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, setVideoId != null && setVideoId.isNotEmpty())
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
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 long lastTimeResponse = 0;
|
||||
|
||||
/**
|
||||
* Replace the SW AV01 codec to VP9 codec.
|
||||
@ -22,32 +15,4 @@ public class AV1CodecPatch {
|
||||
public static String replaceCodec(String 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;
|
||||
}
|
||||
|
||||
public static String[] getListEntries() {
|
||||
public static String[] getEntries() {
|
||||
return isCustomPlaybackSpeedEnabled()
|
||||
? customSpeedEntries
|
||||
: defaultSpeedEntries;
|
||||
}
|
||||
|
||||
public static String[] getListEntryValues() {
|
||||
public static String[] getEntryValues() {
|
||||
return isCustomPlaybackSpeedEnabled()
|
||||
? customSpeedEntryValues
|
||||
: defaultSpeedEntryValues;
|
||||
}
|
||||
|
||||
public static String[] getTrimmedListEntries() {
|
||||
public static String[] getTrimmedEntries() {
|
||||
if (playbackSpeedEntries == null) {
|
||||
final String[] playbackSpeedWithAutoEntries = getListEntries();
|
||||
final String[] playbackSpeedWithAutoEntries = getEntries();
|
||||
playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length);
|
||||
}
|
||||
|
||||
return playbackSpeedEntries;
|
||||
}
|
||||
|
||||
public static String[] getTrimmedListEntryValues() {
|
||||
public static String[] getTrimmedEntryValues() {
|
||||
if (playbackSpeedEntryValues == null) {
|
||||
final String[] playbackSpeedWithAutoEntryValues = getListEntryValues();
|
||||
final String[] playbackSpeedWithAutoEntryValues = getEntryValues();
|
||||
playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length);
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,18 @@
|
||||
package app.revanced.extension.youtube.patches.video;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
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.Utils;
|
||||
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
||||
@ -17,25 +23,70 @@ import app.revanced.extension.youtube.whitelist.Whitelist;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
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 =
|
||||
Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get();
|
||||
private static final long TOAST_DELAY_MILLISECONDS = 750;
|
||||
private static long lastTimeSpeedChanged;
|
||||
private static boolean isLiveStream;
|
||||
|
||||
/**
|
||||
* The last used playback speed.
|
||||
* This value is used when the default playback speed is 'Auto'.
|
||||
*/
|
||||
private static float lastSelectedPlaybackSpeed = 1.0f;
|
||||
private static float lastSelectedShortsPlaybackSpeed = 1.0f;
|
||||
|
||||
/**
|
||||
* The last regular video id.
|
||||
*/
|
||||
private static String videoId = "";
|
||||
|
||||
@GuardedBy("itself")
|
||||
private static final Map<String, Float> ignoredPlaybackSpeedVideoIds = new LinkedHashMap<>() {
|
||||
private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 3;
|
||||
|
||||
@Override
|
||||
protected boolean removeEldestEntry(Map.Entry eldest) {
|
||||
return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* This method is used to reset the playback speed to 1.0 when a general video is started, whether it is a live stream, music, or whitelist.
|
||||
*/
|
||||
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||
isLiveStream = newlyLoadedLiveStreamValue;
|
||||
Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId);
|
||||
if (isShortsActive()) {
|
||||
return;
|
||||
}
|
||||
if (videoId.equals(newlyLoadedVideoId)) {
|
||||
return;
|
||||
}
|
||||
videoId = newlyLoadedVideoId;
|
||||
|
||||
final float defaultPlaybackSpeed = getDefaultPlaybackSpeed(newlyLoadedChannelId, newlyLoadedVideoId);
|
||||
Logger.printDebug(() -> "overridePlaybackSpeed: " + defaultPlaybackSpeed);
|
||||
boolean isMusic = isMusic(newlyLoadedVideoId);
|
||||
boolean isWhitelisted = Whitelist.isChannelWhitelistedPlaybackSpeed(newlyLoadedVideoId);
|
||||
|
||||
VideoInformation.overridePlaybackSpeed(defaultPlaybackSpeed);
|
||||
if (newlyLoadedLiveStreamValue || isMusic || isWhitelisted) {
|
||||
synchronized(ignoredPlaybackSpeedVideoIds) {
|
||||
if (!ignoredPlaybackSpeedVideoIds.containsKey(newlyLoadedVideoId)) {
|
||||
lastSelectedPlaybackSpeed = 1.0f;
|
||||
ignoredPlaybackSpeedVideoIds.put(newlyLoadedVideoId, lastSelectedPlaybackSpeed);
|
||||
|
||||
VideoInformation.setPlaybackSpeed(lastSelectedPlaybackSpeed);
|
||||
VideoInformation.overridePlaybackSpeed(lastSelectedPlaybackSpeed);
|
||||
|
||||
Logger.printDebug(() -> "changing playback speed to: 1.0, isLiveStream: " + newlyLoadedLiveStreamValue +
|
||||
", isMusic: " + isMusic + ", isWhitelisted: " + isWhitelisted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,18 +115,32 @@ public class PlaybackSpeedPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* This method is called every second for regular videos and Shorts.
|
||||
*/
|
||||
public static float getPlaybackSpeedInShorts(final float playbackSpeed) {
|
||||
if (VideoInformation.lastPlayerResponseIsShort() &&
|
||||
Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get()
|
||||
) {
|
||||
float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null);
|
||||
Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed);
|
||||
public static float getPlaybackSpeed(float playbackSpeed) {
|
||||
boolean isShorts = isShortsActive();
|
||||
float defaultPlaybackSpeed = isShorts ? DEFAULT_PLAYBACK_SPEED_SHORTS.get() : DEFAULT_PLAYBACK_SPEED.get();
|
||||
|
||||
if (defaultPlaybackSpeed < 0) { // If the default playback speed is 'Auto', it will be overridden to the last used playback speed.
|
||||
float finalPlaybackSpeed = isShorts ? lastSelectedShortsPlaybackSpeed : lastSelectedPlaybackSpeed;
|
||||
VideoInformation.overridePlaybackSpeed(finalPlaybackSpeed);
|
||||
Logger.printDebug(() -> "changing playback speed to: " + finalPlaybackSpeed);
|
||||
return finalPlaybackSpeed;
|
||||
} else { // Otherwise the default playback speed is used.
|
||||
synchronized (ignoredPlaybackSpeedVideoIds) {
|
||||
if (isShorts) {
|
||||
// For Shorts, the VideoInformation.overridePlaybackSpeed() method is not used, so manually save the playback speed in VideoInformation.
|
||||
VideoInformation.setPlaybackSpeed(defaultPlaybackSpeed);
|
||||
} else if (ignoredPlaybackSpeedVideoIds.containsKey(videoId)) {
|
||||
// For general videos, check whether the default video playback speed should not be applied.
|
||||
Logger.printDebug(() -> "changing playback speed to: 1.0");
|
||||
return 1.0f;
|
||||
}
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "changing playback speed to: " + defaultPlaybackSpeed);
|
||||
return defaultPlaybackSpeed;
|
||||
}
|
||||
|
||||
return playbackSpeed;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -86,51 +151,78 @@ public class PlaybackSpeedPatch {
|
||||
*/
|
||||
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
|
||||
try {
|
||||
if (PatchStatus.RememberPlaybackSpeed() &&
|
||||
Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
|
||||
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
|
||||
// then the menu will allow increasing without bounds but the max speed is
|
||||
// still capped to under 8.0x.
|
||||
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
|
||||
boolean isShorts = isShortsActive();
|
||||
|
||||
// Prevent toast spamming if using the 0.05x adjustments.
|
||||
// Show exactly one toast after the user stops interacting with the speed menu.
|
||||
final long now = System.currentTimeMillis();
|
||||
lastTimeSpeedChanged = now;
|
||||
// Saves the user-selected playback speed in the method.
|
||||
if (isShorts) {
|
||||
lastSelectedShortsPlaybackSpeed = playbackSpeed;
|
||||
} else {
|
||||
lastSelectedPlaybackSpeed = playbackSpeed;
|
||||
// If the user has manually changed the playback speed, the whitelist has already been applied.
|
||||
// If there is a videoId on the map, it will be removed.
|
||||
synchronized (ignoredPlaybackSpeedVideoIds) {
|
||||
ignoredPlaybackSpeedVideoIds.remove(videoId);
|
||||
}
|
||||
}
|
||||
|
||||
final float finalPlaybackSpeed = playbackSpeed;
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
if (lastTimeSpeedChanged != now) {
|
||||
// The user made additional speed adjustments and this call is outdated.
|
||||
return;
|
||||
}
|
||||
if (PatchStatus.RememberPlaybackSpeed()) {
|
||||
BooleanSetting rememberPlaybackSpeedLastSelectedSetting = isShorts
|
||||
? Settings.REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED
|
||||
: Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED;
|
||||
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;
|
||||
|
||||
if (Settings.DEFAULT_PLAYBACK_SPEED.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;
|
||||
}
|
||||
Settings.DEFAULT_PLAYBACK_SPEED.save(finalPlaybackSpeed);
|
||||
if (rememberPlaybackSpeedLastSelectedSetting.get()) {
|
||||
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
|
||||
// then the menu will allow increasing without bounds but the max speed is
|
||||
// still capped to under 8.0x.
|
||||
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
|
||||
|
||||
if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) {
|
||||
return;
|
||||
}
|
||||
Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
|
||||
}, TOAST_DELAY_MILLISECONDS);
|
||||
// Prevent toast spamming if using the 0.05x adjustments.
|
||||
// Show exactly one toast after the user stops interacting with the speed menu.
|
||||
final long now = System.currentTimeMillis();
|
||||
lastTimeSpeedChanged = now;
|
||||
|
||||
final float finalPlaybackSpeed = playbackSpeed;
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
if (lastTimeSpeedChanged != now) {
|
||||
// The user made additional speed adjustments and this call is outdated.
|
||||
return;
|
||||
}
|
||||
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 (showToastSetting.get()) {
|
||||
Utils.showToastShort(str(isShorts ? "revanced_remember_playback_speed_toast_shorts" : "revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
|
||||
}
|
||||
}, TOAST_DELAY_MILLISECONDS);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "userSelectedPlaybackSpeed failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) {
|
||||
return (isLiveStream || Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) || isMusic(videoId))
|
||||
? 1.0f
|
||||
: Settings.DEFAULT_PLAYBACK_SPEED.get();
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void onDismiss() {
|
||||
synchronized (ignoredPlaybackSpeedVideoIds) {
|
||||
ignoredPlaybackSpeedVideoIds.remove(videoId);
|
||||
videoId = "";
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isMusic(@Nullable String videoId) {
|
||||
if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && videoId != null) {
|
||||
private static boolean isMusic(String videoId) {
|
||||
if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && !videoId.isEmpty()) {
|
||||
try {
|
||||
MusicRequest request = MusicRequest.getRequestForVideoId(videoId);
|
||||
final boolean isMusic = request != null && BooleanUtils.toBoolean(request.getStream());
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app.revanced.extension.youtube.patches.video;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
@ -14,8 +15,10 @@ import app.revanced.extension.youtube.shared.VideoInformation;
|
||||
@SuppressWarnings("unused")
|
||||
public class VideoQualityPatch {
|
||||
private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2;
|
||||
private static final IntegerSetting mobileQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_MOBILE;
|
||||
private static final IntegerSetting wifiQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_WIFI;
|
||||
private static final IntegerSetting shortsQualityMobile = Settings.DEFAULT_VIDEO_QUALITY_MOBILE_SHORTS;
|
||||
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
|
||||
public static String videoId = "";
|
||||
@ -35,12 +38,11 @@ public class VideoQualityPatch {
|
||||
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
|
||||
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
|
||||
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
|
||||
if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL)
|
||||
return;
|
||||
if (videoId.equals(newlyLoadedVideoId))
|
||||
return;
|
||||
videoId = newlyLoadedVideoId;
|
||||
setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 750);
|
||||
if (PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL &&
|
||||
!videoId.equals(newlyLoadedVideoId)) {
|
||||
videoId = newlyLoadedVideoId;
|
||||
setVideoQuality(750);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -53,42 +55,62 @@ public class VideoQualityPatch {
|
||||
);
|
||||
}
|
||||
|
||||
private static void setVideoQuality(final long delayMillis) {
|
||||
final int defaultQuality = Utils.getNetworkType() == Utils.NetworkType.MOBILE
|
||||
? mobileQualitySetting.get()
|
||||
: wifiQualitySetting.get();
|
||||
private static void setVideoQuality(long delayMillis) {
|
||||
boolean isShorts = isShortsActive();
|
||||
IntegerSetting defaultQualitySetting = Utils.getNetworkType() == Utils.NetworkType.MOBILE
|
||||
? isShorts ? shortsQualityMobile : videoQualityMobile
|
||||
: isShorts ? shortsQualityWifi : videoQualityWifi;
|
||||
|
||||
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
|
||||
return;
|
||||
int defaultQuality = defaultQualitySetting.get();
|
||||
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality);
|
||||
Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal);
|
||||
VideoInformation.overrideVideoQuality(qualityToUseFinal);
|
||||
}, delayMillis
|
||||
);
|
||||
if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality);
|
||||
Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal);
|
||||
VideoInformation.overrideVideoQuality(qualityToUseFinal);
|
||||
}, delayMillis
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void userSelectedVideoQuality(final int defaultQuality) {
|
||||
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get())
|
||||
return;
|
||||
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY)
|
||||
return;
|
||||
if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
|
||||
final Utils.NetworkType networkType = Utils.getNetworkType();
|
||||
String networkTypeMessage = networkType == Utils.NetworkType.MOBILE
|
||||
? 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) {
|
||||
case NONE -> {
|
||||
Utils.showToastShort(str("revanced_remember_video_quality_none"));
|
||||
return;
|
||||
defaultQualitySetting.save(defaultQuality);
|
||||
|
||||
if (Settings.REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED_TOAST.get()) {
|
||||
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 androidx.annotation.GuardedBy
|
||||
import app.revanced.extension.shared.patches.client.YouTubeAppClient
|
||||
import app.revanced.extension.shared.patches.client.YouTubeWebClient
|
||||
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes
|
||||
import app.revanced.extension.shared.innertube.client.YouTubeAppClient
|
||||
import app.revanced.extension.shared.innertube.client.YouTubeWebClient
|
||||
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.utils.Logger
|
||||
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" }
|
||||
|
||||
try {
|
||||
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
|
||||
PlayerRoutes.GET_PLAYLIST_PAGE,
|
||||
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||
GET_PLAYLIST_PAGE,
|
||||
clientType
|
||||
)
|
||||
val requestBody =
|
||||
PlayerRoutes.createApplicationRequestBody(
|
||||
createApplicationRequestBody(
|
||||
clientType = clientType,
|
||||
videoId = videoId,
|
||||
playlistId = "RD$videoId"
|
||||
@ -168,12 +172,11 @@ class MusicRequest private constructor(
|
||||
Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" }
|
||||
|
||||
try {
|
||||
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
|
||||
PlayerRoutes.GET_CATEGORY,
|
||||
val connection = getInnerTubeResponseConnectionFromRoute(
|
||||
GET_CATEGORY,
|
||||
clientType
|
||||
)
|
||||
val requestBody =
|
||||
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
|
||||
val requestBody = createWebInnertubeBody(clientType, videoId)
|
||||
|
||||
connection.setFixedLengthStreamingMode(requestBody.size)
|
||||
connection.outputStream.write(requestBody)
|
||||
|
@ -115,7 +115,7 @@ public class ReturnYouTubeDislike {
|
||||
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;
|
||||
private static final ShapeDrawable leftSeparatorShape;
|
||||
@ -131,7 +131,7 @@ public class ReturnYouTubeDislike {
|
||||
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
|
||||
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.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.StringSetting;
|
||||
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.StillImagesAvailability;
|
||||
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.shorts.AnimationFeedbackPatch.AnimationType;
|
||||
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.shared.PlaylistIdPrefix;
|
||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||
@ -148,7 +151,6 @@ public class Settings extends BaseSettings {
|
||||
new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
|
||||
public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE);
|
||||
public static final BooleanSetting DISABLE_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_splash_animation", PatchStatus.SplashAnimation(), true);
|
||||
public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", TRUE, true);
|
||||
public static final BooleanSetting ENABLE_GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_enable_gradient_loading_screen", FALSE, true);
|
||||
public static final BooleanSetting HIDE_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true);
|
||||
public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE);
|
||||
@ -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 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 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_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 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);
|
||||
|
||||
// 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_VIDEO_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_video_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, 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_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
|
||||
@ -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);
|
||||
|
||||
// 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 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));
|
||||
@ -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_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_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_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);
|
||||
@ -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_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_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 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,
|
||||
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
|
||||
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_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_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_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 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);
|
||||
|
||||
|
||||
// PreferenceScreen: Video
|
||||
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);
|
||||
// PreferenceScreen: Video - Codec
|
||||
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 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 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_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);
|
||||
// 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_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 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
|
||||
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_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_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_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_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_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_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_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_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_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_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
|
||||
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 {
|
||||
// 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.
|
||||
Set<Setting<?>> sbCategories = new HashSet<>(Arrays.asList(
|
||||
SB_CATEGORY_SPONSOR,
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
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.updateListPreferenceSummary;
|
||||
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.showToastShort;
|
||||
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_TYPE;
|
||||
|
||||
@ -31,6 +33,7 @@ import android.preference.PreferenceGroup;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.util.Pair;
|
||||
import android.util.TypedValue;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
@ -55,12 +58,17 @@ import java.util.Set;
|
||||
import java.util.SortedMap;
|
||||
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.EnumSetting;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
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.youtube.patches.video.CustomPlaybackSpeedPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.utils.ExtendedUtils;
|
||||
import app.revanced.extension.youtube.utils.ThemeUtils;
|
||||
|
||||
@ -74,14 +82,19 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
||||
@SuppressLint("SuspiciousIndentation")
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||
try {
|
||||
if (str == null) return;
|
||||
Setting<?> setting = Setting.getSettingFromPath(Objects.requireNonNull(str));
|
||||
if (str == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (setting == null) return;
|
||||
Setting<?> setting = Setting.getSettingFromPath(str);
|
||||
if (setting == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Preference mPreference = findPreference(str);
|
||||
|
||||
if (mPreference == null) return;
|
||||
if (mPreference == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mPreference instanceof SwitchPreference switchPreference) {
|
||||
BooleanSetting boolSetting = (BooleanSetting) setting;
|
||||
@ -108,9 +121,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
||||
} else {
|
||||
Setting.privateSetValueFromString(setting, listPreference.getValue());
|
||||
}
|
||||
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
|
||||
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
|
||||
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
|
||||
if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
|
||||
listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
|
||||
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)) {
|
||||
updateListPreferenceSummary(listPreference, setting);
|
||||
@ -122,18 +139,11 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
ReVancedSettingsPreference.initializeReVancedSettings();
|
||||
|
||||
if (settingImportInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showingUserDialogMessage) {
|
||||
if (!settingImportInProgress && !showingUserDialogMessage) {
|
||||
final Context context = getActivity();
|
||||
|
||||
if (setting.userDialogMessage != null
|
||||
&& mPreference instanceof SwitchPreference switchPreference
|
||||
&& setting.defaultValue instanceof Boolean defaultValue
|
||||
&& switchPreference.isChecked() != defaultValue) {
|
||||
showSettingUserDialogConfirmation(context, switchPreference, (BooleanSetting) setting);
|
||||
if (setting.userDialogMessage != null && !prefIsSetToDefault(mPreference, setting)) {
|
||||
showSettingUserDialogConfirmation(context, mPreference, setting);
|
||||
} else if (setting.rebootApp) {
|
||||
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();
|
||||
|
||||
showingUserDialogMessage = true;
|
||||
assert setting.userDialogMessage != null;
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("revanced_extended_confirm_user_dialog_title"))
|
||||
.setMessage(setting.userDialogMessage.toString())
|
||||
.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.
|
||||
})
|
||||
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
final StringRef userDialogMessage = setting.userDialogMessage;
|
||||
if (context != null && userDialogMessage != null) {
|
||||
showingUserDialogMessage = true;
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(str("revanced_extended_confirm_user_dialog_title"))
|
||||
.setMessage(userDialogMessage.toString())
|
||||
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
||||
if (setting.rebootApp) {
|
||||
showRestartDialog(context);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||
// Restore whatever the setting was before the change.
|
||||
if (setting instanceof BooleanSetting booleanSetting &&
|
||||
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;
|
||||
@ -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()) {
|
||||
mPreferenceScreen.setOnPreferenceClickListener(
|
||||
preferenceScreen -> {
|
||||
@ -205,11 +249,24 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
||||
.findViewById(android.R.id.content)
|
||||
.getParent();
|
||||
|
||||
// Fix required for Android 15
|
||||
if (isSDKAbove(35)) {
|
||||
// Edge-to-edge is enforced if the following conditions are met:
|
||||
// 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) -> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
@ -283,9 +340,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
||||
} else if (preference instanceof EditTextPreference editTextPreference) {
|
||||
editTextPreference.setText(setting.get().toString());
|
||||
} else if (preference instanceof ListPreference listPreference) {
|
||||
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) {
|
||||
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries());
|
||||
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues());
|
||||
if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
|
||||
listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
|
||||
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)) {
|
||||
updateListPreferenceSummary(listPreference, setting);
|
||||
@ -298,6 +359,10 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity());
|
||||
copyPreferences(getPreferenceScreen(), originalPreferenceScreen);
|
||||
|
||||
sortPreferenceListMenu(Settings.CHANGE_START_PAGE);
|
||||
sortPreferenceListMenu(Settings.SPOOF_STREAMING_DATA_LANGUAGE);
|
||||
sortPreferenceListMenu(BaseSettings.REVANCED_LANGUAGE);
|
||||
} catch (Exception th) {
|
||||
Logger.printException(() -> "Error during onCreate()", th);
|
||||
}
|
||||
@ -312,9 +377,69 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
|
||||
Utils.resetLocalizedContext();
|
||||
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.
|
||||
*
|
||||
|
@ -2,11 +2,12 @@ package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.utils.StringRef.str;
|
||||
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.SwitchPreference;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch;
|
||||
import app.revanced.extension.youtube.patches.utils.PatchStatus;
|
||||
@ -43,11 +44,12 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
|
||||
enableDisablePreferences();
|
||||
|
||||
AmbientModePreferenceLinks();
|
||||
ExternalDownloaderPreferenceLinks();
|
||||
FullScreenPanelPreferenceLinks();
|
||||
NavigationPreferenceLinks();
|
||||
PatchInformationPreferenceLinks();
|
||||
RYDPreferenceLinks();
|
||||
SeekBarPreferenceLinks();
|
||||
ShortsPreferenceLinks();
|
||||
SpeedOverlayPreferenceLinks();
|
||||
QuickActionsPreferenceLinks();
|
||||
TabletLayoutLinks();
|
||||
@ -65,18 +67,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
|
||||
*/
|
||||
@ -156,6 +146,26 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set patch information preference summary
|
||||
*/
|
||||
private static void PatchInformationPreferenceLinks() {
|
||||
Preference appNamePreference = mPreferenceManager.findPreference("revanced_app_name");
|
||||
if (appNamePreference != null) {
|
||||
appNamePreference.setSummary(ExtendedUtils.getAppLabel());
|
||||
}
|
||||
Preference appVersionPreference = mPreferenceManager.findPreference("revanced_app_version");
|
||||
if (appVersionPreference != null) {
|
||||
appVersionPreference.setSummary(ExtendedUtils.getAppVersionName());
|
||||
}
|
||||
Preference patchedDatePreference = mPreferenceManager.findPreference("revanced_patched_date");
|
||||
if (patchedDatePreference != null) {
|
||||
long patchedTime = PatchStatus.PatchedTime();
|
||||
Date date = new Date(patchedTime);
|
||||
patchedDatePreference.setSummary(date.toLocaleString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/Disable Preference related to RYD settings
|
||||
*/
|
||||
@ -200,6 +210,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
|
||||
*/
|
||||
|
@ -1,6 +1,7 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
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.content.Context;
|
||||
@ -12,41 +13,51 @@ import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TableRow;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
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.sponsorblock.objects.CategoryBehaviour;
|
||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class SegmentCategoryListPreference extends ListPreference {
|
||||
private SegmentCategory mCategory;
|
||||
private EditText mEditText;
|
||||
private int mClickedDialogEntryIndex;
|
||||
private SegmentCategory category;
|
||||
private TextView colorDotView;
|
||||
private EditText colorEditText;
|
||||
private EditText opacityEditText;
|
||||
/**
|
||||
* #RRGGBB
|
||||
*/
|
||||
private int categoryColor;
|
||||
/**
|
||||
* [0, 1]
|
||||
*/
|
||||
private float categoryOpacity;
|
||||
private int selectedDialogEntryIndex;
|
||||
|
||||
private void init() {
|
||||
final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey());
|
||||
final boolean isHighlightCategory = segmentCategory == SegmentCategory.HIGHLIGHT;
|
||||
mCategory = Objects.requireNonNull(segmentCategory);
|
||||
category = Objects.requireNonNull(segmentCategory);
|
||||
|
||||
// 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);
|
||||
setDefaultValue(segmentCategory.behaviorSetting.defaultValue);
|
||||
|
||||
final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
|
||||
setEntries(isHighlightCategory
|
||||
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
|
||||
: CategoryBehaviour.getBehaviorDescriptions());
|
||||
setEntryValues(isHighlightCategory
|
||||
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
|
||||
: CategoryBehaviour.getBehaviorKeyValues());
|
||||
updateTitle();
|
||||
|
||||
updateTitleFromCategory();
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
|
||||
categoryColor = category.getColorNoOpacity();
|
||||
categoryOpacity = category.getOpacity();
|
||||
|
||||
Context context = builder.getContext();
|
||||
TableLayout table = new TableLayout(context);
|
||||
table.setOrientation(LinearLayout.HORIZONTAL);
|
||||
table.setPadding(70, 0, 150, 0);
|
||||
|
||||
TableRow row = new TableRow(context);
|
||||
GridLayout gridLayout = new GridLayout(context);
|
||||
gridLayout.setPadding(70, 0, 150, 0); // Padding for the entire layout.
|
||||
gridLayout.setColumnCount(3);
|
||||
gridLayout.setRowCount(2);
|
||||
|
||||
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);
|
||||
colorTextLabel.setText(str("revanced_sb_color_dot_label"));
|
||||
row.addView(colorTextLabel);
|
||||
colorTextLabel.setLayoutParams(gridParams);
|
||||
gridLayout.addView(colorTextLabel);
|
||||
|
||||
TextView colorDotView = new TextView(context);
|
||||
colorDotView.setText(mCategory.getCategoryColorDot());
|
||||
colorDotView.setPadding(30, 0, 30, 0);
|
||||
row.addView(colorDotView);
|
||||
gridParams = new GridLayout.LayoutParams();
|
||||
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
||||
gridParams.columnSpec = GridLayout.spec(1); // Second column.
|
||||
gridParams.setMargins(0, 0, 10, 0);
|
||||
colorDotView = new TextView(context);
|
||||
colorDotView.setLayoutParams(gridParams);
|
||||
gridLayout.addView(colorDotView);
|
||||
updateCategoryColorDot();
|
||||
|
||||
mEditText = new EditText(context);
|
||||
mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
|
||||
mEditText.setText(mCategory.colorString());
|
||||
mEditText.addTextChangedListener(new TextWatcher() {
|
||||
gridParams = new GridLayout.LayoutParams();
|
||||
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
||||
gridParams.columnSpec = GridLayout.spec(2); // Third column.
|
||||
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
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
@ -104,44 +128,111 @@ public class SegmentCategoryListPreference extends ListPreference {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
public void afterTextChanged(Editable edit) {
|
||||
try {
|
||||
String colorString = s.toString();
|
||||
String colorString = edit.toString();
|
||||
final int colorStringLength = colorString.length();
|
||||
|
||||
if (!colorString.startsWith("#")) {
|
||||
s.insert(0, "#"); // recursively calls back into this method
|
||||
edit.insert(0, "#"); // Recursively calls back into this method.
|
||||
return;
|
||||
}
|
||||
if (colorString.length() > 7) {
|
||||
s.delete(7, colorString.length());
|
||||
|
||||
final int maxColorStringLength = 7; // #RRGGBB
|
||||
if (colorStringLength > maxColorStringLength) {
|
||||
edit.delete(maxColorStringLength, colorStringLength);
|
||||
return;
|
||||
}
|
||||
final int color = Color.parseColor(colorString);
|
||||
colorDotView.setText(SegmentCategory.getCategoryColorDot(color));
|
||||
|
||||
categoryColor = Color.parseColor(colorString);
|
||||
updateCategoryColorDot();
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// ignore
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
});
|
||||
mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f));
|
||||
row.addView(mEditText);
|
||||
colorEditText.setLayoutParams(gridParams);
|
||||
gridLayout.addView(colorEditText);
|
||||
|
||||
table.addView(row);
|
||||
builder.setView(table);
|
||||
builder.setTitle(mCategory.title.toString());
|
||||
gridParams = new GridLayout.LayoutParams();
|
||||
gridParams.rowSpec = GridLayout.spec(1); // Second row.
|
||||
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.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
|
||||
try {
|
||||
mCategory.resetColor();
|
||||
updateTitle();
|
||||
category.resetColorAndOpacity();
|
||||
updateTitleFromCategory();
|
||||
Utils.showToastShort(str("revanced_sb_color_reset"));
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setNeutralButton failure", ex);
|
||||
}
|
||||
});
|
||||
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) {
|
||||
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
||||
}
|
||||
@ -150,31 +241,50 @@ public class SegmentCategoryListPreference extends ListPreference {
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
try {
|
||||
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) {
|
||||
String value = getEntryValues()[mClickedDialogEntryIndex].toString();
|
||||
if (positiveResult && selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
|
||||
String value = getEntryValues()[selectedDialogEntryIndex].toString();
|
||||
if (callChangeListener(value)) {
|
||||
setValue(value);
|
||||
mCategory.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
|
||||
category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
|
||||
SegmentCategory.updateEnabledCategories();
|
||||
}
|
||||
String colorString = mEditText.getText().toString();
|
||||
try {
|
||||
if (!colorString.equals(mCategory.colorString())) {
|
||||
mCategory.setColor(colorString);
|
||||
String colorString = colorEditText.getText().toString();
|
||||
if (!colorString.equals(category.getColorString()) || categoryOpacity != category.getOpacity()) {
|
||||
category.setColor(colorString);
|
||||
category.setOpacity(categoryOpacity);
|
||||
Utils.showToastShort(str("revanced_sb_color_changed"));
|
||||
}
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Utils.showToastShort(str("revanced_sb_color_invalid"));
|
||||
}
|
||||
updateTitle();
|
||||
|
||||
updateTitleFromCategory();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onDialogClosed failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTitle() {
|
||||
setTitle(mCategory.getTitleWithColorDot());
|
||||
setEnabled(Settings.SB_ENABLED.get());
|
||||
private void applyOpacityToCategoryColor() {
|
||||
categoryColor = applyOpacityToColor(categoryColor, categoryOpacity);
|
||||
}
|
||||
|
||||
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.setLayoutResource(preferencesCategoryLayout);
|
||||
statsCategory.setTitle(str("revanced_sb_stats"));
|
||||
statsCategory.setEnabled(Settings.SB_ENABLED.get());
|
||||
mPreferenceScreen.addPreference(statsCategory);
|
||||
fetchAndDisplayStats();
|
||||
|
||||
@ -261,7 +262,6 @@ public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
|
||||
final String key = category.keyValue;
|
||||
if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) {
|
||||
segmentCategoryListPreference.setTitle(category.getTitleWithColorDot());
|
||||
segmentCategoryListPreference.setEnabled(Settings.SB_ENABLED.get());
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
|
@ -58,6 +58,10 @@ public final class RootView {
|
||||
return PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get();
|
||||
}
|
||||
|
||||
public static boolean isShortsActive() {
|
||||
return ShortsPlayerState.getCurrent().isOpen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current BrowseId.
|
||||
* Rest of the implementation added by patch.
|
||||
|
@ -48,4 +48,12 @@ enum class ShortsPlayerState {
|
||||
fun isClosed(): Boolean {
|
||||
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) {
|
||||
JSONObject categoryObject = new JSONObject();
|
||||
String categoryKey = category.keyValue;
|
||||
categoryObject.put("color", category.colorString());
|
||||
categoryObject.put("color", category.getColorString());
|
||||
barTypesObject.put(categoryKey, categoryObject);
|
||||
|
||||
if (category.behaviour != CategoryBehaviour.IGNORE) {
|
||||
|
@ -6,7 +6,12 @@ import android.annotation.TargetApi;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
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 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.
|
||||
*
|
||||
* @noinspection deprecation
|
||||
*/
|
||||
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 Pattern manualEditTimePattern
|
||||
@ -162,28 +165,34 @@ public class SponsorBlockUtils {
|
||||
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
|
||||
? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category
|
||||
: 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];
|
||||
String title = voteOption.title.toString();
|
||||
if (Settings.SB_USER_IS_VIP.get() && segment.isLocked && voteOption.shouldHighlight) {
|
||||
items[i] = Html.fromHtml(String.format("<font color=\"%s\">%s</font>", LOCKED_COLOR, title));
|
||||
} else {
|
||||
items[i] = title;
|
||||
CharSequence title = voteOption.title.toString();
|
||||
if (userIsVip && segment.isLocked && voteOption.highlightIfVipAndVideoIsLocked) {
|
||||
SpannableString coloredTitle = new SpannableString(title);
|
||||
coloredTitle.setSpan(new ForegroundColorSpan(LOCKED_COLOR),
|
||||
0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
title = coloredTitle;
|
||||
}
|
||||
items[i] = title;
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setItems(items, (dialog1, which1) -> {
|
||||
SegmentVote voteOption = voteOptions[which1];
|
||||
switch (voteOption) {
|
||||
case UPVOTE, DOWNVOTE ->
|
||||
SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
|
||||
case CATEGORY_CHANGE -> onNewCategorySelect(segment, context);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
new AlertDialog.Builder(context).setItems(items, (dialog1, which1) -> {
|
||||
SegmentVote voteOption = voteOptions[which1];
|
||||
switch (voteOption) {
|
||||
case UPVOTE:
|
||||
case DOWNVOTE:
|
||||
SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
|
||||
break;
|
||||
case CATEGORY_CHANGE:
|
||||
onNewCategorySelect(segment, context);
|
||||
break;
|
||||
}
|
||||
}).show();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "segmentVoteClickListener failure", ex);
|
||||
}
|
||||
@ -287,22 +296,33 @@ public class SponsorBlockUtils {
|
||||
if (segment.category == SegmentCategory.UNSUBMITTED) {
|
||||
continue;
|
||||
}
|
||||
StringBuilder htmlBuilder = new StringBuilder();
|
||||
htmlBuilder.append(String.format("<b><font color=\"#%06X\">⬤</font> %s<br>",
|
||||
segment.category.color, segment.category.title));
|
||||
htmlBuilder.append(formatSegmentTime(segment.start));
|
||||
if (segment.category != SegmentCategory.HIGHLIGHT) {
|
||||
htmlBuilder.append(" to ").append(formatSegmentTime(segment.end));
|
||||
|
||||
SpannableStringBuilder spannableBuilder = new SpannableStringBuilder();
|
||||
|
||||
spannableBuilder.append(segment.category.getTitleWithColorDot());
|
||||
spannableBuilder.append('\n');
|
||||
|
||||
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
|
||||
htmlBuilder.append("<br>");
|
||||
titles[i] = Html.fromHtml(htmlBuilder.toString());
|
||||
|
||||
if (i + 1 != numberOfSegments) {
|
||||
// prevents trailing new line after last segment
|
||||
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)
|
||||
.setItems(titles, segmentVoteClickListener)
|
||||
.show();
|
||||
new AlertDialog.Builder(context).setItems(titles, segmentVoteClickListener).show();
|
||||
} catch (Exception 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.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_OPACITY;
|
||||
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_OPACITY;
|
||||
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_OPACITY;
|
||||
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_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_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_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_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_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_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_COLOR;
|
||||
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_OPACITY;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@ -34,45 +45,46 @@ import androidx.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.settings.FloatSetting;
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
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.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings({"deprecation", "StaticFieldLeak"})
|
||||
@SuppressWarnings("StaticFieldLeak")
|
||||
public enum SegmentCategory {
|
||||
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"),
|
||||
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"),
|
||||
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.
|
||||
*/
|
||||
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"),
|
||||
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"),
|
||||
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"),
|
||||
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"),
|
||||
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"),
|
||||
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"),
|
||||
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"),
|
||||
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"),
|
||||
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 skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight");
|
||||
@ -111,12 +123,10 @@ public enum SegmentCategory {
|
||||
mValuesMap.put(value.keyValue, value);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static SegmentCategory[] categoriesWithoutUnsubmitted() {
|
||||
return categoriesWithoutUnsubmitted;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static SegmentCategory[] 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() {
|
||||
Utils.verifyOnMainThread();
|
||||
@ -154,30 +164,32 @@ public enum SegmentCategory {
|
||||
updateEnabledCategories();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public final String keyValue;
|
||||
@NonNull
|
||||
public final StringSetting behaviorSetting;
|
||||
@NonNull
|
||||
private final StringSetting colorSetting;
|
||||
public static int applyOpacityToColor(int color, float opacity) {
|
||||
if (opacity < 0 || opacity > 1.0f) {
|
||||
throw new IllegalArgumentException("Invalid opacity: " + opacity);
|
||||
}
|
||||
final int opacityInt = (int) (255 * opacity);
|
||||
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;
|
||||
|
||||
/**
|
||||
* Skip button text, if the skip occurs in the first quarter of the video
|
||||
*/
|
||||
@NonNull
|
||||
public final StringRef skipButtonTextBeginning;
|
||||
/**
|
||||
* Skip button text, if the skip occurs in the middle half of the video
|
||||
*/
|
||||
@NonNull
|
||||
public final StringRef skipButtonTextMiddle;
|
||||
/**
|
||||
* Skip button text, if the skip occurs in the last quarter of the video
|
||||
*/
|
||||
@NonNull
|
||||
public final StringRef skipButtonTextEnd;
|
||||
/**
|
||||
* Skipped segment toast, if the skip occurred in the first quarter of the video
|
||||
@ -198,10 +210,7 @@ public enum SegmentCategory {
|
||||
@NonNull
|
||||
public final Paint paint;
|
||||
|
||||
/**
|
||||
* Value must be changed using {@link #setColor(String)}.
|
||||
*/
|
||||
public int color;
|
||||
private int color;
|
||||
|
||||
/**
|
||||
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
|
||||
@ -213,17 +222,20 @@ public enum SegmentCategory {
|
||||
SegmentCategory(String keyValue, StringRef title,
|
||||
StringRef skipButtonText,
|
||||
StringRef skippedToastText,
|
||||
StringSetting behavior, StringSetting color) {
|
||||
StringSetting behavior,
|
||||
StringSetting color, FloatSetting opacity) {
|
||||
this(keyValue, title,
|
||||
skipButtonText, skipButtonText, skipButtonText,
|
||||
skippedToastText, skippedToastText, skippedToastText,
|
||||
behavior, color);
|
||||
behavior,
|
||||
color, opacity);
|
||||
}
|
||||
|
||||
SegmentCategory(String keyValue, StringRef title,
|
||||
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
|
||||
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
|
||||
StringSetting behavior, StringSetting color) {
|
||||
StringSetting behavior,
|
||||
StringSetting color, FloatSetting opacity) {
|
||||
this.keyValue = Objects.requireNonNull(keyValue);
|
||||
this.title = Objects.requireNonNull(title);
|
||||
this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning);
|
||||
@ -234,6 +246,7 @@ public enum SegmentCategory {
|
||||
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
|
||||
this.behaviorSetting = Objects.requireNonNull(behavior);
|
||||
this.colorSetting = Objects.requireNonNull(color);
|
||||
this.opacitySetting = Objects.requireNonNull(opacity);
|
||||
this.paint = new Paint();
|
||||
loadFromSettings();
|
||||
}
|
||||
@ -250,11 +263,14 @@ public enum SegmentCategory {
|
||||
this.behaviour = savedBehavior;
|
||||
|
||||
String colorString = colorSetting.get();
|
||||
final float opacity = opacitySetting.get();
|
||||
try {
|
||||
setColor(colorString);
|
||||
setOpacity(opacity);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Invalid color: " + colorString, ex);
|
||||
Logger.printException(() -> "Invalid color: " + colorString + " opacity: " + opacity, ex);
|
||||
colorSetting.resetToDefault();
|
||||
opacitySetting.resetToDefault();
|
||||
loadFromSettings();
|
||||
}
|
||||
}
|
||||
@ -264,45 +280,77 @@ public enum SegmentCategory {
|
||||
this.behaviorSetting.save(behaviour.reVancedKeyValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HTML color format string
|
||||
*/
|
||||
@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;
|
||||
private void updateColor() {
|
||||
color = applyOpacityToColor(color, opacitySetting.get());
|
||||
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);
|
||||
setOpacity(opacitySetting.defaultValue);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static String getCategoryColorDotHTML(int color) {
|
||||
color &= 0xFFFFFF;
|
||||
return String.format("<font color=\"#%06X\">⬤</font>", color);
|
||||
/**
|
||||
* @param colorString Segment color with #RRGGBB format.
|
||||
*/
|
||||
public void setColor(String colorString) throws IllegalArgumentException {
|
||||
color = Color.parseColor(colorString);
|
||||
colorSetting.save(colorString);
|
||||
|
||||
updateColor();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Spanned getCategoryColorDot(int color) {
|
||||
return Html.fromHtml(getCategoryColorDotHTML(color));
|
||||
/**
|
||||
* @return Integer color of #RRGGBB format.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Spanned getTitleWithColorDot() {
|
||||
return Html.fromHtml(getCategoryColorDotHTML(color) + " " + title);
|
||||
public SpannableString getTitleWithColorDot(int categoryColor) {
|
||||
return getCategoryColorDotSpan(" " + title, categoryColor);
|
||||
}
|
||||
|
||||
public SpannableString getTitleWithColorDot() {
|
||||
return getTitleWithColorDot(color);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -310,7 +358,6 @@ public enum SegmentCategory {
|
||||
* @param videoLength length of the video
|
||||
* @return the skip button text
|
||||
*/
|
||||
@NonNull
|
||||
StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
|
||||
if (Settings.SB_COMPACT_SKIP_BUTTON.get()) {
|
||||
return (this == SegmentCategory.HIGHLIGHT)
|
||||
@ -319,7 +366,7 @@ public enum SegmentCategory {
|
||||
}
|
||||
|
||||
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;
|
||||
if (position < 0.25f) {
|
||||
@ -335,10 +382,9 @@ public enum SegmentCategory {
|
||||
* @param videoLength length of the video
|
||||
* @return 'skipped segment' toast message
|
||||
*/
|
||||
@NonNull
|
||||
StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
|
||||
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;
|
||||
if (position < 0.25f) {
|
||||
|
@ -24,12 +24,15 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
|
||||
@NonNull
|
||||
public final StringRef title;
|
||||
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.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
|
||||
*/
|
||||
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
|
||||
@ -133,6 +133,59 @@ class SwipeControlsConfigurationProvider(
|
||||
val overlayForegroundColor: Int
|
||||
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
|
||||
|
||||
// region behaviour
|
||||
|
@ -24,7 +24,7 @@ import java.lang.ref.WeakReference
|
||||
* The main controller for volume and brightness swipe controls.
|
||||
* 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() {
|
||||
/**
|
||||
|
@ -1,14 +1,18 @@
|
||||
package app.revanced.extension.youtube.swipecontrols.views
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
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.GradientDrawable
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
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.misc.SwipeControlsOverlay
|
||||
import app.revanced.extension.youtube.swipecontrols.misc.applyDimension
|
||||
import kotlin.math.min
|
||||
import kotlin.math.round
|
||||
|
||||
/**
|
||||
@ -33,36 +38,82 @@ class SwipeControlsOverlayLayout(
|
||||
*/
|
||||
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context))
|
||||
|
||||
private val feedbackTextView: TextView
|
||||
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 mutedVolumeIcon: Drawable
|
||||
private val lowVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_low")
|
||||
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 {
|
||||
return resources.getDrawable(
|
||||
private val circularProgressView: CircularProgressView = CircularProgressView(
|
||||
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),
|
||||
context.theme
|
||||
).apply {
|
||||
setTint(config.overlayForegroundColor)
|
||||
setBounds(
|
||||
context.theme,
|
||||
)
|
||||
|
||||
if (width != null && height != null) {
|
||||
drawable.setTint(config.overlayForegroundColor)
|
||||
drawable.setBounds(
|
||||
0,
|
||||
0,
|
||||
width,
|
||||
height,
|
||||
)
|
||||
} else {
|
||||
drawable.setTint(config.overlayTextColor)
|
||||
}
|
||||
return drawable
|
||||
}
|
||||
|
||||
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
|
||||
val feedbackYTextViewPadding = 5.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
val feedbackXTextViewPadding = 12.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
|
||||
feedbackTextView = TextView(context).apply {
|
||||
layoutParams = LayoutParams(
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
LayoutParams.WRAP_CONTENT,
|
||||
).apply {
|
||||
addRule(CENTER_IN_PARENT, TRUE)
|
||||
setPadding(
|
||||
@ -81,19 +132,36 @@ class SwipeControlsOverlayLayout(
|
||||
compoundDrawablePadding = compoundIconPadding
|
||||
visibility = GONE
|
||||
}
|
||||
addView(feedbackTextView)
|
||||
|
||||
// get icons scaled, assuming square icons
|
||||
val iconHeight = round(feedbackTextView.lineHeight * .8).toInt()
|
||||
autoBrightnessIcon = getDrawable("ic_sc_brightness_auto", iconHeight, iconHeight)
|
||||
manualBrightnessIcon = getDrawable("ic_sc_brightness_manual", iconHeight, iconHeight)
|
||||
mutedVolumeIcon = getDrawable("ic_sc_volume_mute", iconHeight, iconHeight)
|
||||
normalVolumeIcon = getDrawable("ic_sc_volume_normal", iconHeight, iconHeight)
|
||||
if (isAlternativeUI) {
|
||||
addView(circularProgressView)
|
||||
addView(horizontalProgressView)
|
||||
|
||||
autoBrightnessIcon = getDrawable("revanced_ic_sc_brightness_auto")
|
||||
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 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) {
|
||||
showFeedbackView(
|
||||
"$newVolume",
|
||||
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon,
|
||||
)
|
||||
if (isAlternativeUI) {
|
||||
val volumePercentage = (newVolume.toFloat() / maximumVolume) * 100
|
||||
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) {
|
||||
if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) {
|
||||
showFeedbackView(
|
||||
str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
|
||||
autoBrightnessIcon,
|
||||
)
|
||||
if (isAlternativeUI) {
|
||||
showFeedbackView(
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
package app.revanced.extension.youtube.utils;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.utils.Logger;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class AuthUtils {
|
||||
public static final String AUTHORIZATION_HEADER = "Authorization";
|
||||
public static final String[] REQUEST_HEADER_KEYS = {
|
||||
AUTHORIZATION_HEADER,
|
||||
"X-GOOG-API-FORMAT-VERSION",
|
||||
"X-Goog-Visitor-Id"
|
||||
};
|
||||
public static volatile String authorization = "";
|
||||
public static volatile String dataSyncId = "";
|
||||
public static volatile boolean isIncognito = false;
|
||||
public static volatile Map<String, String> requestHeader;
|
||||
public static volatile String playlistId = "";
|
||||
public static volatile String videoId = "";
|
||||
|
||||
public static void setRequestHeaders(String url, Map<String, String> requestHeaders) {
|
||||
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.initializationException(AuthUtils.class, "setRequestHeaders failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,8 +2,27 @@ package app.revanced.extension.youtube.utils;
|
||||
|
||||
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 java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.settings.BooleanSetting;
|
||||
import app.revanced.extension.shared.settings.FloatSetting;
|
||||
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;
|
||||
|
||||
public class ExtendedUtils extends PackageUtils {
|
||||
|
||||
private static boolean isVersionOrGreater(String version) {
|
||||
return getAppVersionName().compareTo(version) >= 0;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final boolean IS_19_17_OR_GREATER = getAppVersionName().compareTo("19.17.00") >= 0;
|
||||
public static final boolean IS_19_20_OR_GREATER = getAppVersionName().compareTo("19.20.00") >= 0;
|
||||
public static final boolean IS_19_21_OR_GREATER = getAppVersionName().compareTo("19.21.00") >= 0;
|
||||
public static final boolean IS_19_26_OR_GREATER = getAppVersionName().compareTo("19.26.00") >= 0;
|
||||
public static final boolean IS_19_28_OR_GREATER = getAppVersionName().compareTo("19.28.00") >= 0;
|
||||
public static final boolean IS_19_29_OR_GREATER = getAppVersionName().compareTo("19.29.00") >= 0;
|
||||
public static final boolean IS_19_34_OR_GREATER = getAppVersionName().compareTo("19.34.00") >= 0;
|
||||
public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00");
|
||||
public static final boolean IS_19_20_OR_GREATER = isVersionOrGreater("19.20.00");
|
||||
public static final boolean IS_19_21_OR_GREATER = isVersionOrGreater("19.21.00");
|
||||
public static final boolean IS_19_26_OR_GREATER = isVersionOrGreater("19.26.00");
|
||||
public static final boolean IS_19_28_OR_GREATER = isVersionOrGreater("19.28.00");
|
||||
public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00");
|
||||
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) {
|
||||
int value = settings.get();
|
||||
@ -114,4 +139,88 @@ public class ExtendedUtils extends PackageUtils {
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
private static String getVideoScheme(String videoId, boolean isShorts) {
|
||||
public static String getVideoScheme(String videoId, boolean isShorts) {
|
||||
return String.format(
|
||||
Locale.ENGLISH,
|
||||
isShorts ? VIDEO_SCHEME_INTENT_FORMAT : VIDEO_SCHEME_LINK_FORMAT,
|
||||
@ -128,6 +128,34 @@ public class VideoUtils extends IntentUtils {
|
||||
launchView(getChannelUrl(channelId), getContext().getPackageName());
|
||||
}
|
||||
|
||||
public static void openPlaylist(@NonNull String playlistId) {
|
||||
openPlaylist(playlistId, "");
|
||||
}
|
||||
|
||||
public static void openPlaylist(@NonNull String playlistId, @NonNull String videoId) {
|
||||
openPlaylist(playlistId, videoId, false);
|
||||
}
|
||||
|
||||
public static void openPlaylist(@NonNull String playlistId, @NonNull String videoId, boolean withTimestamp) {
|
||||
final StringBuilder sb = new StringBuilder();
|
||||
if (videoId.isEmpty()) {
|
||||
sb.append(getPlaylistUrl(playlistId));
|
||||
} else {
|
||||
sb.append(VIDEO_URL);
|
||||
sb.append(videoId);
|
||||
sb.append("?list=");
|
||||
sb.append(playlistId);
|
||||
if (withTimestamp) {
|
||||
final long currentVideoTimeInSeconds = VideoInformation.getVideoTimeInSeconds();
|
||||
if (currentVideoTimeInSeconds > 0) {
|
||||
sb.append("&t=");
|
||||
sb.append(currentVideoTimeInSeconds);
|
||||
}
|
||||
}
|
||||
}
|
||||
launchView(sb.toString(), getContext().getPackageName());
|
||||
}
|
||||
|
||||
public static void openVideo() {
|
||||
openVideo(VideoInformation.getVideoId());
|
||||
}
|
||||
@ -177,8 +205,8 @@ public class VideoUtils extends IntentUtils {
|
||||
}
|
||||
|
||||
public static void showPlaybackSpeedDialog(@NonNull Context context) {
|
||||
final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedListEntries();
|
||||
final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedListEntryValues();
|
||||
final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedEntries();
|
||||
final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedEntryValues();
|
||||
|
||||
final float playbackSpeed = VideoInformation.getPlaybackSpeed();
|
||||
final int index = Arrays.binarySearch(playbackSpeedEntryValues, String.valueOf(playbackSpeed));
|
||||
@ -186,6 +214,7 @@ public class VideoUtils extends IntentUtils {
|
||||
new AlertDialog.Builder(context)
|
||||
.setSingleChoiceItems(playbackSpeedEntries, index, (mDialog, mIndex) -> {
|
||||
final float selectedPlaybackSpeed = Float.parseFloat(playbackSpeedEntryValues[mIndex] + "f");
|
||||
VideoInformation.setPlaybackSpeed(selectedPlaybackSpeed);
|
||||
VideoInformation.overridePlaybackSpeed(selectedPlaybackSpeed);
|
||||
userSelectedPlaybackSpeed(selectedPlaybackSpeed);
|
||||
mDialog.dismiss();
|
||||
@ -252,6 +281,13 @@ public class VideoUtils extends IntentUtils {
|
||||
return !isExternalDownloaderLaunched.get() && original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rest of the implementation added by patch.
|
||||
*/
|
||||
public static void dismissPlayer() {
|
||||
Logger.printDebug(() -> "Dismiss player");
|
||||
}
|
||||
|
||||
/**
|
||||
* Rest of the implementation added by patch.
|
||||
*/
|
||||
|
@ -1,7 +1,9 @@
|
||||
package com.google.android.apps.youtube.app.settings.videoquality;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
@ -25,8 +27,8 @@ import app.revanced.extension.youtube.utils.ThemeUtils;
|
||||
@SuppressWarnings("deprecation")
|
||||
public class VideoQualitySettingsActivity extends Activity {
|
||||
|
||||
private static final String rvxSettingsLabel = ResourceUtils.getString("revanced_extended_settings_title");
|
||||
private static final String searchLabel = ResourceUtils.getString("revanced_extended_settings_search_title");
|
||||
private static String rvxSettingsLabel;
|
||||
private static String searchLabel;
|
||||
private static WeakReference<SearchView> searchViewRef = new WeakReference<>(null);
|
||||
private static WeakReference<ImageView> closeButtonRef = new WeakReference<>(null);
|
||||
private ReVancedPreferenceFragment fragment;
|
||||
@ -71,6 +73,10 @@ public class VideoQualitySettingsActivity extends Activity {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set label
|
||||
rvxSettingsLabel = getString("revanced_extended_settings_title");
|
||||
searchLabel = getString("revanced_extended_settings_search_title");
|
||||
|
||||
// Set toolbar
|
||||
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) {
|
||||
if (fragment == null) return;
|
||||
fragment.filterPreferences(query);
|
||||
|
@ -4,5 +4,5 @@ org.gradle.parallel = true
|
||||
android.useAndroidX = true
|
||||
kotlin.code.style = official
|
||||
kotlin.jvm.target.validation.mode = IGNORE
|
||||
version = 5.5.1
|
||||
version = 5.6.2
|
||||
|
||||
|
@ -6,12 +6,14 @@ smali = "3.0.5"
|
||||
gson = "2.12.1"
|
||||
agp = "8.2.2"
|
||||
annotation = "1.9.1"
|
||||
collections4 = "4.5.0-M3"
|
||||
lang3 = "3.17.0"
|
||||
preference = "1.2.1"
|
||||
|
||||
[libraries]
|
||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
||||
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" }
|
||||
preference = { module = "androidx.preference:preference", version.ref = "preference" }
|
||||
|
||||
|
748
patches.json
748
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 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 fun getDefinedClassName ()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 getButtonContainer ()J
|
||||
public static final fun getButtonIconPaddingMedium ()J
|
||||
public static final fun getChannelHandle ()J
|
||||
public static final fun getChipCloud ()J
|
||||
public static final fun getColorGrey ()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 getQualityAuto ()J
|
||||
public static final fun getRemixGenericButtonSize ()J
|
||||
public static final fun getSearchButton ()J
|
||||
public static final fun getSlidingDialogAnimation ()J
|
||||
public static final fun getTapBloomView ()J
|
||||
public static final fun getText1 ()J
|
||||
@ -453,7 +459,7 @@ public final class app/revanced/patches/reddit/utils/extension/SharedExtensionPa
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatchKt {
|
||||
public static final fun getScreenShotShareBanner ()J
|
||||
public static final fun getNsfwDialogTitle ()J
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/reddit/utils/settings/SettingsPatchKt {
|
||||
@ -550,10 +556,9 @@ public final class app/revanced/patches/shared/mapping/ResourceElement {
|
||||
}
|
||||
|
||||
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 get (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)J
|
||||
public static final fun getResourceId (Lapp/revanced/patches/shared/mapping/ResourceType;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 getResourceMappings ()Ljava/util/List;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/shared/mapping/ResourceType : java/lang/Enum {
|
||||
@ -715,6 +720,10 @@ public final class app/revanced/patches/youtube/general/toolbar/ToolBarComponent
|
||||
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 static final fun getShortsActionButtonsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
@ -866,6 +875,7 @@ public final class app/revanced/patches/youtube/player/seekbar/SeekbarComponents
|
||||
|
||||
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 indexOfDismissInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/shorts/components/ShortsComponentPatchKt {
|
||||
@ -885,6 +895,10 @@ public final class app/revanced/patches/youtube/utils/FingerprintsKt {
|
||||
public static final fun indexOfSpannedCharSequenceInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/utils/auth/AuthHookPatchKt {
|
||||
public static final fun getAuthHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/utils/bottomsheet/BottomSheetHookPatchKt {
|
||||
public static final fun getBottomSheetHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@ -897,6 +911,10 @@ public final class app/revanced/patches/youtube/utils/controlsoverlay/ControlsOv
|
||||
public static final fun getControlsOverlayConfigPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/utils/dismiss/DismissPlayerHookPatchKt {
|
||||
public static final fun getDismissPlayerHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/utils/engagement/EngagementPanelHookPatchKt {
|
||||
public static final fun getEngagementPanelHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@ -1013,6 +1031,10 @@ public final class app/revanced/patches/youtube/utils/playertype/PlayerTypeHookP
|
||||
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 static final fun getVersionCheckPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
public static final fun is_18_31_or_greater ()Z
|
||||
@ -1020,12 +1042,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_42_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_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_11_or_greater ()Z
|
||||
public static final fun is_19_15_or_greater ()Z
|
||||
public static final fun is_19_16_or_greater ()Z
|
||||
public static final fun is_19_17_or_greater ()Z
|
||||
public static final fun is_19_18_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_26_or_greater ()Z
|
||||
@ -1036,14 +1062,18 @@ public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPa
|
||||
public static final fun is_19_34_or_greater ()Z
|
||||
public static final fun is_19_36_or_greater ()Z
|
||||
public static final fun is_19_41_or_greater ()Z
|
||||
public static final fun is_19_42_or_greater ()Z
|
||||
public static final fun is_19_43_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_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_03_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_12_or_greater ()Z
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/utils/recyclerview/RecyclerViewTreeObserverPatchKt {
|
||||
@ -1145,7 +1175,6 @@ public final class app/revanced/patches/youtube/utils/resourceid/SharedResourceI
|
||||
public static final fun getRelatedChipCloudMargin ()J
|
||||
public static final fun getRightComment ()J
|
||||
public static final fun getScrimOverlay ()J
|
||||
public static final fun getScrubbing ()J
|
||||
public static final fun getSeekEasyHorizontalTouchOffsetToStartScrubbing ()J
|
||||
public static final fun getSeekUndoEduOverlayStub ()J
|
||||
public static final fun getSettingsFragment ()J
|
||||
@ -1160,13 +1189,17 @@ public final class app/revanced/patches/youtube/utils/resourceid/SharedResourceI
|
||||
public static final fun getTotalTime ()J
|
||||
public static final fun getTouchArea ()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 getVideoQualityUnavailableAnnouncement ()J
|
||||
public static final fun getVideoZoomSnapIndicator ()J
|
||||
public static final fun getVoiceSearch ()J
|
||||
public static final fun getYouTubeControlsOverlaySubtitleButton ()J
|
||||
public static final fun getYouTubeLogo ()J
|
||||
public static final fun getYtCallToAction ()J
|
||||
public static final fun getYtFillBell ()J
|
||||
public static final fun getYtOutlineLibrary ()J
|
||||
public static final fun getYtOutlineMoonZ ()J
|
||||
public static final fun getYtOutlinePictureInPictureWhite ()J
|
||||
public static final fun getYtOutlineVideoCamera ()J
|
||||
|
@ -152,7 +152,10 @@ private enum class MethodCall(
|
||||
RegisterNetworkCallback1(
|
||||
"Landroid/net/ConnectivityManager;",
|
||||
"registerNetworkCallback",
|
||||
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"),
|
||||
arrayOf(
|
||||
"Landroid/net/NetworkRequest;",
|
||||
"Landroid/net/ConnectivityManager\$NetworkCallback;"
|
||||
),
|
||||
"V",
|
||||
),
|
||||
RegisterNetworkCallback2(
|
||||
@ -174,13 +177,20 @@ private enum class MethodCall(
|
||||
RequestNetwork1(
|
||||
"Landroid/net/ConnectivityManager;",
|
||||
"requestNetwork",
|
||||
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"),
|
||||
arrayOf(
|
||||
"Landroid/net/NetworkRequest;",
|
||||
"Landroid/net/ConnectivityManager\$NetworkCallback;"
|
||||
),
|
||||
"V",
|
||||
),
|
||||
RequestNetwork2(
|
||||
"Landroid/net/ConnectivityManager;",
|
||||
"requestNetwork",
|
||||
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;", "I"),
|
||||
arrayOf(
|
||||
"Landroid/net/NetworkRequest;",
|
||||
"Landroid/net/ConnectivityManager\$NetworkCallback;",
|
||||
"I"
|
||||
),
|
||||
"V",
|
||||
),
|
||||
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.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
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.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.settings.CategoryType
|
||||
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
|
||||
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
|
||||
import app.revanced.patches.music.utils.settings.addSwitchPreference
|
||||
import app.revanced.patches.music.utils.settings.settingsPatch
|
||||
import app.revanced.util.fingerprint.matchOrThrow
|
||||
import app.revanced.util.fingerprint.methodOrThrow
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
|
||||
@Suppress("unused")
|
||||
@ -84,17 +88,50 @@ val accountComponentsPatch = bytecodePatch(
|
||||
}
|
||||
|
||||
// account switcher
|
||||
namesInactiveAccountThumbnailSizeFingerprint.matchOrThrow().let {
|
||||
it.method.apply {
|
||||
val targetIndex = it.patternMatch!!.startIndex
|
||||
val targetRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA
|
||||
val textViewField = with(
|
||||
channelHandleFingerprint
|
||||
.methodOrThrow(namesInactiveAccountThumbnailSizeFingerprint)
|
||||
) {
|
||||
val literalIndex = indexOfFirstLiteralInstructionOrThrow(channelHandle)
|
||||
getInstruction(
|
||||
indexOfFirstInstructionOrThrow(literalIndex) {
|
||||
opcode == Opcode.IPUT_OBJECT &&
|
||||
getReference<FieldReference>()?.type == "Landroid/widget/TextView;"
|
||||
},
|
||||
).getReference<FieldReference>()
|
||||
}
|
||||
|
||||
addInstructions(
|
||||
targetIndex, """
|
||||
invoke-static {v$targetRegister}, $ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Z)Z
|
||||
move-result v$targetRegister
|
||||
"""
|
||||
)
|
||||
namesInactiveAccountThumbnailSizeFingerprint.methodOrThrow().apply {
|
||||
var hook = false
|
||||
|
||||
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
|
||||
|
||||
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.namesInactiveAccountThumbnailSize
|
||||
import app.revanced.patches.music.utils.resourceid.tosFooter
|
||||
import app.revanced.util.fingerprint.legacyFingerprint
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
|
||||
internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
|
||||
name = "accountSwitcherAccessibilityLabelFingerprint",
|
||||
@ -14,6 +14,12 @@ internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
|
||||
literals = listOf(accountSwitcherAccessibility)
|
||||
)
|
||||
|
||||
internal val channelHandleFingerprint = legacyFingerprint(
|
||||
name = "channelHandleFingerprint",
|
||||
returnType = "V",
|
||||
literals = listOf(channelHandle),
|
||||
)
|
||||
|
||||
internal val menuEntryFingerprint = legacyFingerprint(
|
||||
name = "menuEntryFingerprint",
|
||||
returnType = "V",
|
||||
@ -24,19 +30,6 @@ internal val namesInactiveAccountThumbnailSizeFingerprint = legacyFingerprint(
|
||||
name = "namesInactiveAccountThumbnailSizeFingerprint",
|
||||
returnType = "V",
|
||||
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)
|
||||
)
|
||||
|
||||
|
@ -115,7 +115,8 @@ val adsPatch = bytecodePatch(
|
||||
.methodOrThrow(getPremiumDialogParentFingerprint)
|
||||
.apply {
|
||||
val setContentViewIndex = indexOfSetContentViewInstruction(this)
|
||||
val dialogInstruction = getInstruction<FiveRegisterInstruction>(setContentViewIndex)
|
||||
val dialogInstruction =
|
||||
getInstruction<FiveRegisterInstruction>(setContentViewIndex)
|
||||
val dialogRegister = dialogInstruction.registerC
|
||||
val viewRegister = dialogInstruction.registerD
|
||||
|
||||
|
@ -97,8 +97,6 @@ internal val showDialogCommandFingerprint = legacyFingerprint(
|
||||
name = "showDialogCommandFingerprint",
|
||||
returnType = "V",
|
||||
opcodes = listOf(
|
||||
Opcode.IF_EQ,
|
||||
Opcode.IGET_OBJECT,
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
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.offlineSettingsMenuItem
|
||||
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.topBarMenuItemImageView
|
||||
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(
|
||||
name = "searchBarFingerprint",
|
||||
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.resourceid.musicTasteBuilderShelf
|
||||
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.topBarMenuItemImageView
|
||||
import app.revanced.patches.music.utils.settings.CategoryType
|
||||
@ -198,6 +199,23 @@ val layoutComponentsPatch = bytecodePatch(
|
||||
|
||||
// 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
|
||||
|
||||
if (is_6_48_or_greater) {
|
||||
@ -353,6 +371,11 @@ val layoutComponentsPatch = bytecodePatch(
|
||||
"revanced_hide_samples_shelf",
|
||||
"false"
|
||||
)
|
||||
addSwitchPreference(
|
||||
CategoryType.GENERAL,
|
||||
"revanced_hide_search_button",
|
||||
"false"
|
||||
)
|
||||
if (is_6_48_or_greater) {
|
||||
addSwitchPreference(
|
||||
CategoryType.GENERAL,
|
||||
|
@ -32,25 +32,32 @@ private val spoofAppVersionBytecodePatch = bytecodePatch(
|
||||
)
|
||||
|
||||
execute {
|
||||
if (!is_6_43_or_greater || is_7_25_or_greater) {
|
||||
if (!is_6_43_or_greater) {
|
||||
return@execute
|
||||
}
|
||||
if (is_7_17_or_greater) {
|
||||
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
|
||||
name == "SpoofAppVersionDefaultString"
|
||||
}.replaceInstruction(
|
||||
0,
|
||||
"const-string v0, \"7.16.53\""
|
||||
)
|
||||
var defaultVersionString = "6.42.55"
|
||||
|
||||
if (is_7_17_or_greater && !is_7_25_or_greater) {
|
||||
defaultVersionString = "7.16.53"
|
||||
defaultValue = "true"
|
||||
|
||||
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
|
||||
name == "SpoofAppVersionDefaultBoolean"
|
||||
}.replaceInstruction(
|
||||
0,
|
||||
"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(
|
||||
"6.51.53",
|
||||
"7.16.53",
|
||||
"7.25.53",
|
||||
"8.05.51",
|
||||
"8.10.52",
|
||||
),
|
||||
)
|
||||
|
||||
@ -73,13 +83,19 @@ val spoofAppVersionPatch = resourcePatch(
|
||||
)
|
||||
|
||||
execute {
|
||||
if (!is_6_43_or_greater || is_7_25_or_greater) {
|
||||
printWarn("\"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 6.43.53 ~ 7.24.51.")
|
||||
if (!is_6_43_or_greater) {
|
||||
printWarn("\"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 6.51.53 or later.")
|
||||
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")
|
||||
}
|
||||
if (is_7_25_or_greater) {
|
||||
appendAppVersion("7.17.52")
|
||||
}
|
||||
|
||||
addSwitchPreference(
|
||||
CategoryType.GENERAL,
|
||||
|
@ -206,7 +206,8 @@ val changeHeaderPatch = resourcePatch(
|
||||
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) {
|
||||
document("res/layout/signin_fragment.xml").use { document ->
|
||||
document.doRecursively node@{ node ->
|
||||
|
@ -24,7 +24,7 @@ import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import app.revanced.util.indexOfFirstInstructionReversedOrThrow
|
||||
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
|
||||
private const val EXTENSION_METHOD_DESCRIPTOR =
|
||||
@ -41,7 +41,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
|
||||
"7.16.53",
|
||||
"7.25.53",
|
||||
"8.05.51",
|
||||
"8.10.51",
|
||||
"8.12.53",
|
||||
),
|
||||
)
|
||||
|
||||
@ -57,7 +57,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
|
||||
return@execute
|
||||
} else if (!is_7_20_or_greater) {
|
||||
cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall(
|
||||
45635386L,
|
||||
CAIRO_SPLASH_ANIMATION_FEATURE_FLAG,
|
||||
EXTENSION_METHOD_DESCRIPTOR
|
||||
)
|
||||
} else {
|
||||
@ -69,18 +69,13 @@ val cairoSplashAnimationPatch = bytecodePatch(
|
||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||
getReference<MethodReference>()?.name == "setContentView"
|
||||
} + 1
|
||||
val viewStubFindViewByIdIndex = indexOfFirstInstructionOrThrow(literalIndex) {
|
||||
val reference = getReference<MethodReference>()
|
||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||
reference?.name == "findViewById" &&
|
||||
reference.definingClass != "Landroid/view/View;"
|
||||
}
|
||||
val freeIndex = indexOfFirstInstructionOrThrow(insertIndex, Opcode.CONST)
|
||||
val freeRegister =
|
||||
getInstruction<FiveRegisterInstruction>(viewStubFindViewByIdIndex).registerD
|
||||
val jumpIndex = indexOfFirstInstructionReversedOrThrow(
|
||||
viewStubFindViewByIdIndex,
|
||||
Opcode.IGET_OBJECT
|
||||
)
|
||||
getInstruction<OneRegisterInstruction>(freeIndex).registerA
|
||||
val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex) {
|
||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||
getReference<MethodReference>()?.parameterTypes?.firstOrNull() == "Ljava/lang/Runnable;"
|
||||
} + 1
|
||||
|
||||
addInstructionsWithLabels(
|
||||
insertIndex, """
|
||||
|
@ -5,6 +5,8 @@ import app.revanced.patches.music.utils.resourceid.mainActivityLaunchAnimation
|
||||
import app.revanced.util.fingerprint.legacyFingerprint
|
||||
import app.revanced.util.indexOfFirstLiteralInstruction
|
||||
|
||||
internal const val CAIRO_SPLASH_ANIMATION_FEATURE_FLAG = 45635386L
|
||||
|
||||
/**
|
||||
* This fingerprint is compatible with YouTube Music v7.06.53+
|
||||
*/
|
||||
@ -20,7 +22,7 @@ internal val cairoSplashAnimationConfigFingerprint = legacyFingerprint(
|
||||
if (is_7_20_or_greater) {
|
||||
method.indexOfFirstLiteralInstruction(mainActivityLaunchAnimation) >= 0
|
||||
} else {
|
||||
method.indexOfFirstLiteralInstruction(45635386) >= 0
|
||||
method.indexOfFirstLiteralInstruction(CAIRO_SPLASH_ANIMATION_FEATURE_FLAG) >= 0
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -1,13 +1,13 @@
|
||||
package app.revanced.patches.music.misc.watchhistory
|
||||
|
||||
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.patch.PatchList.WATCH_HISTORY
|
||||
import app.revanced.patches.music.utils.settings.CategoryType
|
||||
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
|
||||
import app.revanced.patches.music.utils.settings.settingsPatch
|
||||
import app.revanced.patches.shared.trackingurlhook.hookWatchHistory
|
||||
import app.revanced.patches.shared.trackingurlhook.trackingUrlHookPatch
|
||||
|
||||
@Suppress("unused")
|
||||
val watchHistoryPatch = bytecodePatch(
|
||||
|
@ -124,14 +124,17 @@ val navigationBarComponentsPatch = bytecodePatch(
|
||||
opcode == Opcode.IGET_OBJECT &&
|
||||
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 componentIndex = indexOfFirstInstructionOrThrow(stringIndex) {
|
||||
opcode == Opcode.IGET_OBJECT &&
|
||||
getReference<FieldReference>()?.toString() == browseIdReference.toString()
|
||||
}
|
||||
val browseIdRegister = getInstruction<TwoRegisterInstruction>(componentIndex).registerA
|
||||
val componentRegister = getInstruction<TwoRegisterInstruction>(componentIndex).registerB
|
||||
val browseIdRegister =
|
||||
getInstruction<TwoRegisterInstruction>(componentIndex).registerA
|
||||
val componentRegister =
|
||||
getInstruction<TwoRegisterInstruction>(componentIndex).registerB
|
||||
|
||||
val enumIndex = it.patternMatch!!.startIndex + 3
|
||||
val enumRegister = getInstruction<OneRegisterInstruction>(enumIndex).registerA
|
||||
|
@ -54,7 +54,7 @@ internal val engagementPanelHeightFingerprint = legacyFingerprint(
|
||||
parameters = emptyList(),
|
||||
customFingerprint = { method, _ ->
|
||||
AccessFlags.FINAL.isSet(method.accessFlags) &&
|
||||
method.containsLiteralInstruction(1) &&
|
||||
method.containsLiteralInstruction(1) &&
|
||||
method.indexOfFirstInstruction {
|
||||
opcode == Opcode.INVOKE_VIRTUAL &&
|
||||
getReference<MethodReference>()?.name == "booleanValue"
|
||||
|
@ -746,15 +746,22 @@ val playerComponentsPatch = bytecodePatch(
|
||||
val freeRegister =
|
||||
getInstruction<FiveRegisterInstruction>(bottomSheetBehaviorIndex).registerD
|
||||
|
||||
val getFieldIndex = bottomSheetBehaviorIndex - 2
|
||||
val getFieldReference =
|
||||
getInstruction<ReferenceInstruction>(getFieldIndex).reference
|
||||
val getFieldInstruction = getInstruction<TwoRegisterInstruction>(getFieldIndex)
|
||||
|
||||
addInstructionsWithLabels(
|
||||
bottomSheetBehaviorIndex - 2,
|
||||
getFieldIndex + 1,
|
||||
"""
|
||||
invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer()Z
|
||||
move-result v$freeRegister
|
||||
if-nez v$freeRegister, :dismiss
|
||||
iget-object v${getFieldInstruction.registerA}, v${getFieldInstruction.registerB}, $getFieldReference
|
||||
""",
|
||||
ExternalLabel("dismiss", getInstruction(bottomSheetBehaviorIndex + 1))
|
||||
)
|
||||
removeInstruction(getFieldIndex)
|
||||
} ?: 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.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.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
|
||||
|
||||
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
|
||||
|
||||
val sharedExtensionPatch = sharedExtensionPatch(
|
||||
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.addSwitchPreference
|
||||
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.customspeed.customPlaybackSpeedPatch
|
||||
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.indexOfModelInstruction
|
||||
import app.revanced.patches.shared.indexOfReleaseInstruction
|
||||
import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
|
||||
import app.revanced.util.findMethodOrThrow
|
||||
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
|
||||
import app.revanced.util.fingerprint.matchOrThrow
|
||||
@ -63,9 +63,11 @@ private const val CLIENT_INFO_CLASS_DESCRIPTOR =
|
||||
|
||||
@Suppress("unused")
|
||||
val spoofClientPatch = bytecodePatch(
|
||||
SPOOF_CLIENT.title,
|
||||
SPOOF_CLIENT.summary,
|
||||
false,
|
||||
// Removed from the patch list to avoid user confusion:
|
||||
// https://github.com/inotia00/ReVanced_Extended/issues/2832#issuecomment-2745941171
|
||||
// SPOOF_CLIENT.title,
|
||||
// SPOOF_CLIENT.summary,
|
||||
// false,
|
||||
) {
|
||||
compatibleWith(COMPATIBLE_PACKAGE)
|
||||
|
||||
|
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