Compare commits

...

105 Commits

Author SHA1 Message Date
inotia00
42cc8201f5 Merge branch 'dev' into revanced-extended 2025-04-03 10:23:04 +09:00
inotia00
15d0fafcf8 bump 5.6.2 2025-04-03 10:20:34 +09:00
inotia00
db10e410d3 feat(Translations): Update translation 2025-04-03 10:20:10 +09:00
inotia00
8f23d76f37 feat(Reddit): Change the latest supported version from 2025.12.0 to 2025.12.1 2025-04-03 10:19:36 +09:00
inotia00
7168e49121 fix(Reddit - Disable screenshot popup): No exception is thrown if no matching pattern is found 2025-04-03 10:18:02 +09:00
inotia00
2b52380294 bump 5.6.2-dev.2 2025-04-01 19:12:45 +09:00
inotia00
0cc693961a feat(Translations): Update translation 2025-04-01 19:11:59 +09:00
inotia00
bbf863c630 fix(YouTube - Description components): Expand video descriptions requires longer wait time 2025-04-01 19:11:45 +09:00
inotia00
783e366242 fix(YouTube - Hook download actions, Overlay buttons): Auth key is set before the Context is initialized 2025-04-01 19:09:19 +09:00
inotia00
4a19a960c5 fix(YouTube - Settings): The default value for the patch option is incorrect 2025-04-01 19:06:28 +09:00
inotia00
e55fd4eb74 chore: Remove debug instruction 2025-04-01 19:05:20 +09:00
inotia00
64af7fd8b6 fix(YouTube - Video playback): Overridden to default playback speed in onResume callback https://github.com/inotia00/ReVanced_Extended/issues/2896 2025-04-01 19:02:55 +09:00
inotia00
457dbfec6c feat(YouTube - Disable resuming Shorts on startup): Match with ReVanced 2025-04-01 19:01:18 +09:00
inotia00
22b98336d5 fix(YouTube - Custom branding icon): Splash animation background color is always white https://github.com/inotia00/ReVanced_Extended/issues/2892 2025-04-01 19:00:18 +09:00
inotia00
7e4a71b385 feat(GmsCore support): Add patch option Patch all manifest components 2025-04-01 18:59:47 +09:00
inotia00
bb5964ce98 fix(Reddit - Disable screenshot popup): Screenshot popup not being completely removed https://github.com/inotia00/ReVanced_Extended/issues/1810 2025-04-01 18:51:58 +09:00
inotia00
15db05c636 fix(Reddit - Remove subreddit dialog): Can't open post from search results 2025-04-01 18:49:47 +09:00
KobeW50
3f9edca15d
feat(YouTube - Settings): Place RVX settings at top of settings by default (#147)
Co-authored-by: Kobe <kobew5050@gmail.com>
2025-04-01 08:50:25 +09:00
inotia00
1eca8c854c bump 5.6.2-dev.1 2025-03-31 21:21:14 +09:00
inotia00
692b4f2c53 feat(Translations): Update translation 2025-03-31 21:19:33 +09:00
inotia00
5162dccecc feat(YouTube - Settings): Add App name, App version and Patched date to patch information 2025-03-31 21:16:29 +09:00
inotia00
56b713b0db fix build error 2025-03-31 21:13:36 +09:00
inotia00
f761bc45ec feat(YouTube - Hook download actions, Overlay buttons): Add Add to queue and reload video and Remove from queue and reload video settings, fixed a bug where the queue was unavailable under certain conditions 2025-03-31 21:12:59 +09:00
inotia00
bccd6dc5df fix(YouTube - Swipe controls): Patch fails on YouTube 19.43.41 when Player components patch is included 2025-03-31 21:09:11 +09:00
inotia00
09a8eb7114 fix(YouTube - Custom branding icon): Splash animation background color is always white https://github.com/inotia00/ReVanced_Extended/issues/2892 2025-03-31 21:05:06 +09:00
inotia00
b439ef3ee7 fix(Reddit - Remove subreddit dialog): New type of NSFW dialog not hidden https://github.com/inotia00/ReVanced_Extended/issues/2895 2025-03-31 21:01:10 +09:00
inotia00
7f85e802c2 fix(Reddit - Disable screenshot popup): Patch does not work in certain environments https://github.com/inotia00/ReVanced_Extended/issues/2891 2025-03-31 20:54:10 +09:00
inotia00
bb1498df76 Merge branch 'dev' into revanced-extended 2025-03-30 19:49:58 +09:00
inotia00
1c58c6a36e feat(YouTube): Remove support version 20.03.43
Not reflected in branch due to issue with source comparison tool
2025-03-30 19:49:43 +09:00
inotia00
26bf2f4b82 Merge branch 'dev' into revanced-extended 2025-03-30 19:33:08 +09:00
inotia00
a037b25c13 bump 5.6.1 2025-03-30 19:32:31 +09:00
inotia00
b72bb71e30 feat(YouTube): Remove support version 20.03.43 2025-03-30 19:27:16 +09:00
inotia00
28ff781786 chore: Lint code 2025-03-30 19:13:14 +09:00
inotia00
f249e88ce8 bump 5.6.1-dev.5 2025-03-30 18:24:50 +09:00
inotia00
036e3dad11 feat(Translations): Update translation 2025-03-30 18:24:10 +09:00
inotia00
9342e1f7d2 fix(Reddit - Disable screenshot popup): Restart dialog is missing 2025-03-30 18:23:49 +09:00
inotia00
f19dd54026 fix(YouTube - Hook download actions, Overlay buttons): Queue manager fails to identify brand account 2025-03-30 18:22:29 +09:00
inotia00
edccd61e6b fix(YouTube - Custom branding icon): Remove unnecessary hooks 2025-03-30 18:20:02 +09:00
inotia00
35e6c26823 fix(Extensions): Remove unnecessary Context hooks 2025-03-30 18:18:46 +09:00
inotia00
88d59d05b9 fix(YouTube - Shorts components): Custom actions do not override Shorts flyout menu in YouTube 19.05.36 2025-03-30 18:16:34 +09:00
inotia00
aac38dc8af fix(YouTube - Settings): RVX language no longer changes the YouTube app language 2025-03-30 18:12:07 +09:00
inotia00
1dd7eda606 chore(YouTube): Reflecting the changes in ReVanced 2025-03-30 18:08:49 +09:00
MondayNitro
05195caa5a
feat(YouTube - Custom branding icon): Update old splash animation background color (#146)
* Update revanced_extended_settings_key_icon.xml

* Update $avd_anim__0.xml

* Update strings.xml
2025-03-30 18:05:53 +09:00
inotia00
82158deaf2 chore(YouTube - Fullscreen components): Remove warning for Keep landscape mode setting 2025-03-30 18:01:01 +09:00
inotia00
21be41c2a9 fix(Reddit - Disable screenshot popup): Screenshot popup not being completely removed https://github.com/inotia00/ReVanced_Extended/issues/1810 2025-03-30 17:56:59 +09:00
inotia00
56d2f7c4ad bump 5.6.1-dev.4 2025-03-29 16:58:52 +09:00
inotia00
aca0591575 feat(Translations): Update translation 2025-03-29 16:58:04 +09:00
inotia00
274e10aabc fix: add missing dependency 2025-03-29 16:57:21 +09:00
inotia00
4330b7f6df fix(Reddit - Hide ads): Hide comment ads does not work https://github.com/inotia00/ReVanced_Extended/issues/2884 2025-03-29 16:56:21 +09:00
inotia00
4feff6b150 feat(Reddit): Restore support version 2025.05.1 2025-03-29 16:55:41 +09:00
inotia00
fbf19ee78b feat(YouTube): Change the latest supported version from 20.03.45 to 20.03.43 https://github.com/inotia00/ReVanced_Extended/issues/2885#issuecomment-2763077766 2025-03-29 16:54:24 +09:00
inotia00
e157e9447d fix(YouTube): Playback speed sometimes changes to 1.0x in Shorts (Unpatched YouTube bug) 2025-03-29 16:52:45 +09:00
inotia00
79f933dad4 fix(YouTube - Remove background playback restrictions): Media controls appear in the status bar when playing Shorts from the feed 2025-03-29 16:51:35 +09:00
inotia00
b8f3917b55 chore(YouTube): Integrate methods related to Shorts state into RootView 2025-03-29 16:46:28 +09:00
inotia00
758e8ac568 fix(YouTube - Video playback): Update descriptions 2025-03-28 20:29:24 +09:00
inotia00
b8b61fdf51 fix typo 2025-03-28 20:28:07 +09:00
inotia00
34e482b03e fix(YouTube - Video playback): Update descriptions 2025-03-28 20:26:12 +09:00
inotia00
481a310d06 fix typo 2025-03-28 20:25:24 +09:00
inotia00
6818df4507 bump 5.6.1-dev.3 2025-03-28 19:48:38 +09:00
inotia00
af26cd58a8 feat(Translations): Update translation 2025-03-28 19:48:00 +09:00
inotia00
3e8c748f48 feat(YouTube): Add support version 20.03.45 https://github.com/inotia00/ReVanced_Extended/issues/2717#issuecomment-2760796211 2025-03-28 19:47:08 +09:00
inotia00
f0b1155d20 fix(YouTube - Change form factor): No user dialog shown when changing settings 2025-03-28 19:36:39 +09:00
inotia00
bf9ba0b1ef refactor(YouTube - Video playback): Add support for Shorts..
- Add 'Speed dialog' setting to Shorts custom actions
- Add default quality settings and default playback speed settings for Shorts
- Remove deprecated settings - 'Reject software AV1 codec response', 'Enable Shorts default playback speed'
- Reorder video categories
2025-03-28 19:31:25 +09:00
inotia00
82ceb8aa76 fix(YouTube Music - Disable music video in album): The redirect wait time may be too short. 2025-03-28 19:21:17 +09:00
inotia00
4f911d9a55 fix(YouTube - Spoof streaming data): No toast message is shown even if fetch fails 2025-03-28 19:16:21 +09:00
inotia00
a235c84e19 feat(Reddit): Add support version 2025.12.0, drop support version 2025.05.1 2025-03-28 19:14:17 +09:00
inotia00
98d362ad93 feat(YouTube Music): Add support version 8.12.53, drop support version 8.10.52 2025-03-28 19:12:18 +09:00
inotia00
8169ccacc2 refactor: Use map instead of list to lookup resource ids 2025-03-28 19:09:31 +09:00
inotia00
f3abc04812 bump 5.6.1-dev.2 2025-03-26 21:33:40 +09:00
inotia00
5446847f5f feat(Translations): Update translation 2025-03-26 21:33:05 +09:00
inotia00
29ba8f7a7d feat(YouTube Music - Spoof app version): Add target version 7.17.52 2025-03-26 21:27:39 +09:00
inotia00
0639b559b1 fix(YouTube - Spoof streaming data): Add patch option Use iOS client and add user dialog for warning 2025-03-26 21:03:35 +09:00
inotia00
dcb72cc803 feat(YouTube Music - Custom branding icon): Update afn icons https://github.com/inotia00/ReVanced_Extended/issues/2866 2025-03-26 20:45:45 +09:00
inotia00
d4ad05d4ba fix(YouTube - Theme): Change method to fix dark theme in YouTube 19.32+ 2025-03-26 20:34:30 +09:00
inotia00
451a14a74d feat(YouTube - Swipe controls): Add Swipe overlay alternative UI setting (Closes https://github.com/inotia00/ReVanced_Extended/issues/2828) 2025-03-26 18:58:02 +09:00
inotia00
99fa969857 fix(YouTube - SponsorBlock): Dependencies for some settings are not set 2025-03-26 15:24:58 +09:00
inotia00
efead108f9 feat(YouTube - SponsorBlock): Add opacity setting to category segment colors 2025-03-26 15:11:06 +09:00
inotia00
9d37e31a24 chore: Lint code 2025-03-26 15:08:28 +09:00
inotia00
a764e3aea3 fix(YouTube - Shorts components): Pause option of Change Shorts repeat state not working (20.09+) 2025-03-26 15:05:39 +09:00
inotia00
0bfabbc384 fix(YouTube - Shorts components): Shorts player automatically goes to next Short (Closes https://github.com/inotia00/ReVanced_Extended/issues/2873) 2025-03-26 15:03:55 +09:00
inotia00
cf90f7b94e fix(YouTube - Hook download actions, Overlay buttons): Sometimes the AlertDialog does not show 2025-03-26 15:02:02 +09:00
inotia00
df41f035b6 fix(YouTube - Theme): Change method to fix dark theme in YouTube 19.32+ 2025-03-26 15:00:26 +09:00
inotia00
5c743c299a feat(YouTube Music): Change the latest supported version from 8.10.51 to 8.10.52 2025-03-26 14:57:32 +09:00
inotia00
6ac6fcf953 feat(YouTube - Hide layout components): Change default value of Disable translucent status bar setting and move it to experimental flag 2025-03-26 14:55:48 +09:00
inotia00
123082b676 refactor(InnterTube): Move classes to the appropriate path 2025-03-26 12:31:03 +09:00
inotia00
ae69aca189 Merge branch 'dev' into revanced-extended 2025-03-16 13:07:53 +09:00
inotia00
8d84304737 Merge branch 'dev' into revanced-extended 2025-03-07 09:50:18 +09:00
inotia00
1926becdfc Merge branch 'dev' into revanced-extended 2025-02-13 11:21:24 +09:00
inotia00
9146d9283b Merge branch 'dev' into revanced-extended 2025-01-22 17:23:55 +09:00
inotia00
8db38af44f Merge branch 'dev' into revanced-extended 2025-01-16 12:43:34 +09:00
inotia00
1aaf03df50 Merge branch 'dev' into revanced-extended 2025-01-07 14:16:07 +09:00
inotia00
ee0a7b5443 Merge branch 'dev' into revanced-extended 2024-12-22 19:46:21 +09:00
inotia00
a9408b0f51 Merge branch 'dev' into revanced-extended 2024-12-22 18:25:46 +09:00
inotia00
03c0826fab Merge branch 'dev' into revanced-extended 2024-12-22 16:54:00 +09:00
inotia00
0384907324 Merge branch 'dev' into revanced-extended 2024-12-21 15:10:47 +09:00
inotia00
2b0f12cb9e Merge branch 'dev' into revanced-extended 2024-12-09 22:34:28 +09:00
inotia00
5088daa434 Merge branch 'dev' into revanced-extended 2024-12-09 01:40:58 +09:00
inotia00
b5fe136898 Merge branch 'dev' into revanced-extended 2024-12-08 18:48:19 +09:00
inotia00
807494f8a7 Merge branch 'dev' into revanced-extended 2024-11-08 22:42:17 +09:00
inotia00
d8ae741853 Merge branch 'dev' into revanced-extended 2024-10-21 20:29:13 +09:00
inotia00
934c33a0f9 Merge branch 'dev' into revanced-extended 2024-10-13 02:34:39 +09:00
inotia00
b725e54aee Merge branch 'dev' into revanced-extended 2024-09-29 00:26:24 +09:00
inotia00
c57de97576 Merge branch 'dev' into revanced-extended 2024-09-07 09:32:24 +09:00
inotia00
2e3274152b Merge branch 'dev' into revanced-extended 2024-08-12 12:02:15 +09:00
KobeW50
6499b7d3f0
ci: workflow to ping Discord users when patches are released (#72)
* init: Workflow to notify discord users of releases

* Rename workflow

* chore (Background playback): Shorten description

* Revert "chore (Background playback): Shorten description"

This reverts commit 10661b870f0c9c670c5d522f9b2ca7cc82d32772.

* Change message contents
2024-08-11 13:11:10 +09:00
287 changed files with 8453 additions and 4248 deletions

115
README.md
View File

@ -85,48 +85,48 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| 💊 Patch | 📜 Description | 🏹 Target Version | | 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:| |:--------:|:--------------:|:-----------------:|
| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 8.10.51 | | `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 8.12.53 |
| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 8.10.51 | | `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 8.12.53 |
| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 8.10.51 | | `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 8.12.53 |
| `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 6.20.51 ~ 8.10.51 | | `Change share sheet` | Adds an option to change the in-app share sheet to the system share sheet. | 6.20.51 ~ 8.12.53 |
| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 8.10.51 | | `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 8.12.53 |
| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 8.10.51 | | `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 8.12.53 |
| `Custom branding name for YouTube Music` | Changes the YouTube Music app name to the name specified in patch options. | 6.20.51 ~ 8.10.51 | | `Custom branding name for YouTube Music` | Changes the YouTube Music app name to the name specified in patch options. | 6.20.51 ~ 8.12.53 |
| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 8.10.51 | | `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 8.12.53 |
| `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 6.20.51 ~ 8.10.51 | | `Dark theme` | Changes the app's dark theme to the values specified in patch options. | 6.20.51 ~ 8.12.53 |
| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.10.51 | | `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 8.12.53 |
| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 8.10.51 | | `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 8.12.53 |
| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 6.20.51 ~ 8.10.51 | | `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 6.20.51 ~ 8.12.53 |
| `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 8.10.51 | | `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 8.12.53 |
| `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 8.10.51 | | `Disable forced auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 8.12.53 |
| `Disable music video in album` | Adds option to redirect music videos from albums for non-premium users. | 6.20.51 ~ 8.10.51 | | `Disable music video in album` | Adds option to redirect music videos from albums for non-premium users. | 6.20.51 ~ 8.12.53 |
| `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 6.20.51 ~ 8.10.51 | | `Enable OPUS codec` | Adds an option to enable the OPUS audio codec if the player response includes it. | 6.20.51 ~ 8.12.53 |
| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 8.10.51 | | `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 8.12.53 |
| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 8.10.51 | | `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 8.12.53 |
| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 8.10.51 | | `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 8.12.53 |
| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 6.20.51 ~ 8.10.51 | | `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 6.20.51 ~ 8.12.53 |
| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.10.51 | | `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 8.12.53 |
| `Hide action bar components` | Adds options to hide action bar components and replace the offline download button with an external download button. | 6.20.51 ~ 8.10.51 | | `Hide action bar components` | Adds options to hide action bar components and replace the offline download button with an external download button. | 6.20.51 ~ 8.12.53 |
| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.10.51 | | `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 8.12.53 |
| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 8.10.51 | | `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 8.12.53 |
| `Hide overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 8.10.51 | | `Hide overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 8.12.53 |
| `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 8.10.51 | | `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 8.12.53 |
| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 6.20.51 ~ 8.10.51 | | `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 6.20.51 ~ 8.12.53 |
| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.10.51 | | `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 8.12.53 |
| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 8.10.51 | | `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 8.12.53 |
| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 6.20.51 ~ 8.10.51 | | `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 6.20.51 ~ 8.12.53 |
| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 6.20.51 ~ 8.10.51 | | `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 6.20.51 ~ 8.12.53 |
| `Return YouTube Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 8.10.51 | | `Return YouTube Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 8.12.53 |
| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 8.10.51 | | `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 8.12.53 |
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 6.20.51 ~ 8.10.51 | | `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 6.20.51 ~ 8.12.53 |
| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 6.20.51 ~ 8.10.51 | | `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 6.20.51 ~ 8.12.53 |
| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 8.10.51 | | `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 8.12.53 |
| `Spoof app version` | Adds options to spoof the YouTube Music client version. This can be used to restore old UI elements and features. | 6.51.53 ~ 7.16.53 | | `Spoof app version` | Adds options to spoof the YouTube Music client version. This can be used to restore old UI elements and features. | 6.51.53 ~ 8.10.52 |
| `Spoof player parameter` | Adds options to spoof player parameter to allow playback. | 6.20.51 ~ 8.10.51 | | `Spoof player parameter` | Adds options to spoof player parameter to allow playback. | 6.20.51 ~ 8.12.53 |
| `Translations for YouTube Music` | Add translations or remove string resources. | 6.20.51 ~ 8.10.51 | | `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.10.51 | | `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 6.20.51 ~ 8.12.53 |
| `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 8.10.51 | | `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 8.12.53 |
| `Watch history` | Adds an option to change the domain of the watch history or check its status. | 6.20.51 ~ 8.10.51 | | `Watch history` | Adds an option to change the domain of the watch history or check its status. | 6.20.51 ~ 8.12.53 |
</details> </details>
### [📦 `com.reddit.frontpage`](https://play.google.com/store/apps/details?id=com.reddit.frontpage) ### [📦 `com.reddit.frontpage`](https://play.google.com/store/apps/details?id=com.reddit.frontpage)
@ -134,19 +134,19 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm
| 💊 Patch | 📜 Description | 🏹 Target Version | | 💊 Patch | 📜 Description | 🏹 Target Version |
|:--------:|:--------------:|:-----------------:| |:--------:|:--------------:|:-----------------:|
| `Change package name` | Changes the package name for Reddit to the name specified in patch options. | 2024.17.0 ~ 2025.05.1 | | `Change package name` | Changes the package name for Reddit to the name specified in patch options. | 2024.17.0 ~ 2025.12.1 |
| `Custom branding name for Reddit` | Changes the Reddit app name to the name specified in patch options. | 2024.17.0 ~ 2025.05.1 | | `Custom branding name for Reddit` | Changes the Reddit app name to the name specified in patch options. | 2024.17.0 ~ 2025.12.1 |
| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2024.17.0 ~ 2025.05.1 | | `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2024.17.0 ~ 2025.12.1 |
| `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2024.17.0 ~ 2025.05.1 | | `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2024.17.0 ~ 2025.12.1 |
| `Hide ads` | Adds options to hide ads. | 2024.17.0 ~ 2025.05.1 | | `Hide ads` | Adds options to hide ads. | 2024.17.0 ~ 2025.12.1 |
| `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | 2024.17.0 ~ 2025.05.1 | | `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | 2024.17.0 ~ 2025.12.1 |
| `Hide recommended communities shelf` | Adds an option to hide the recommended communities shelves in subreddits. | 2024.17.0 ~ 2025.05.1 | | `Hide recommended communities shelf` | Adds an option to hide the recommended communities shelves in subreddits. | 2024.17.0 ~ 2025.12.1 |
| `Open links directly` | Adds an option to skip over redirection URLs in external links. | 2024.17.0 ~ 2025.05.1 | | `Open links directly` | Adds an option to skip over redirection URLs in external links. | 2024.17.0 ~ 2025.12.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 | | `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.05.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.05.1 | | `Remove subreddit dialog` | Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically. | 2024.17.0 ~ 2025.12.1 |
| `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 2024.17.0 ~ 2025.05.1 | | `Sanitize sharing links` | Adds an option to sanitize sharing links by removing tracking query parameters. | 2024.17.0 ~ 2025.12.1 |
| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2024.17.0 ~ 2025.05.1 | | `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2024.17.0 ~ 2025.12.1 |
</details> </details>
@ -187,7 +187,7 @@ Example:
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -199,7 +199,8 @@ Example:
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [] "options": []

View File

@ -108,13 +108,13 @@ public class FlyoutPatch {
if (REPLACE_FLYOUT_MENU_DISMISS_QUEUE.get() && if (REPLACE_FLYOUT_MENU_DISMISS_QUEUE.get() &&
textView.getParent() instanceof ViewGroup clickAbleArea) { textView.getParent() instanceof ViewGroup clickAbleArea) {
runOnMainThreadDelayed(() -> { runOnMainThreadDelayed(() -> {
textView.setText(str("revanced_replace_flyout_menu_dismiss_queue_watch_on_youtube_label")); textView.setText(str("revanced_replace_flyout_menu_dismiss_queue_watch_on_youtube_label"));
imageView.setImageResource(getIdentifier("yt_outline_youtube_logo_icon_vd_theme_24", ResourceType.DRAWABLE, clickAbleArea.getContext())); imageView.setImageResource(getIdentifier("yt_outline_youtube_logo_icon_vd_theme_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
clickAbleArea.setOnClickListener(view -> { clickAbleArea.setOnClickListener(view -> {
clickView(touchOutSideViewRef.get()); clickView(touchOutSideViewRef.get());
VideoUtils.openInYouTube(); VideoUtils.openInYouTube();
}); });
}, 0L }, 0L
); );
} }
} }
@ -126,14 +126,14 @@ public class FlyoutPatch {
textView.getParent() instanceof ViewGroup clickAbleArea textView.getParent() instanceof ViewGroup clickAbleArea
) { ) {
runOnMainThreadDelayed(() -> { runOnMainThreadDelayed(() -> {
textView.setText(str("playback_rate_title")); textView.setText(str("playback_rate_title"));
imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext())); imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext()));
imageView.setColorFilter(cf); imageView.setColorFilter(cf);
clickAbleArea.setOnClickListener(view -> { clickAbleArea.setOnClickListener(view -> {
clickView(touchOutSideViewRef.get()); clickView(touchOutSideViewRef.get());
VideoUtils.showPlaybackSpeedFlyoutMenu(); VideoUtils.showPlaybackSpeedFlyoutMenu();
}); });
}, 0L }, 0L
); );
} }
} }

View File

@ -7,15 +7,12 @@ import androidx.annotation.NonNull;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.music.patches.misc.requests.PlaylistRequest; import app.revanced.extension.music.patches.misc.requests.PlaylistRequest;
import app.revanced.extension.music.settings.Settings; import app.revanced.extension.music.settings.Settings;
import app.revanced.extension.music.shared.VideoInformation; import app.revanced.extension.music.shared.VideoInformation;
import app.revanced.extension.music.utils.VideoUtils; import app.revanced.extension.music.utils.VideoUtils;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class AlbumMusicVideoPatch { public class AlbumMusicVideoPatch {
@ -40,7 +37,7 @@ public class AlbumMusicVideoPatch {
private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK"; private static final String YOUTUBE_MUSIC_ALBUM_PREFIX = "OLAK";
private static final AtomicBoolean isVideoLaunched = new AtomicBoolean(false); private static volatile boolean isVideoLaunched = false;
@NonNull @NonNull
private static volatile String playerResponseVideoId = ""; private static volatile String playerResponseVideoId = "";
@ -100,14 +97,6 @@ public class AlbumMusicVideoPatch {
if (request == null) { if (request == null) {
return; return;
} }
// This hook is always called off the main thread,
// but this can later be called for the same video id from the main thread.
// This is not a concern, since the fetch will always be finished
// and never block the main thread.
// But if debugging, then still verify this is the situation.
if (BaseSettings.ENABLE_DEBUG_LOGGING.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) {
Logger.printException(() -> "Error: Blocking main thread");
}
String songId = request.getStream(); String songId = request.getStream();
if (songId.isEmpty()) { if (songId.isEmpty()) {
Logger.printDebug(() -> "Official song not found, videoId: " + videoId); Logger.printDebug(() -> "Official song not found, videoId: " + videoId);
@ -149,17 +138,16 @@ public class AlbumMusicVideoPatch {
private static void openMusic(@NonNull String songId) { private static void openMusic(@NonNull String songId) {
try { try {
isVideoLaunched.compareAndSet(false, true);
// The newly opened video is not a music video. // The newly opened video is not a music video.
// To prevent fetch requests from being sent, set the video id to the newly opened video // To prevent fetch requests from being sent, set the video id to the newly opened video
VideoUtils.runOnMainThreadDelayed(() -> { VideoUtils.runOnMainThreadDelayed(() -> {
isVideoLaunched = true;
playerResponseVideoId = songId; playerResponseVideoId = songId;
currentVideoId = songId; currentVideoId = songId;
VideoUtils.openInYouTubeMusic(songId); VideoUtils.openInYouTubeMusic(songId);
}, 1000); VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched = false, 3000);
}, 1500);
VideoUtils.runOnMainThreadDelayed(() -> isVideoLaunched.compareAndSet(true, false), 2500);
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "openMusic failure", ex); Logger.printException(() -> "openMusic failure", ex);
} }
@ -191,7 +179,7 @@ public class AlbumMusicVideoPatch {
* Injection point. * Injection point.
*/ */
public static boolean hideSnackBar() { public static boolean hideSnackBar() {
return DISABLE_MUSIC_VIDEO_IN_ALBUM && isVideoLaunched.get(); return DISABLE_MUSIC_VIDEO_IN_ALBUM && isVideoLaunched;
} }
} }

View File

@ -9,7 +9,6 @@ import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import java.util.Arrays;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;

View File

@ -2,8 +2,10 @@ package app.revanced.extension.music.patches.misc.requests
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLIST_PAGE
import app.revanced.extension.shared.requests.Requester import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.settings.AppLanguage import app.revanced.extension.shared.settings.AppLanguage
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
@ -136,10 +138,11 @@ class PlaylistRequest private constructor(
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLIST_PAGE, GET_PLAYLIST_PAGE,
clientType clientType
) )
/** /**
* For some reason, the tracks in Top Songs have the playlistId of the album: * For some reason, the tracks in Top Songs have the playlistId of the album:
* [ReVanced_Extended#2835](https://github.com/inotia00/ReVanced_Extended/issues/2835) * [ReVanced_Extended#2835](https://github.com/inotia00/ReVanced_Extended/issues/2835)
@ -152,7 +155,7 @@ class PlaylistRequest private constructor(
* So we can work around this by setting the language to English when sending the request. * So we can work around this by setting the language to English when sending the request.
*/ */
val requestBody = val requestBody =
PlayerRoutes.createApplicationRequestBody( createApplicationRequestBody(
clientType = clientType, clientType = clientType,
videoId = videoId, videoId = videoId,
playlistId = playlistId, playlistId = playlistId,

View File

@ -2,7 +2,10 @@ package app.revanced.extension.reddit.patches;
import static app.revanced.extension.shared.utils.StringRef.str; import static app.revanced.extension.shared.utils.StringRef.str;
import android.app.Dialog;
import android.view.View; import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -34,6 +37,35 @@ public class RemoveSubRedditDialogPatch {
return Settings.REMOVE_NSFW_DIALOG.get() || hasBeenVisited; 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) { public static boolean spoofLoggedInStatus(boolean isLoggedIn) {
return !Settings.REMOVE_NOTIFICATION_DIALOG.get() && isLoggedIn; return !Settings.REMOVE_NOTIFICATION_DIALOG.get() && isLoggedIn;
} }

View File

@ -5,7 +5,7 @@ import app.revanced.extension.reddit.settings.Settings;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class ScreenshotPopupPatch { public class ScreenshotPopupPatch {
public static boolean disableScreenshotPopup() { public static Boolean disableScreenshotPopup(Boolean original) {
return Settings.DISABLE_SCREENSHOT_POPUP.get(); return Settings.DISABLE_SCREENSHOT_POPUP.get() ? Boolean.FALSE : original;
} }
} }

View File

@ -13,7 +13,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_NEW_POST_ADS = new BooleanSetting("revanced_hide_new_post_ads", TRUE, true); public static final BooleanSetting HIDE_NEW_POST_ADS = new BooleanSetting("revanced_hide_new_post_ads", TRUE, true);
// Layout // Layout
public static final BooleanSetting DISABLE_SCREENSHOT_POPUP = new BooleanSetting("revanced_disable_screenshot_popup", TRUE); public static final BooleanSetting DISABLE_SCREENSHOT_POPUP = new BooleanSetting("revanced_disable_screenshot_popup", TRUE, true);
public static final BooleanSetting HIDE_CHAT_BUTTON = new BooleanSetting("revanced_hide_chat_button", FALSE, true); public static final BooleanSetting HIDE_CHAT_BUTTON = new BooleanSetting("revanced_hide_chat_button", FALSE, true);
public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", FALSE, true); public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", FALSE, true);
public static final BooleanSetting HIDE_DISCOVER_BUTTON = new BooleanSetting("revanced_hide_discover_button", FALSE, true); public static final BooleanSetting HIDE_DISCOVER_BUTTON = new BooleanSetting("revanced_hide_discover_button", FALSE, true);

View File

@ -1,6 +1,7 @@
package app.revanced.extension.shared.patches.client package app.revanced.extension.shared.innertube.client
import android.os.Build import android.os.Build
import app.revanced.extension.shared.patches.PatchStatus
import app.revanced.extension.shared.settings.BaseSettings import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.PackageUtils import app.revanced.extension.shared.utils.PackageUtils
import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.ArrayUtils
@ -212,8 +213,15 @@ object YouTubeAppClient {
return BaseSettings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get() return BaseSettings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get()
} }
private fun useIOS(): Boolean {
return PatchStatus.SpoofStreamingDataIOS() && BaseSettings.SPOOF_STREAMING_DATA_TYPE_IOS.get()
}
fun availableClientTypes(preferredClient: ClientType): Array<ClientType> { fun availableClientTypes(preferredClient: ClientType): Array<ClientType> {
val availableClientTypes = ClientType.CLIENT_ORDER_TO_USE val availableClientTypes = if (useIOS())
ClientType.CLIENT_ORDER_TO_USE_IOS
else
ClientType.CLIENT_ORDER_TO_USE
if (ArrayUtils.contains(availableClientTypes, preferredClient)) { if (ArrayUtils.contains(availableClientTypes, preferredClient)) {
val clientToUse: Array<ClientType?> = arrayOfNulls(availableClientTypes.size) val clientToUse: Array<ClientType?> = arrayOfNulls(availableClientTypes.size)
@ -278,10 +286,6 @@ object YouTubeAppClient {
* If true, 'Authorization' must be included. * If true, 'Authorization' must be included.
*/ */
val requireAuth: Boolean = false, val requireAuth: Boolean = false,
/**
* Whether a poToken is required to get playback for more than 1 minute.
*/
val requirePoToken: Boolean = false,
/** /**
* Client name for innertube body. * Client name for innertube body.
*/ */
@ -363,7 +367,7 @@ object YouTubeAppClient {
else else
"iOS TV" "iOS TV"
), ),
IOS( IOS_DEPRECATED(
id = 5, id = 5,
deviceMake = DEVICE_MAKE_IOS, deviceMake = DEVICE_MAKE_IOS,
deviceModel = DEVICE_MODEL_IOS, deviceModel = DEVICE_MODEL_IOS,
@ -372,7 +376,6 @@ object YouTubeAppClient {
userAgent = USER_AGENT_IOS, userAgent = USER_AGENT_IOS,
clientVersion = CLIENT_VERSION_IOS, clientVersion = CLIENT_VERSION_IOS,
supportsCookies = false, supportsCookies = false,
requirePoToken = true,
clientName = "IOS", clientName = "IOS",
friendlyName = if (forceAVC()) friendlyName = if (forceAVC())
"iOS Force AVC" "iOS Force AVC"
@ -388,6 +391,15 @@ object YouTubeAppClient {
IOS_UNPLUGGED, IOS_UNPLUGGED,
ANDROID_VR, 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,
)
} }
} }
} }

View File

@ -1,10 +1,10 @@
package app.revanced.extension.shared.patches.client; package app.revanced.extension.shared.innertube.client;
import android.os.Build; import android.os.Build;
import java.util.Locale; import java.util.Locale;
public class MusicAppClient { public class YouTubeMusicAppClient {
// Response to the '/next' request is 'Please update to continue using the app': // Response to the '/next' request is 'Please update to continue using the app':
// https://github.com/inotia00/ReVanced_Extended/issues/2743 // https://github.com/inotia00/ReVanced_Extended/issues/2743
@ -46,7 +46,7 @@ public class MusicAppClient {
private static final String DEVICE_MAKE_IOS_MUSIC = "Apple"; private static final String DEVICE_MAKE_IOS_MUSIC = "Apple";
private static final String OS_NAME_IOS_MUSIC = "iOS"; private static final String OS_NAME_IOS_MUSIC = "iOS";
private MusicAppClient() { private YouTubeMusicAppClient() {
} }
private static String androidUserAgent(String clientVersion) { private static String androidUserAgent(String clientVersion) {

View File

@ -1,14 +1,10 @@
package app.revanced.extension.shared.patches.client package app.revanced.extension.shared.innertube.client
/** /**
* Used to fetch video information. * Used to fetch video information.
*/ */
@Suppress("unused") @Suppress("unused")
object YouTubeWebClient { object YouTubeWebClient {
/**
* This user agent does not require a PoToken in [ClientType.MWEB]
* https://github.com/yt-dlp/yt-dlp/blob/0b6b7742c2e7f2a1fcb0b54ef3dd484bab404b3f/yt_dlp/extractor/youtube.py#L259
*/
private const val USER_AGENT_SAFARI = private const val USER_AGENT_SAFARI =
"Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)" "Mozilla/5.0 (iPad; CPU OS 16_7_10 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.6 Mobile/15E148 Safari/604.1,gzip(gfe)"
@ -26,11 +22,11 @@ object YouTubeWebClient {
* Client version. * Client version.
*/ */
@JvmField @JvmField
val clientVersion: String val clientVersion: String,
) { ) {
MWEB( MWEB(
id = 2, id = 2,
clientVersion = "2.20241202.07.00" clientVersion = "2.20241202.07.00",
), ),
WEB_REMIX( WEB_REMIX(
id = 29, id = 29,

View File

@ -1,9 +1,8 @@
package app.revanced.extension.shared.patches.spoof.requests package app.revanced.extension.shared.innertube.requests
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.client.YouTubeWebClient import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.requests.Requester 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.requests.Route.CompiledRoute
import app.revanced.extension.shared.settings.BaseSettings import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
@ -21,96 +20,17 @@ import java.util.Locale
import java.util.TimeZone import java.util.TimeZone
@Suppress("deprecation") @Suppress("deprecation")
object PlayerRoutes { object InnerTubeRequestBody {
@JvmField
val CREATE_PLAYLIST: CompiledRoute = Route(
Route.Method.POST,
"playlist/create" +
"?prettyPrint=false" +
"&fields=playlistId"
).compile()
@JvmField
val DELETE_PLAYLIST: CompiledRoute = Route(
Route.Method.POST,
"playlist/delete" +
"?prettyPrint=false"
).compile()
@JvmField
val EDIT_PLAYLIST: CompiledRoute = Route(
Route.Method.POST,
"browse/edit_playlist" +
"?prettyPrint=false" +
"&fields=status," +
"playlistEditResults"
).compile()
@JvmField
val GET_PLAYLISTS: CompiledRoute = Route(
Route.Method.POST,
"playlist/get_add_to_playlist" +
"?prettyPrint=false" +
"&fields=contents.addToPlaylistRenderer.playlists.playlistAddToOptionRenderer"
).compile()
@JvmField
val GET_CATEGORY: CompiledRoute = Route(
Route.Method.POST,
"player" +
"?prettyPrint=false" +
"&fields=microformat.playerMicroformatRenderer.category"
).compile()
@JvmField
val GET_SET_VIDEO_ID: CompiledRoute = Route(
Route.Method.POST,
"next" +
"?prettyPrint=false" +
"&fields=contents.singleColumnWatchNextResults." +
"playlist.playlist.contents.playlistPanelVideoRenderer." +
"playlistSetVideoId"
).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/" 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 * TCP connection and HTTP read timeout
*/ */
@ -230,16 +150,10 @@ object PlayerRoutes {
client.put("osName", clientType.osName) client.put("osName", clientType.osName)
client.put("osVersion", clientType.osVersion) client.put("osVersion", clientType.osVersion)
client.put("androidSdkVersion", clientType.androidSdkVersion) client.put("androidSdkVersion", clientType.androidSdkVersion)
if (clientType.gmscoreVersionCode != null) { client.put("hl", LOCALE_LANGUAGE)
client.put("gmscoreVersionCode", clientType.gmscoreVersionCode)
}
client.put(
"hl",
LOCALE_LANGUAGE
)
client.put("gl", LOCALE_COUNTRY) client.put("gl", LOCALE_COUNTRY)
client.put("timeZone", TIME_ZONE_ID) client.put("timeZone", TIME_ZONE_ID)
client.put("utcOffsetMinutes", "$UTC_OFFSET_MINUTES") client.put("utcOffsetMinutes", UTC_OFFSET_MINUTES.toString())
val context = JSONObject() val context = JSONObject()
context.put("client", client) context.put("client", client)
@ -269,7 +183,7 @@ object PlayerRoutes {
videoIds.put(0, videoId) videoIds.put(0, videoId)
innerTubeBody.put("videoIds", videoIds) innerTubeBody.put("videoIds", videoIds)
} catch (e: JSONException) { } catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e) Logger.printException({ "Failed to create create/playlist innerTubeBody" }, e)
} }
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8) return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -284,7 +198,7 @@ object PlayerRoutes {
try { try {
innerTubeBody.put("playlistId", playlistId) innerTubeBody.put("playlistId", playlistId)
} catch (e: JSONException) { } catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e) Logger.printException({ "Failed to create delete/playlist innerTubeBody" }, e)
} }
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8) return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -314,7 +228,7 @@ object PlayerRoutes {
actionsArray.put(0, actionsObject) actionsArray.put(0, actionsObject)
innerTubeBody.put("actions", actionsArray) innerTubeBody.put("actions", actionsArray)
} catch (e: JSONException) { } catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e) Logger.printException({ "Failed to create edit/playlist innerTubeBody" }, e)
} }
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8) return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -330,7 +244,7 @@ object PlayerRoutes {
innerTubeBody.put("playlistId", playlistId) innerTubeBody.put("playlistId", playlistId)
innerTubeBody.put("excludeWatchLater", false) innerTubeBody.put("excludeWatchLater", false)
} catch (e: JSONException) { } catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e) Logger.printException({ "Failed to create get/playlists innerTubeBody" }, e)
} }
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8) return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
@ -354,44 +268,62 @@ object PlayerRoutes {
actionsArray.put(0, actionsObject) actionsArray.put(0, actionsObject)
innerTubeBody.put("actions", actionsArray) innerTubeBody.put("actions", actionsArray)
} catch (e: JSONException) { } catch (e: JSONException) {
Logger.printException({ "Failed to create playlist innerTubeBody" }, e) Logger.printException({ "Failed to create save/playlist innerTubeBody" }, e)
} }
return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8) return innerTubeBody.toString().toByteArray(StandardCharsets.UTF_8)
} }
@JvmStatic @JvmStatic
fun getPlayerResponseConnectionFromRoute( fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute, route: CompiledRoute,
clientType: YouTubeAppClient.ClientType clientType: YouTubeAppClient.ClientType,
): HttpURLConnection { requestHeader: Map<String, String>? = null,
return getPlayerResponseConnectionFromRoute( dataSyncId: String? = null,
route, connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
clientType.userAgent, readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
clientType.id.toString(), ) = getInnerTubeResponseConnectionFromRoute(
clientType.clientVersion route = route,
) userAgent = clientType.userAgent,
} clientId = clientType.id.toString(),
clientVersion = clientType.clientVersion,
supportsCookies = clientType.supportsCookies,
requestHeader = requestHeader,
dataSyncId = dataSyncId,
connectTimeout = connectTimeout,
readTimeout = readTimeout,
)
@JvmStatic @JvmStatic
fun getPlayerResponseConnectionFromRoute( fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute, route: CompiledRoute,
clientType: YouTubeWebClient.ClientType clientType: YouTubeWebClient.ClientType,
): HttpURLConnection { requestHeader: Map<String, String>? = null,
return getPlayerResponseConnectionFromRoute( dataSyncId: String? = null,
route, connectTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
clientType.userAgent, readTimeout: Int = CONNECTION_TIMEOUT_MILLISECONDS,
clientType.id.toString(), ) = getInnerTubeResponseConnectionFromRoute(
clientType.clientVersion, route = route,
) userAgent = clientType.userAgent,
} clientId = clientType.id.toString(),
clientVersion = clientType.clientVersion,
requestHeader = requestHeader,
dataSyncId = dataSyncId,
connectTimeout = connectTimeout,
readTimeout = readTimeout,
)
@Throws(IOException::class) @Throws(IOException::class)
fun getPlayerResponseConnectionFromRoute( fun getInnerTubeResponseConnectionFromRoute(
route: CompiledRoute, route: CompiledRoute,
userAgent: String, userAgent: String,
clientId: String, clientId: String,
clientVersion: 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 { ): HttpURLConnection {
val connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route) val connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route)
@ -403,8 +335,29 @@ object PlayerRoutes {
connection.useCaches = false connection.useCaches = false
connection.doOutput = true connection.doOutput = true
connection.connectTimeout = CONNECTION_TIMEOUT_MILLISECONDS connection.connectTimeout = connectTimeout
connection.readTimeout = CONNECTION_TIMEOUT_MILLISECONDS 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 return connection
} }

View File

@ -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()
}
}

View File

@ -37,7 +37,7 @@ public class FullscreenAdsPatch {
* Therefore, make sure that the dialog contains the ads at the beginning of the Method * Therefore, make sure that the dialog contains the ads at the beginning of the Method
* *
* @param bytes proto buffer array * @param bytes proto buffer array
* @param type dialog type (similar to {@link Enum#ordinal()}) * @param type dialog type (similar to {@link Enum#ordinal()})
*/ */
public static void checkDialog(byte[] bytes, int type) { public static void checkDialog(byte[] bytes, int type) {
if (!HIDE_FULLSCREEN_ADS) { if (!HIDE_FULLSCREEN_ADS) {

View File

@ -11,4 +11,8 @@ public class PatchStatus {
// Replace this with true If the Spoof streaming data patch succeeds in YouTube. // Replace this with true If the Spoof streaming data patch succeeds in YouTube.
return false; return false;
} }
public static boolean SpoofStreamingDataIOS() {
return false;
}
} }

View File

@ -2,7 +2,6 @@ package app.revanced.extension.shared.patches;
import static java.lang.Boolean.FALSE; import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan; import static app.revanced.extension.shared.utils.Utils.newSpanUsingStylingOfAnotherSpan;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;

View File

@ -2,8 +2,8 @@ package app.revanced.extension.shared.patches;
import android.net.Uri; import android.net.Uri;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.utils.Logger;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class WatchHistoryPatch { public final class WatchHistoryPatch {

View File

@ -1,11 +1,11 @@
package app.revanced.extension.shared.patches.spoof; package app.revanced.extension.shared.patches.spoof;
import app.revanced.extension.shared.patches.client.MusicAppClient.ClientType; import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient.ClientType;
import app.revanced.extension.music.settings.Settings; import app.revanced.extension.shared.settings.BaseSettings;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class SpoofClientPatch extends BlockRequestPatch { public class SpoofClientPatch extends BlockRequestPatch {
private static final ClientType CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get(); private static final ClientType CLIENT_TYPE = BaseSettings.SPOOF_CLIENT_TYPE.get();
/** /**
* Injection point. * Injection point.

View File

@ -10,21 +10,21 @@ import java.util.Collections;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import app.revanced.extension.shared.patches.client.YouTubeAppClient.ClientType; import app.revanced.extension.shared.innertube.client.YouTubeAppClient.ClientType;
import app.revanced.extension.shared.patches.PatchStatus;
import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest; import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class SpoofStreamingDataPatch extends BlockRequestPatch { public class SpoofStreamingDataPatch extends BlockRequestPatch {
private static final String PO_TOKEN =
BaseSettings.SPOOF_STREAMING_DATA_PO_TOKEN.get();
private static final String VISITOR_DATA =
BaseSettings.SPOOF_STREAMING_DATA_VISITOR_DATA.get();
private static final boolean SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION = private static final boolean SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION =
SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION.get(); SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION.get();
private static final boolean SPOOF_STREAMING_DATA_TYPE_IOS =
PatchStatus.SpoofStreamingDataIOS() && BaseSettings.SPOOF_STREAMING_DATA_TYPE_IOS.get();
/** /**
* Any unreachable ip address. Used to intentionally fail requests. * Any unreachable ip address. Used to intentionally fail requests.
@ -69,17 +69,27 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
* Skip response encryption in OnesiePlayerRequest. * Skip response encryption in OnesiePlayerRequest.
*/ */
public static boolean skipResponseEncryption(boolean original) { public static boolean skipResponseEncryption(boolean original) {
if (SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) { if (!SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION) {
return false; return original;
} }
return false;
}
return original; /**
* Injection point.
* Turns off a feature flag that interferes with video playback.
*/
public static boolean usePlaybackStartFeatureFlag(boolean original) {
if (!SPOOF_STREAMING_DATA) {
return original;
}
return false;
} }
/** /**
* Injection point. * Injection point.
*/ */
public static void fetchStreams(String url, Map<String, String> requestHeaders) { public static void fetchStreams(String url, Map<String, String> requestHeader) {
if (SPOOF_STREAMING_DATA) { if (SPOOF_STREAMING_DATA) {
String id = Utils.getVideoIdFromRequest(url); String id = Utils.getVideoIdFromRequest(url);
if (id == null) { if (id == null) {
@ -89,7 +99,7 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
return; return;
} }
StreamingDataRequest.fetchRequest(id, requestHeaders, VISITOR_DATA, PO_TOKEN); StreamingDataRequest.fetchRequest(id, requestHeader);
} }
} }
@ -210,6 +220,18 @@ public class SpoofStreamingDataPatch extends BlockRequestPatch {
return videoFormat; return videoFormat;
} }
public static String[] getEntries() {
return SPOOF_STREAMING_DATA_TYPE_IOS
? ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_ios_entries")
: ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_entries");
}
public static String[] getEntryValues() {
return SPOOF_STREAMING_DATA_TYPE_IOS
? ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_ios_entry_values")
: ResourceUtils.getStringArray("revanced_spoof_streaming_data_type_entry_values");
}
public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability { public static final class AudioStreamLanguageOverrideAvailability implements Setting.Availability {
@Override @Override
public boolean isAvailable() { public boolean isAvailable() {

View File

@ -1,14 +1,14 @@
package app.revanced.extension.shared.patches.spoof.requests package app.revanced.extension.shared.patches.spoof.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.createApplicationRequestBody import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.getPlayerResponseConnectionFromRoute import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_STREAMING_DATA
import app.revanced.extension.shared.settings.BaseSettings import app.revanced.extension.shared.settings.BaseSettings
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.StringRef.str
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
import org.apache.commons.lang3.StringUtils
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.IOException import java.io.IOException
@ -32,21 +32,19 @@ import java.util.concurrent.TimeoutException
* did use its own client streams. * did use its own client streams.
*/ */
class StreamingDataRequest private constructor( class StreamingDataRequest private constructor(
videoId: String, playerHeaders: Map<String, String>, videoId: String,
visitorId: String, botGuardPoToken: String requestHeader: Map<String, String>,
) { ) {
private val videoId: String private val videoId: String
private val future: Future<ByteBuffer?> private val future: Future<ByteBuffer?>
init { init {
Objects.requireNonNull(playerHeaders) Objects.requireNonNull(requestHeader)
this.videoId = videoId this.videoId = videoId
this.future = Utils.submitOnBackgroundThread { this.future = Utils.submitOnBackgroundThread {
fetch( fetch(
videoId, videoId,
playerHeaders, requestHeader,
visitorId,
botGuardPoToken
) )
} }
} }
@ -86,33 +84,16 @@ class StreamingDataRequest private constructor(
companion object { companion object {
private const val AUTHORIZATION_HEADER = "Authorization" private const val AUTHORIZATION_HEADER = "Authorization"
private const val VISITOR_ID_HEADER = "X-Goog-Visitor-Id" private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
private val REQUEST_HEADER_KEYS = arrayOf(
AUTHORIZATION_HEADER, // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
VISITOR_ID_HEADER
)
private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType = private val SPOOF_STREAMING_DATA_TYPE: YouTubeAppClient.ClientType =
BaseSettings.SPOOF_STREAMING_DATA_TYPE.get() BaseSettings.SPOOF_STREAMING_DATA_TYPE.get()
private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> = private val CLIENT_ORDER_TO_USE: Array<YouTubeAppClient.ClientType> =
YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE) YouTubeAppClient.availableClientTypes(SPOOF_STREAMING_DATA_TYPE)
private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean = private val DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH: Boolean =
SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH SPOOF_STREAMING_DATA_TYPE == YouTubeAppClient.ClientType.ANDROID_VR_NO_AUTH
private var lastSpoofedClientType: YouTubeAppClient.ClientType? = null private var lastSpoofedClientFriendlyName: String? = null
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap( val cache: MutableMap<String, StreamingDataRequest> = Collections.synchronizedMap(
@ -126,22 +107,24 @@ class StreamingDataRequest private constructor(
@JvmStatic @JvmStatic
val lastSpoofedClientName: String val lastSpoofedClientName: String
get() = lastSpoofedClientType get() {
?.friendlyName return if (lastSpoofedClientFriendlyName != null) {
?: "Unknown" lastSpoofedClientFriendlyName!!
} else {
"Unknown"
}
}
@JvmStatic @JvmStatic
fun fetchRequest( fun fetchRequest(
videoId: String, fetchHeaders: Map<String, String>, videoId: String,
visitorId: String, botGuardPoToken: String fetchHeaders: Map<String, String>,
) { ) {
// Always fetch, even if there is an existing request for the same video. // Always fetch, even if there is an existing request for the same video.
cache[videoId] = cache[videoId] =
StreamingDataRequest( StreamingDataRequest(
videoId, videoId,
fetchHeaders, fetchHeaders
visitorId,
botGuardPoToken
) )
} }
@ -150,71 +133,40 @@ class StreamingDataRequest private constructor(
return cache[videoId] return cache[videoId]
} }
private fun handleConnectionError(toastMessage: String, ex: Exception?) { private fun handleConnectionError(
toastMessage: String,
ex: Exception?,
showToast: Boolean = false,
) {
if (showToast) Utils.showToastShort(toastMessage)
Logger.printInfo({ toastMessage }, ex) Logger.printInfo({ toastMessage }, ex)
} }
private fun send( private fun send(
clientType: YouTubeAppClient.ClientType, clientType: YouTubeAppClient.ClientType,
videoId: String, videoId: String,
playerHeaders: Map<String, String>, requestHeader: Map<String, String>,
visitorId: String,
botGuardPoToken: String
): HttpURLConnection? { ): HttpURLConnection? {
Objects.requireNonNull(clientType) Objects.requireNonNull(clientType)
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
Objects.requireNonNull(playerHeaders) Objects.requireNonNull(requestHeader)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" } Logger.printDebug { "Fetching video streams for: $videoId using client: $clientType" }
try { try {
val connection = val connection =
getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType) getInnerTubeResponseConnectionFromRoute(
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS GET_STREAMING_DATA,
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS clientType,
requestHeader
val usePoToken =
clientType.requirePoToken && !StringUtils.isAnyEmpty(botGuardPoToken, visitorId)
for (key in REQUEST_HEADER_KEYS) {
var value = playerHeaders[key]
if (value != null) {
if (key == AUTHORIZATION_HEADER) {
if (!clientType.supportsCookies) {
Logger.printDebug { "Not including request header: $key" }
continue
}
}
if (key == VISITOR_ID_HEADER && usePoToken) {
val originalVisitorId: String = value
Logger.printDebug { "Original visitor id:\n$originalVisitorId" }
Logger.printDebug { "Replaced visitor id:\n$visitorId" }
value = visitorId
}
connection.setRequestProperty(key, value)
}
}
val requestBody: ByteArray
if (usePoToken) {
requestBody = createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
botGuardPoToken = botGuardPoToken,
visitorId = visitorId,
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
) )
Logger.printDebug { "Set poToken (botGuardPoToken):\n$botGuardPoToken" }
} else { val requestBody = createApplicationRequestBody(
requestBody = clientType = clientType,
createApplicationRequestBody( videoId = videoId,
clientType = clientType, setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
videoId = videoId, )
setLocale = DEFAULT_CLIENT_IS_ANDROID_VR_NO_AUTH,
)
}
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -243,15 +195,15 @@ class StreamingDataRequest private constructor(
} }
private fun fetch( private fun fetch(
videoId: String, playerHeaders: Map<String, String>, videoId: String,
visitorId: String, botGuardPoToken: String requestHeader: Map<String, String>,
): ByteBuffer? { ): ByteBuffer? {
lastSpoofedClientType = null lastSpoofedClientFriendlyName = null
// Retry with different client if empty response body is received. // Retry with different client if empty response body is received.
for (clientType in CLIENT_ORDER_TO_USE) { for (clientType in CLIENT_ORDER_TO_USE) {
if (clientType.requireAuth && if (clientType.requireAuth &&
playerHeaders[AUTHORIZATION_HEADER] == null requestHeader[AUTHORIZATION_HEADER] == null
) { ) {
Logger.printDebug { "Skipped login-required client (incognito mode or not logged in)\nClient: $clientType\nVideo: $videoId" } Logger.printDebug { "Skipped login-required client (incognito mode or not logged in)\nClient: $clientType\nVideo: $videoId" }
continue continue
@ -259,9 +211,7 @@ class StreamingDataRequest private constructor(
send( send(
clientType, clientType,
videoId, videoId,
playerHeaders, requestHeader,
visitorId,
botGuardPoToken
)?.let { connection -> )?.let { connection ->
try { try {
// gzip encoding doesn't response with content length (-1), // gzip encoding doesn't response with content length (-1),
@ -271,14 +221,14 @@ class StreamingDataRequest private constructor(
} else { } else {
BufferedInputStream(connection.inputStream).use { inputStream -> BufferedInputStream(connection.inputStream).use { inputStream ->
ByteArrayOutputStream().use { stream -> ByteArrayOutputStream().use { stream ->
val buffer = ByteArray(2048) val buffer = ByteArray(4096)
var bytesRead: Int var bytesRead: Int
while ((inputStream.read(buffer) while ((inputStream.read(buffer)
.also { bytesRead = it }) >= 0 .also { bytesRead = it }) >= 0
) { ) {
stream.write(buffer, 0, bytesRead) stream.write(buffer, 0, bytesRead)
} }
lastSpoofedClientType = clientType lastSpoofedClientFriendlyName = clientType.friendlyName
return ByteBuffer.wrap(stream.toByteArray()) return ByteBuffer.wrap(stream.toByteArray())
} }
} }
@ -289,7 +239,12 @@ class StreamingDataRequest private constructor(
} }
} }
handleConnectionError("Could not fetch any client streams", null) handleConnectionError(str("revanced_spoof_streaming_data_failed_forbidden"), null, true)
handleConnectionError(
str("revanced_spoof_streaming_data_failed_forbidden_suggestion"),
null,
true
)
return null return null
} }
} }

View File

@ -3,10 +3,10 @@ package app.revanced.extension.shared.settings;
import static java.lang.Boolean.FALSE; import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE; import static java.lang.Boolean.TRUE;
import app.revanced.extension.shared.innertube.client.YouTubeAppClient;
import app.revanced.extension.shared.innertube.client.YouTubeMusicAppClient;
import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat; import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat;
import app.revanced.extension.shared.patches.WatchHistoryPatch.WatchHistoryType; import app.revanced.extension.shared.patches.WatchHistoryPatch.WatchHistoryType;
import app.revanced.extension.shared.patches.client.MusicAppClient;
import app.revanced.extension.shared.patches.client.YouTubeAppClient;
import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch.AudioStreamLanguageOverrideAvailability; import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch.AudioStreamLanguageOverrideAvailability;
/** /**
@ -31,7 +31,7 @@ public class BaseSettings {
* Some patches are in a shared path, so they are declared here. * Some patches are in a shared path, so they are declared here.
*/ */
public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true); public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true);
public static final EnumSetting<MusicAppClient.ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", MusicAppClient.ClientType.IOS_MUSIC_6_21, true); public static final EnumSetting<YouTubeMusicAppClient.ClientType> SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", YouTubeMusicAppClient.ClientType.IOS_MUSIC_6_21, true);
/** /**
* These settings are used by YouTube. * These settings are used by YouTube.
@ -43,11 +43,9 @@ public class BaseSettings {
"revanced_spoof_streaming_data_ios_force_avc_user_dialog_message"); "revanced_spoof_streaming_data_ios_force_avc_user_dialog_message");
public static final BooleanSetting SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION = new BooleanSetting("revanced_spoof_streaming_data_skip_response_encryption", TRUE, true); public static final BooleanSetting SPOOF_STREAMING_DATA_SKIP_RESPONSE_ENCRYPTION = new BooleanSetting("revanced_spoof_streaming_data_skip_response_encryption", TRUE, true);
public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE); public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE);
public static final BooleanSetting SPOOF_STREAMING_DATA_TYPE_IOS = new BooleanSetting("revanced_spoof_streaming_data_type_ios", FALSE, true, "revanced_spoof_streaming_data_type_ios_user_dialog_message");
// Client type must be last spoof setting due to cyclic references. // Client type must be last spoof setting due to cyclic references.
public static final EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_UNPLUGGED, true); public static final EnumSetting<YouTubeAppClient.ClientType> SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", YouTubeAppClient.ClientType.ANDROID_VR, true);
public static final StringSetting SPOOF_STREAMING_DATA_PO_TOKEN = new StringSetting("revanced_spoof_streaming_data_po_token", "", true);
public static final StringSetting SPOOF_STREAMING_DATA_VISITOR_DATA = new StringSetting("revanced_spoof_streaming_data_visitor_data", "", true);
/** /**
* These settings are used by YouTube and YouTube Music. * These settings are used by YouTube and YouTube Music.

View File

@ -149,7 +149,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
* Updates all Preferences values and their availability using the current values in {@link Setting}. * Updates all Preferences values and their availability using the current values in {@link Setting}.
*/ */
protected void updateUIToSettingValues() { protected void updateUIToSettingValues() {
updatePreferenceScreen(getPreferenceScreen(), true,true); updatePreferenceScreen(getPreferenceScreen(), true, true);
} }
/** /**
@ -246,7 +246,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
/** /**
* Updates a UI Preference with the {@link Setting} that backs it. * Updates a UI Preference with the {@link Setting} that backs it.
* *
* @param syncSetting If the UI should be synced {@link Setting} <-> Preference * @param syncSetting If the UI should be synced {@link Setting} <-> Preference
* @param applySettingToPreference If true, then apply {@link Setting} -> Preference. * @param applySettingToPreference If true, then apply {@link Setting} -> Preference.
* If false, then apply {@link Setting} <- Preference. * If false, then apply {@link Setting} <- Preference.
*/ */

View File

@ -10,6 +10,8 @@ import android.util.AttributeSet;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import androidx.annotation.Nullable;
import java.util.Objects; import java.util.Objects;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
@ -19,6 +21,12 @@ import app.revanced.extension.shared.utils.Utils;
@SuppressWarnings({"unused", "deprecation"}) @SuppressWarnings({"unused", "deprecation"})
public class ResettableEditTextPreference extends EditTextPreference { public class ResettableEditTextPreference extends EditTextPreference {
/**
* Setting to reset.
*/
@Nullable
private Setting<?> setting;
public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes); super(context, attrs, defStyleAttr, defStyleRes);
} }
@ -35,6 +43,10 @@ public class ResettableEditTextPreference extends EditTextPreference {
super(context); super(context);
} }
public void setSetting(@Nullable Setting<?> setting) {
this.setting = setting;
}
@Override @Override
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
Utils.setEditTextDialogTheme(builder); Utils.setEditTextDialogTheme(builder);
@ -44,7 +56,12 @@ public class ResettableEditTextPreference extends EditTextPreference {
if (title != null) { if (title != null) {
builder.setTitle(getTitle()); builder.setTitle(getTitle());
} }
final Setting<?> setting = Setting.getSettingFromPath(getKey()); if (setting == null) {
String key = getKey();
if (key != null) {
setting = Setting.getSettingFromPath(key);
}
}
if (setting != null) { if (setting != null) {
builder.setNeutralButton(str("revanced_extended_settings_reset"), null); builder.setNeutralButton(str("revanced_extended_settings_reset"), null);
} }
@ -65,8 +82,7 @@ public class ResettableEditTextPreference extends EditTextPreference {
} }
button.setOnClickListener(v -> { button.setOnClickListener(v -> {
try { try {
Setting<?> setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey())); String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
String defaultStringValue = setting.defaultValue.toString();
EditText editText = getEditText(); EditText editText = getEditText();
editText.setText(defaultStringValue); editText.setText(defaultStringValue);
editText.setSelection(defaultStringValue.length()); // move cursor to end of text editText.setSelection(defaultStringValue.length()); // move cursor to end of text

View File

@ -60,6 +60,7 @@ public class Utils {
private static WeakReference<Activity> activityRef = new WeakReference<>(null); private static WeakReference<Activity> activityRef = new WeakReference<>(null);
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
private static volatile Context context; private static volatile Context context;
private static Locale contextLocale;
protected Utils() { protected Utils() {
} // utility class } // utility class
@ -308,34 +309,51 @@ public class Utils {
* @return Context with locale applied. * @return Context with locale applied.
*/ */
public static Context getLocalizedContext(Context mContext) { public static Context getLocalizedContext(Context mContext) {
Activity mActivity = activityRef.get(); try {
if (mActivity == null) { Activity mActivity = activityRef.get();
return mContext; if (mActivity != null && mContext != null) {
} AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
if (mContext == null) {
return null; // Locale of Application.
Locale applicationLocale = language == AppLanguage.DEFAULT
? mActivity.getResources().getConfiguration().locale
: language.getLocale();
// Locale of Context.
Locale contextLocale = mContext.getResources().getConfiguration().locale;
// If they are different, overrides the Locale of the Context and resource.
if (applicationLocale != contextLocale) {
Utils.contextLocale = contextLocale;
// If they are different, overrides the Locale of the Context and resource.
Locale.setDefault(applicationLocale);
Configuration configuration = new Configuration(mContext.getResources().getConfiguration());
configuration.setLocale(applicationLocale);
return mContext.createConfigurationContext(configuration);
}
}
} catch (Exception ex) {
Logger.printException(() -> "getLocalizedContext failed", ex);
} }
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get(); return mContext;
}
// Locale of Application. public static void resetLocalizedContext() {
Locale applicationLocale = language == AppLanguage.DEFAULT try {
? mActivity.getResources().getConfiguration().locale if (contextLocale != null) {
: language.getLocale(); Locale.setDefault(contextLocale);
Context mContext = getContext();
// Locale of Context. if (mContext != null) {
Locale contextLocale = mContext.getResources().getConfiguration().locale; Configuration config = mContext.getResources().getConfiguration();
config.setLocale(contextLocale);
// If they are identical, no need to override them. setContext(mContext.createConfigurationContext(config));
if (applicationLocale == contextLocale) { }
return mContext; }
} catch (Exception ex) {
Logger.printException(() -> "resetLocalizedContext failed", ex);
} }
// If they are different, overrides the Locale of the Context and resource.
Locale.setDefault(applicationLocale);
Configuration configuration = new Configuration(mContext.getResources().getConfiguration());
configuration.setLocale(applicationLocale);
return mContext.createConfigurationContext(configuration);
} }
public static void setActivity(Activity mainActivity) { public static void setActivity(Activity mainActivity) {
@ -353,14 +371,6 @@ public class Utils {
// Must initially set context to check the app language. // Must initially set context to check the app language.
context = appContext; context = appContext;
Logger.initializationInfo(Utils.class, "Set context: " + appContext); Logger.initializationInfo(Utils.class, "Set context: " + appContext);
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
if (language != AppLanguage.DEFAULT) {
// Create a new context with the desired language.
Configuration config = appContext.getResources().getConfiguration();
config.setLocale(language.getLocale());
context = appContext.createConfigurationContext(config);
}
} }
public static void setClipboard(@NonNull String text) { public static void setClipboard(@NonNull String text) {
@ -538,14 +548,6 @@ public class Utils {
return Build.VERSION.SDK_INT >= sdk; return Build.VERSION.SDK_INT >= sdk;
} }
public static int dpToPx(float dp) {
if (context == null) {
return (int) dp;
} else {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
}
}
public static int dpToPx(int dp) { public static int dpToPx(int dp) {
if (context == null) { if (context == null) {
return dp; return dp;
@ -608,10 +610,10 @@ public class Utils {
* <br> * <br>
* Be aware the on start action can be called multiple times for some situations, * Be aware the on start action can be called multiple times for some situations,
* such as the user switching apps without dismissing the dialog then switching back to this app. * such as the user switching apps without dismissing the dialog then switching back to this app.
*<br> * <br>
* This method is only useful during app startup and multiple patches may show their own dialog, * This method is only useful during app startup and multiple patches may show their own dialog,
* and the most important dialog can be called last (using a delay) so it's always on top. * and the most important dialog can be called last (using a delay) so it's always on top.
*<br> * <br>
* For all other situations it's better to not use this method and * For all other situations it's better to not use this method and
* call {@link AlertDialog#show()} on the dialog. * call {@link AlertDialog#show()} on the dialog.
*/ */

View File

@ -6,7 +6,6 @@ import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup;
import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList;
import app.revanced.extension.shared.patches.components.Filter; import app.revanced.extension.shared.patches.components.Filter;
import app.revanced.extension.shared.patches.components.StringFilterGroup; import app.revanced.extension.shared.patches.components.StringFilterGroup;
import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused") @SuppressWarnings("unused")

View File

@ -6,7 +6,6 @@ import app.revanced.extension.shared.settings.Setting.Availability
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.youtube.settings.Settings import app.revanced.extension.youtube.settings.Settings
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import kotlin.Boolean
@Suppress("unused") @Suppress("unused")
object ChangeStartPagePatch { object ChangeStartPagePatch {
@ -44,7 +43,7 @@ object ChangeStartPagePatch {
} }
appLaunched = true appLaunched = true
Logger.printDebug{ "Changing browseId to $browseId" } Logger.printDebug { "Changing browseId to $browseId" }
return browseId return browseId
} }

View File

@ -39,7 +39,7 @@ public final class DownloadActionsPatch {
* <p> * <p>
* Appears to always be called from the main thread. * Appears to always be called from the main thread.
*/ */
public static boolean inAppVideoDownloadButtonOnClick(@Nullable Map<Object, Object> map,Object offlineVideoEndpointOuterClass, public static boolean inAppVideoDownloadButtonOnClick(@Nullable Map<Object, Object> map, Object offlineVideoEndpointOuterClass,
@Nullable String videoId) { @Nullable String videoId) {
try { try {
if (OVERRIDE_VIDEO_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(videoId)) { if (OVERRIDE_VIDEO_DOWNLOAD_BUTTON && StringUtils.isNotEmpty(videoId)) {

View File

@ -119,9 +119,9 @@ public class GeneralPatch {
Settings.DISABLE_LAYOUT_UPDATES.get(); Settings.DISABLE_LAYOUT_UPDATES.get();
/** /**
* @param key Keys to be added to the header of CronetBuilder. * @param key Keys to be added to the header of CronetBuilder.
* @param value Values 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. * @return Empty value if setting is enabled.
*/ */
public static String disableLayoutUpdates(String key, String value) { public static String disableLayoutUpdates(String key, String value) {
if (DISABLE_LAYOUT_UPDATES && StringUtils.equalsAny(key, REQUEST_HEADER_KEYS)) { if (DISABLE_LAYOUT_UPDATES && StringUtils.equalsAny(key, REQUEST_HEADER_KEYS)) {

View File

@ -43,8 +43,8 @@ public final class OpenChannelOfLiveAvatarPatch {
/** /**
* Injection point. * Injection point.
* *
* @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor * @param playbackStartDescriptorMap map containing information about PlaybackStartDescriptor
* @param newlyLoadedVideoId id of the current video * @param newlyLoadedVideoId id of the current video
*/ */
public static void fetchChannelId(@NonNull Map<Object, Object> playbackStartDescriptorMap, String newlyLoadedVideoId) { public static void fetchChannelId(@NonNull Map<Object, Object> playbackStartDescriptorMap, String newlyLoadedVideoId) {
try { try {

View File

@ -2,8 +2,10 @@ package app.revanced.extension.youtube.patches.general.requests
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeWebClient import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createWebInnertubeBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_VIDEO_DETAILS
import app.revanced.extension.shared.requests.Requester import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
@ -86,12 +88,11 @@ class VideoDetailsRequest private constructor(
Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching video details request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_VIDEO_DETAILS, GET_VIDEO_DETAILS,
clientType clientType
) )
val requestBody = val requestBody = createWebInnertubeBody(clientType, videoId)
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)

View File

@ -15,7 +15,16 @@ public class BackgroundPlaybackPatch {
*/ */
public static boolean isBackgroundPlaybackAllowed(boolean original) { public static boolean isBackgroundPlaybackAllowed(boolean original) {
if (original) return true; if (original) return true;
return ShortsPlayerState.getCurrent().isClosed(); return ShortsPlayerState.getCurrent().isClosed() &&
// 1. Shorts background playback is enabled.
// 2. Autoplay in feed is turned on.
// 3. Play Shorts from feed.
// 4. Media controls appear in status bar.
// (For unpatched YouTube with Premium accounts, media controls do not appear in the status bar)
//
// This is just a visual bug and does not affect Shorts background play in any way.
// To fix this, just check PlayerType.
PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL;
} }
/** /**

View File

@ -1,5 +1,7 @@
package app.revanced.extension.youtube.patches.player; package app.revanced.extension.youtube.patches.player;
import static app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton.REMIX;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
@ -8,8 +10,6 @@ import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import static app.revanced.extension.youtube.patches.player.ActionButtonsPatch.ActionButton.*;
import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
@ -106,8 +106,8 @@ public class ActionButtonsPatch {
/** /**
* Injection point. * Injection point.
* *
* @param list Type list of litho components * @param list Type list of litho components
* @param identifier Identifier of litho components * @param identifier Identifier of litho components
*/ */
public static List<Object> hideActionButtonByIndex(@Nullable List<Object> list, @Nullable String identifier) { public static List<Object> hideActionButtonByIndex(@Nullable List<Object> list, @Nullable String identifier) {
try { try {

View File

@ -24,7 +24,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.IntegerSetting; import app.revanced.extension.shared.settings.IntegerSetting;
import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils; import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
@ -34,7 +33,6 @@ import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.EngagementPanel; import app.revanced.extension.youtube.shared.EngagementPanel;
import app.revanced.extension.youtube.shared.PlayerType; import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.RootView; import app.revanced.extension.youtube.shared.RootView;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.shared.VideoInformation; import app.revanced.extension.youtube.shared.VideoInformation;
import app.revanced.extension.youtube.utils.VideoUtils; import app.revanced.extension.youtube.utils.VideoUtils;
@ -185,8 +183,8 @@ public class PlayerPatch {
// The type of descriptionView can be either ViewGroup or TextView. (A/B tests) // The type of descriptionView can be either ViewGroup or TextView. (A/B tests)
// If the type of descriptionView is TextView, longer delay is required. // If the type of descriptionView is TextView, longer delay is required.
final long delayMillis = descriptionView instanceof TextView final long delayMillis = descriptionView instanceof TextView
? 500 ? 750
: 100; : 200;
Utils.runOnMainThreadDelayed(() -> Utils.clickView(descriptionView), delayMillis); Utils.runOnMainThreadDelayed(() -> Utils.clickView(descriptionView), delayMillis);
} }
@ -441,7 +439,7 @@ public class PlayerPatch {
if (isLiveChatOrPlaylistPanel) { if (isLiveChatOrPlaylistPanel) {
return true; return true;
} }
return isAutoPopupPanel && ShortsPlayerState.getCurrent().isClosed(); return isAutoPopupPanel && !RootView.isShortsActive();
} }
/** /**
@ -471,8 +469,8 @@ public class PlayerPatch {
* Used in YouTube 20.05.46+. * Used in YouTube 20.05.46+.
*/ */
public static void disableAutoPlayerPopupPanels(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, public static void disableAutoPlayerPopupPanels(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get() && newVideoStarted.compareAndSet(false, true)) { if (Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get() && newVideoStarted.compareAndSet(false, true)) {
Utils.runOnMainThreadDelayed(() -> newVideoStarted.compareAndSet(true, false), 3000L); Utils.runOnMainThreadDelayed(() -> newVideoStarted.compareAndSet(true, false), 3000L);
} }

View File

@ -47,7 +47,7 @@ public class SeekbarColorPatch {
/** /**
* Empty seekbar gradient, if hide seekbar in feed is enabled. * Empty seekbar gradient, if hide seekbar in feed is enabled.
*/ */
private static final int[] HIDDEN_SEEKBAR_GRADIENT_COLORS = { 0x0, 0x0 }; private static final int[] HIDDEN_SEEKBAR_GRADIENT_COLORS = {0x0, 0x0};
/** /**
* Default YouTube seekbar color brightness. * Default YouTube seekbar color brightness.

View File

@ -1,8 +1,10 @@
package app.revanced.extension.youtube.patches.player.requests package app.revanced.extension.youtube.patches.player.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_VIDEO_ACTION_BUTTON
import app.revanced.extension.shared.requests.Requester import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
@ -20,10 +22,10 @@ import java.util.concurrent.TimeoutException
class ActionButtonRequest private constructor( class ActionButtonRequest private constructor(
private val videoId: String, private val videoId: String,
private val playerHeaders: Map<String, String>, private val requestHeader: Map<String, String>,
) { ) {
private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread { private val future: Future<Array<ActionButton>> = Utils.submitOnBackgroundThread {
fetch(videoId, playerHeaders) fetch(videoId, requestHeader)
} }
val array: Array<ActionButton> val array: Array<ActionButton>
@ -52,14 +54,6 @@ class ActionButtonRequest private constructor(
} }
companion object { companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -73,11 +67,11 @@ class ActionButtonRequest private constructor(
}) })
@JvmStatic @JvmStatic
fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map<String, String>) { fun fetchRequestIfNeeded(videoId: String, requestHeader: Map<String, String>) {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
synchronized(cache) { synchronized(cache) {
if (!cache.containsKey(videoId)) { if (!cache.containsKey(videoId)) {
cache[videoId] = ActionButtonRequest(videoId, playerHeaders) cache[videoId] = ActionButtonRequest(videoId, requestHeader)
} }
} }
} }
@ -93,43 +87,28 @@ class ActionButtonRequest private constructor(
Logger.printInfo({ toastMessage }, ex) Logger.printInfo({ toastMessage }, ex)
} }
private val REQUEST_HEADER_KEYS = arrayOf( private fun sendRequest(videoId: String, requestHeader: Map<String, String>): JSONObject? {
"Authorization", // Available only to logged-in users.
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
)
private fun sendRequest(videoId: String, playerHeaders: Map<String, String>): JSONObject? {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
// '/next' request does not require PoToken. // '/next' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(
PlayerRoutes.GET_VIDEO_ACTION_BUTTON,
clientType
)
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
// Since [THANKS] button and [CLIP] button are shown only with the logged in, // Since [THANKS] button and [CLIP] button are shown only with the logged in,
// Set the [Authorization] field to property to get the correct action buttons. // Set the [Authorization] field to property to get the correct action buttons.
for (key in REQUEST_HEADER_KEYS) { val connection = getInnerTubeResponseConnectionFromRoute(
var value = playerHeaders[key] GET_VIDEO_ACTION_BUTTON,
if (value != null) { clientType,
connection.setRequestProperty(key, value) requestHeader,
} )
}
val requestBody = val requestBody = createApplicationRequestBody(
PlayerRoutes.createApplicationRequestBody( clientType = clientType,
clientType = clientType, videoId = videoId
videoId = videoId )
)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -214,8 +193,11 @@ class ActionButtonRequest private constructor(
return emptyArray() return emptyArray()
} }
private fun fetch(videoId: String, playerHeaders: Map<String, String>): Array<ActionButton> { private fun fetch(
val json = sendRequest(videoId, playerHeaders) videoId: String,
requestHeader: Map<String, String>
): Array<ActionButton> {
val json = sendRequest(videoId, requestHeader)
if (json != null) { if (json != null) {
return parseResponse(json) return parseResponse(json)
} }

View File

@ -2,6 +2,7 @@ package app.revanced.extension.youtube.patches.shorts;
import static app.revanced.extension.shared.utils.ResourceUtils.getString; import static app.revanced.extension.shared.utils.ResourceUtils.getString;
import static app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter.isShortsFlyoutMenuVisible; import static app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter.isShortsFlyoutMenuVisible;
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan; import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan;
import android.content.Context; import android.content.Context;
@ -30,7 +31,6 @@ import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter; import app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.utils.ExtendedUtils; import app.revanced.extension.youtube.utils.ExtendedUtils;
import app.revanced.extension.youtube.utils.VideoUtils; import app.revanced.extension.youtube.utils.VideoUtils;
@ -55,7 +55,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) { if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) {
return; return;
} }
if (ShortsPlayerState.getCurrent().isClosed()) { if (!isShortsActive()) {
return; return;
} }
if (!isMoreButton(enumString)) { if (!isMoreButton(enumString)) {
@ -118,7 +118,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) { if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
return; return;
} }
if (ShortsPlayerState.getCurrent().isClosed()) { if (!isShortsActive()) {
return; return;
} }
if (bottomSheetMenuObject == null) { if (bottomSheetMenuObject == null) {
@ -136,7 +136,7 @@ public final class CustomActionsPatch {
if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) { if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) {
return; return;
} }
if (ShortsPlayerState.getCurrent().isClosed()) { if (!isShortsActive()) {
return; return;
} }
for (CustomAction customAction : CustomAction.values()) { for (CustomAction customAction : CustomAction.values()) {
@ -155,6 +155,34 @@ public final class CustomActionsPatch {
Logger.printInfo(() -> customAction.name() + bottomSheetMenuClass + bottomSheetMenuList + bottomSheetMenuObject); Logger.printInfo(() -> customAction.name() + bottomSheetMenuClass + bottomSheetMenuList + bottomSheetMenuObject);
} }
/**
* Injection point.
*/
public static boolean onBottomSheetMenuItemClick(View view) {
try {
if (view instanceof ViewGroup viewGroup) {
TextView textView = Utils.getChildView(viewGroup, v -> v instanceof TextView);
if (textView != null) {
String menuTitle = textView.getText().toString();
for (CustomAction customAction : CustomAction.values()) {
if (customAction.getLabel().equals(menuTitle)) {
View.OnLongClickListener onLongClick = customAction.getOnLongClickListener();
if (onLongClick != null) {
view.setOnLongClickListener(onLongClick);
}
customAction.getOnClickAction().run();
return true;
}
}
}
}
} catch (Exception ex) {
Logger.printException(() -> "onBottomSheetMenuItemClick failed");
}
return false;
}
/** /**
* Injection point. * Injection point.
*/ */
@ -164,7 +192,7 @@ public final class CustomActionsPatch {
} }
recyclerView.getViewTreeObserver().addOnDrawListener(() -> { recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
try { try {
if (ShortsPlayerState.getCurrent().isClosed()) { if (!isShortsActive()) {
return; return;
} }
contextRef = new WeakReference<>(recyclerView.getContext()); contextRef = new WeakReference<>(recyclerView.getContext());
@ -179,8 +207,9 @@ public final class CustomActionsPatch {
if (recyclerView.getChildAt(childCount - i - 1) instanceof ViewGroup parentViewGroup) { if (recyclerView.getChildAt(childCount - i - 1) instanceof ViewGroup parentViewGroup) {
childCount = recyclerView.getChildCount(); childCount = recyclerView.getChildCount();
if (childCount > 3 && parentViewGroup.getChildAt(1) instanceof TextView textView) { if (childCount > 3 && parentViewGroup.getChildAt(1) instanceof TextView textView) {
String menuTitle = textView.getText().toString();
for (CustomAction customAction : CustomAction.values()) { for (CustomAction customAction : CustomAction.values()) {
if (customAction.getLabel().equals(textView.getText().toString())) { if (customAction.getLabel().equals(menuTitle)) {
View.OnClickListener onClick = customAction.getOnClickListener(); View.OnClickListener onClick = customAction.getOnClickListener();
View.OnLongClickListener onLongClick = customAction.getOnLongClickListener(); View.OnLongClickListener onLongClick = customAction.getOnLongClickListener();
recyclerViewRef = new WeakReference<>(recyclerView); recyclerViewRef = new WeakReference<>(recyclerView);
@ -296,6 +325,11 @@ public final class CustomActionsPatch {
true true
) )
), ),
SPEED_DIALOG(
Settings.SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG,
"yt_outline_play_arrow_half_circle_black_24",
() -> VideoUtils.showPlaybackSpeedDialog(contextRef.get())
),
REPEAT_STATE( REPEAT_STATE(
Settings.SHORTS_CUSTOM_ACTIONS_REPEAT_STATE, Settings.SHORTS_CUSTOM_ACTIONS_REPEAT_STATE,
"yt_outline_arrow_repeat_1_black_24", "yt_outline_arrow_repeat_1_black_24",

View File

@ -30,8 +30,8 @@ public class ShortsRepeatStatePatch {
END_SCREEN; END_SCREEN;
static void setYTEnumValue(Enum<?> ytBehavior) { static void setYTEnumValue(Enum<?> ytBehavior) {
String ytName = ytBehavior.name();
for (ShortsLoopBehavior rvBehavior : values()) { for (ShortsLoopBehavior rvBehavior : values()) {
String ytName = ytBehavior.name();
if (ytName.endsWith(rvBehavior.name())) { if (ytName.endsWith(rvBehavior.name())) {
if (rvBehavior.ytEnumValue != null) { if (rvBehavior.ytEnumValue != null) {
Logger.printException(() -> "Conflicting behavior names: " + rvBehavior Logger.printException(() -> "Conflicting behavior names: " + rvBehavior
@ -87,24 +87,24 @@ public class ShortsRepeatStatePatch {
@Nullable @Nullable
public static Enum<?> changeShortsRepeatBehavior(@Nullable Enum<?> original) { public static Enum<?> changeShortsRepeatBehavior(@Nullable Enum<?> original) {
try { try {
if (original == null) {
Logger.printDebug(() -> "Original is null, returning null");
return null;
}
ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER && ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER &&
isAppInBackgroundPiPMode() isAppInBackgroundPiPMode()
? Settings.CHANGE_SHORTS_BACKGROUND_REPEAT_STATE.get() ? Settings.CHANGE_SHORTS_BACKGROUND_REPEAT_STATE.get()
: Settings.CHANGE_SHORTS_REPEAT_STATE.get(); : Settings.CHANGE_SHORTS_REPEAT_STATE.get();
Enum<?> overrideBehavior = behavior.ytEnumValue; Enum<?> overrideBehavior = behavior.ytEnumValue;
if (overrideBehavior != null) { if (behavior != ShortsLoopBehavior.UNKNOWN && overrideBehavior != null) {
Logger.printDebug(() -> overrideBehavior == original Logger.printDebug(() -> {
? "Behavior setting is same as original. Using original: " + original.name() String name = original == null ? "unknown (null)" : original.name();
: "Changing Shorts repeat behavior from: " + original.name() + " to: " + overrideBehavior.name() return overrideBehavior == original
); ? "Behavior setting is same as original. Using original: " + name
: "Changing Shorts repeat behavior from: " + name + " to: " + overrideBehavior.name();
});
return overrideBehavior; // For some reason, in YouTube 20.09+, 'UNKNOWN' functions as 'Pause'.
return ExtendedUtils.IS_20_09_OR_GREATER && behavior == ShortsLoopBehavior.END_SCREEN
? ShortsLoopBehavior.UNKNOWN.ytEnumValue
: overrideBehavior;
} }
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "changeShortsRepeatBehavior failure", ex); Logger.printException(() -> "changeShortsRepeatBehavior failure", ex);
@ -117,6 +117,6 @@ public class ShortsRepeatStatePatch {
* Injection point. * Injection point.
*/ */
public static boolean isAutoPlay(@Nullable Enum<?> original) { public static boolean isAutoPlay(@Nullable Enum<?> original) {
return original != null && ShortsLoopBehavior.SINGLE_PLAY.ytEnumValue == original; return ShortsLoopBehavior.SINGLE_PLAY.ytEnumValue == original;
} }
} }

View File

@ -4,6 +4,7 @@ import android.view.View;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"unused", "deprecation"}) @SuppressWarnings({"unused", "deprecation"})
@ -59,4 +60,20 @@ public class SwipeControlsPatch {
return engagementOverlayView != null && engagementOverlayView.getVisibility() == View.VISIBLE; return engagementOverlayView != null && engagementOverlayView.getVisibility() == View.VISIBLE;
} }
public static final class SwipeOverlayTextSizeAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return (Settings.ENABLE_SWIPE_BRIGHTNESS.get() || Settings.ENABLE_SWIPE_VOLUME.get()) &&
!Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get();
}
}
public static final class SwipeOverlayModernUIAvailability implements Setting.Availability {
@Override
public boolean isAvailable() {
return (Settings.ENABLE_SWIPE_BRIGHTNESS.get() || Settings.ENABLE_SWIPE_VOLUME.get()) &&
Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get();
}
}
} }

View File

@ -9,6 +9,11 @@ public class PatchStatus {
return false; return false;
} }
// Modified by a patch. Do not touch.
public static boolean OldSeekbarThumbnailsDefaultBoolean() {
return false;
}
public static boolean OldSplashAnimation() { public static boolean OldSplashAnimation() {
// Replace this with true if the Restore old splash animation (Custom branding icon) succeeds // Replace this with true if the Restore old splash animation (Custom branding icon) succeeds
return false; return false;
@ -40,23 +45,22 @@ public class PatchStatus {
return false; return false;
} }
public static String SpoofAppVersionDefaultString() {
return "18.17.43";
}
public static boolean ToolBarComponents() { public static boolean ToolBarComponents() {
// Replace this with true if the Toolbar components patch succeeds // Replace this with true if the Toolbar components patch succeeds
return false; return false;
} }
public static long PatchedTime() {
return 0L;
}
public static String SpoofAppVersionDefaultString() {
return "18.17.43";
}
// Modified by a patch. Do not touch. // Modified by a patch. Do not touch.
public static String RVXMusicPackageName() { public static String RVXMusicPackageName() {
return "com.google.android.apps.youtube.music"; return "com.google.android.apps.youtube.music";
} }
// Modified by a patch. Do not touch.
public static boolean OldSeekbarThumbnailsDefaultBoolean() {
return false;
}
} }

View File

@ -1,20 +1,24 @@
package app.revanced.extension.youtube.patches.utils; package app.revanced.extension.youtube.patches.utils;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.youtube.shared.EngagementPanel;
import app.revanced.extension.youtube.shared.PlayerType; import app.revanced.extension.youtube.shared.PlayerType;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class PlaybackSpeedWhilePlayingPatch { public class PlaybackSpeedWhilePlayingPatch {
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
public static boolean playbackSpeedChanged(float playbackSpeed) { public static boolean playbackSpeedChanged(float playbackSpeed) {
PlayerType playerType = PlayerType.getCurrent(); if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED) {
if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED && if (PlayerType.getCurrent().isMaximizedOrFullscreenOrPiP()
playerType.isMaximizedOrFullscreenOrPiP()) { // Since RVX has a default playback speed setting for Shorts,
// Playback speed reset should also be prevented in Shorts.
|| ShortsPlayerState.getCurrent().isOpen() && EngagementPanel.isOpen()) {
Logger.printDebug(() -> "Ignore changing playback speed, as it is invalid request");
Logger.printDebug(() -> "Ignore changing playback speed, as it is invalid request: " + playerType.name()); return true;
}
return true;
} }
return false; return false;

View File

@ -1,5 +1,11 @@
package app.revanced.extension.youtube.patches.utils; 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.content.Context;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.widget.LinearLayout; import android.widget.LinearLayout;
@ -8,6 +14,8 @@ import android.widget.ScrollView;
import androidx.annotation.GuardedBy; import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull; 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;
import org.apache.commons.collections4.bidimap.DualHashBidiMap; import org.apache.commons.collections4.bidimap.DualHashBidiMap;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
@ -26,62 +34,61 @@ 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.GetPlaylistsRequest;
import app.revanced.extension.youtube.patches.utils.requests.SavePlaylistRequest; import app.revanced.extension.youtube.patches.utils.requests.SavePlaylistRequest;
import app.revanced.extension.youtube.settings.Settings; 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 app.revanced.extension.youtube.utils.ExtendedUtils;
import app.revanced.extension.youtube.utils.VideoUtils;
import kotlin.Pair; import kotlin.Pair;
// TODO: Implement sync queue and clean up code. // TODO: Implement sync queue and clean up code.
@SuppressWarnings({"unused", "StaticFieldLeak"}) @SuppressWarnings({"unused", "StaticFieldLeak"})
public class PlaylistPatch extends VideoUtils { public class PlaylistPatch extends AuthUtils {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String[] REQUEST_HEADER_KEYS = {
AUTHORIZATION_HEADER,
"X-GOOG-API-FORMAT-VERSION",
"X-Goog-Visitor-Id"
};
private static final boolean QUEUE_MANAGER = private static final boolean QUEUE_MANAGER =
Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER_QUEUE_MANAGER.get() Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER_QUEUE_MANAGER.get()
|| Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER.get(); || Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON_QUEUE_MANAGER.get();
private static Context mContext; private static Context mContext;
private static volatile String authorization = "";
private static volatile boolean isIncognito = false;
private static volatile Map<String, String> requestHeader;
private static volatile String playlistId = "";
private static volatile String videoId = "";
private static final String checkFailedAuth = private static String checkFailedAuth;
ResourceUtils.getString("revanced_queue_manager_check_failed_auth"); private static String checkFailedPlaylistId;
private static final String checkFailedPlaylistId = private static String checkFailedQueue;
ResourceUtils.getString("revanced_queue_manager_check_failed_playlist_id"); private static String checkFailedVideoId;
private static final String checkFailedQueue = private static String checkFailedGeneric;
ResourceUtils.getString("revanced_queue_manager_check_failed_queue");
private static final String checkFailedVideoId =
ResourceUtils.getString("revanced_queue_manager_check_failed_video_id");
private static final String checkFailedGeneric =
ResourceUtils.getString("revanced_queue_manager_check_failed_generic");
private static final String fetchFailedAdd = private static String fetchFailedAdd;
ResourceUtils.getString("revanced_queue_manager_fetch_failed_add"); private static String fetchFailedCreate;
private static final String fetchFailedCreate = private static String fetchFailedDelete;
ResourceUtils.getString("revanced_queue_manager_fetch_failed_create"); private static String fetchFailedRemove;
private static final String fetchFailedDelete = private static String fetchFailedSave;
ResourceUtils.getString("revanced_queue_manager_fetch_failed_delete");
private static final String fetchFailedRemove =
ResourceUtils.getString("revanced_queue_manager_fetch_failed_remove");
private static final String fetchFailedSave =
ResourceUtils.getString("revanced_queue_manager_fetch_failed_save");
private static final String fetchSucceededAdd = private static String fetchSucceededAdd;
ResourceUtils.getString("revanced_queue_manager_fetch_succeeded_add"); private static String fetchSucceededCreate;
private static final String fetchSucceededCreate = private static String fetchSucceededDelete;
ResourceUtils.getString("revanced_queue_manager_fetch_succeeded_create"); private static String fetchSucceededRemove;
private static final String fetchSucceededDelete = private static String fetchSucceededSave;
ResourceUtils.getString("revanced_queue_manager_fetch_succeeded_delete");
private static final String fetchSucceededRemove = static {
ResourceUtils.getString("revanced_queue_manager_fetch_succeeded_remove"); Context mContext = Utils.getContext();
private static final String fetchSucceededSave = if (mContext != null && mContext.getResources() != null) {
ResourceUtils.getString("revanced_queue_manager_fetch_succeeded_save"); 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") @GuardedBy("itself")
private static final BidiMap<String, String> lastVideoIds = new DualHashBidiMap<>(); private static final BidiMap<String, String> lastVideoIds = new DualHashBidiMap<>();
@ -111,6 +118,7 @@ public class PlaylistPatch extends VideoUtils {
if (videoId != null) { if (videoId != null) {
lastVideoIds.remove(videoId, setVideoId); lastVideoIds.remove(videoId, setVideoId);
EditPlaylistRequest.clearVideoId(videoId); EditPlaylistRequest.clearVideoId(videoId);
Logger.printDebug(() -> "Video removed by YouTube flyout menu: " + videoId);
} }
} }
} }
@ -119,33 +127,9 @@ public class PlaylistPatch extends VideoUtils {
/** /**
* Injection point. * Injection point.
*/ */
public static void setIncognitoStatus(boolean incognito) { public static void setPivotBar(PivotBar view) {
if (QUEUE_MANAGER) { if (QUEUE_MANAGER) {
isIncognito = incognito; mContext = view.getContext();
}
}
/**
* Injection point.
*/
public static void setRequestHeaders(String url, Map<String, String> requestHeaders) {
if (QUEUE_MANAGER) {
try {
// Save requestHeaders whenever an account is switched.
String auth = requestHeaders.get(AUTHORIZATION_HEADER);
if (auth == null || authorization.equals(auth)) {
return;
}
for (String key : REQUEST_HEADER_KEYS) {
if (requestHeaders.get(key) == null) {
return;
}
}
authorization = auth;
requestHeader = requestHeaders;
} catch (Exception ex) {
Logger.printException(() -> "setRequestHeaders failure", ex);
}
} }
} }
@ -160,7 +144,7 @@ public class PlaylistPatch extends VideoUtils {
* Invoked by extension. * Invoked by extension.
*/ */
public static void prepareDialogBuilder(@NonNull String currentVideoId) { public static void prepareDialogBuilder(@NonNull String currentVideoId) {
if (authorization.isEmpty() || isIncognito) { if (authorization.isEmpty() || (dataSyncId.isEmpty() && isIncognito)) {
handleCheckError(checkFailedAuth); handleCheckError(checkFailedAuth);
return; return;
} }
@ -169,9 +153,22 @@ public class PlaylistPatch extends VideoUtils {
} else { } else {
videoId = currentVideoId; videoId = currentVideoId;
synchronized (lastVideoIds) { synchronized (lastVideoIds) {
QueueManager[] customActionsEntries = playlistId.isEmpty() || lastVideoIds.get(currentVideoId) == null QueueManager[] customActionsEntries;
? QueueManager.addToQueueEntries boolean canReload = PlayerType.getCurrent().isMaximizedOrFullscreen() &&
: QueueManager.removeFromQueueEntries; 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); buildBottomSheetDialog(customActionsEntries);
} }
@ -200,13 +197,14 @@ public class PlaylistPatch extends VideoUtils {
ExtendedUtils.showBottomSheetDialog(mContext, mScrollView, actionsMap); ExtendedUtils.showBottomSheetDialog(mContext, mScrollView, actionsMap);
} }
private static void fetchQueue(boolean remove, boolean openPlaylist, boolean openVideo) { private static void fetchQueue(boolean remove, boolean openPlaylist,
boolean openVideo, boolean reload) {
try { try {
String currentPlaylistId = playlistId; String currentPlaylistId = playlistId;
String currentVideoId = videoId; String currentVideoId = videoId;
synchronized (lastVideoIds) { synchronized (lastVideoIds) {
if (currentPlaylistId.isEmpty()) { // Queue is empty, create new playlist. if (currentPlaylistId.isEmpty()) { // Queue is empty, create new playlist.
CreatePlaylistRequest.fetchRequestIfNeeded(currentVideoId, requestHeader); CreatePlaylistRequest.fetchRequestIfNeeded(currentVideoId, requestHeader, dataSyncId);
runOnMainThreadDelayed(() -> { runOnMainThreadDelayed(() -> {
CreatePlaylistRequest request = CreatePlaylistRequest.getRequestForVideoId(currentVideoId); CreatePlaylistRequest request = CreatePlaylistRequest.getRequestForVideoId(currentVideoId);
if (request != null) { if (request != null) {
@ -220,7 +218,7 @@ public class PlaylistPatch extends VideoUtils {
showToast(fetchSucceededCreate); showToast(fetchSucceededCreate);
Logger.printDebug(() -> "Queue successfully created, playlistId: " + createdPlaylistId + ", setVideoId: " + setVideoId); Logger.printDebug(() -> "Queue successfully created, playlistId: " + createdPlaylistId + ", setVideoId: " + setVideoId);
if (openPlaylist) { if (openPlaylist) {
openQueue(currentVideoId, openVideo); openQueue(currentVideoId, openVideo, reload);
} }
return; return;
} }
@ -230,7 +228,7 @@ public class PlaylistPatch extends VideoUtils {
}, 1000); }, 1000);
} else { // Queue is not empty, add or remove video. } else { // Queue is not empty, add or remove video.
String setVideoId = lastVideoIds.get(currentVideoId); String setVideoId = lastVideoIds.get(currentVideoId);
EditPlaylistRequest.fetchRequestIfNeeded(currentVideoId, currentPlaylistId, setVideoId, requestHeader); EditPlaylistRequest.fetchRequestIfNeeded(currentVideoId, currentPlaylistId, setVideoId, requestHeader, dataSyncId);
runOnMainThreadDelayed(() -> { runOnMainThreadDelayed(() -> {
EditPlaylistRequest request = EditPlaylistRequest.getRequestForVideoId(currentVideoId); EditPlaylistRequest request = EditPlaylistRequest.getRequestForVideoId(currentVideoId);
@ -238,22 +236,24 @@ public class PlaylistPatch extends VideoUtils {
String fetchedSetVideoId = request.getResult(); String fetchedSetVideoId = request.getResult();
Logger.printDebug(() -> "fetchedSetVideoId: " + fetchedSetVideoId); Logger.printDebug(() -> "fetchedSetVideoId: " + fetchedSetVideoId);
if (remove) { // Remove from queue. if (remove) { // Remove from queue.
if (StringUtils.isEmpty(fetchedSetVideoId)) { if ("".equals(fetchedSetVideoId)) {
lastVideoIds.remove(currentVideoId, setVideoId); lastVideoIds.remove(currentVideoId, setVideoId);
EditPlaylistRequest.clearVideoId(currentVideoId);
showToast(fetchSucceededRemove); showToast(fetchSucceededRemove);
if (openPlaylist) { if (openPlaylist) {
openQueue(currentVideoId, openVideo); openQueue(currentVideoId, openVideo, reload);
} }
return; return;
} }
showToast(fetchFailedRemove); showToast(fetchFailedRemove);
} else { // Add to queue. } else { // Add to queue.
if (StringUtils.isNotEmpty(fetchedSetVideoId)) { if (fetchedSetVideoId != null && !fetchedSetVideoId.isEmpty()) {
lastVideoIds.putIfAbsent(currentVideoId, fetchedSetVideoId); lastVideoIds.putIfAbsent(currentVideoId, fetchedSetVideoId);
EditPlaylistRequest.clearVideoId(currentVideoId);
showToast(fetchSucceededAdd); showToast(fetchSucceededAdd);
Logger.printDebug(() -> "Video successfully added, setVideoId: " + fetchedSetVideoId); Logger.printDebug(() -> "Video successfully added, setVideoId: " + fetchedSetVideoId);
if (openPlaylist) { if (openPlaylist) {
openQueue(currentVideoId, openVideo); openQueue(currentVideoId, openVideo, reload);
} }
return; return;
} }
@ -275,7 +275,7 @@ public class PlaylistPatch extends VideoUtils {
return; return;
} }
try { try {
GetPlaylistsRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader); GetPlaylistsRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader, dataSyncId);
runOnMainThreadDelayed(() -> { runOnMainThreadDelayed(() -> {
GetPlaylistsRequest request = GetPlaylistsRequest.getRequestForPlaylistId(currentPlaylistId); GetPlaylistsRequest request = GetPlaylistsRequest.getRequestForPlaylistId(currentPlaylistId);
if (request != null) { if (request != null) {
@ -317,7 +317,7 @@ public class PlaylistPatch extends VideoUtils {
handleCheckError(checkFailedPlaylistId); handleCheckError(checkFailedPlaylistId);
return; return;
} }
SavePlaylistRequest.fetchRequestIfNeeded(playlistId, libraryId, requestHeader); SavePlaylistRequest.fetchRequestIfNeeded(playlistId, libraryId, requestHeader, dataSyncId);
runOnMainThreadDelayed(() -> { runOnMainThreadDelayed(() -> {
SavePlaylistRequest request = SavePlaylistRequest.getRequestForLibraryId(libraryId); SavePlaylistRequest request = SavePlaylistRequest.getRequestForLibraryId(libraryId);
@ -343,7 +343,7 @@ public class PlaylistPatch extends VideoUtils {
return; return;
} }
try { try {
DeletePlaylistRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader); DeletePlaylistRequest.fetchRequestIfNeeded(currentPlaylistId, requestHeader, dataSyncId);
runOnMainThreadDelayed(() -> { runOnMainThreadDelayed(() -> {
DeletePlaylistRequest request = DeletePlaylistRequest.getRequestForPlaylistId(currentPlaylistId); DeletePlaylistRequest request = DeletePlaylistRequest.getRequestForPlaylistId(currentPlaylistId);
if (request != null) { if (request != null) {
@ -375,10 +375,10 @@ public class PlaylistPatch extends VideoUtils {
} }
private static void openQueue() { private static void openQueue() {
openQueue("", false); openQueue("", false, false);
} }
private static void openQueue(String currentVideoId, boolean openVideo) { private static void openQueue(String currentVideoId, boolean openVideo, boolean reload) {
String currentPlaylistId = playlistId; String currentPlaylistId = playlistId;
if (currentPlaylistId.isEmpty()) { if (currentPlaylistId.isEmpty()) {
handleCheckError(checkFailedQueue); handleCheckError(checkFailedQueue);
@ -390,7 +390,15 @@ public class PlaylistPatch extends VideoUtils {
return; return;
} }
// Open a video from a playlist // Open a video from a playlist
openPlaylist(currentPlaylistId, currentVideoId); 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 { } else {
// Open a playlist // Open a playlist
openPlaylist(currentPlaylistId); openPlaylist(currentPlaylistId);
@ -409,27 +417,37 @@ public class PlaylistPatch extends VideoUtils {
ADD_TO_QUEUE( ADD_TO_QUEUE(
"revanced_queue_manager_add_to_queue", "revanced_queue_manager_add_to_queue",
"yt_outline_list_add_black_24", "yt_outline_list_add_black_24",
() -> fetchQueue(false, false, false) () -> fetchQueue(false, false, false, false)
), ),
ADD_TO_QUEUE_AND_OPEN_QUEUE( ADD_TO_QUEUE_AND_OPEN_QUEUE(
"revanced_queue_manager_add_to_queue_and_open_queue", "revanced_queue_manager_add_to_queue_and_open_queue",
"yt_outline_list_add_black_24", "yt_outline_list_add_black_24",
() -> fetchQueue(false, true, false) () -> fetchQueue(false, true, false, false)
), ),
ADD_TO_QUEUE_AND_PLAY_VIDEO( ADD_TO_QUEUE_AND_PLAY_VIDEO(
"revanced_queue_manager_add_to_queue_and_play_video", "revanced_queue_manager_add_to_queue_and_play_video",
"yt_outline_list_play_arrow_black_24", "yt_outline_list_play_arrow_black_24",
() -> fetchQueue(false, true, true) () -> 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( REMOVE_FROM_QUEUE(
"revanced_queue_manager_remove_from_queue", "revanced_queue_manager_remove_from_queue",
"yt_outline_trash_can_black_24", "yt_outline_trash_can_black_24",
() -> fetchQueue(true, false, false) () -> fetchQueue(true, false, false, false)
), ),
REMOVE_FROM_QUEUE_AND_OPEN_QUEUE( REMOVE_FROM_QUEUE_AND_OPEN_QUEUE(
"revanced_queue_manager_remove_from_queue_and_open_queue", "revanced_queue_manager_remove_from_queue_and_open_queue",
"yt_outline_trash_can_black_24", "yt_outline_trash_can_black_24",
() -> fetchQueue(true, true, false) () -> 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( OPEN_QUEUE(
"revanced_queue_manager_open_queue", "revanced_queue_manager_open_queue",
@ -477,6 +495,17 @@ public class PlaylistPatch extends VideoUtils {
SAVE_QUEUE, 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 = { public static final QueueManager[] removeFromQueueEntries = {
REMOVE_FROM_QUEUE, REMOVE_FROM_QUEUE,
REMOVE_FROM_QUEUE_AND_OPEN_QUEUE, REMOVE_FROM_QUEUE_AND_OPEN_QUEUE,
@ -486,6 +515,16 @@ public class PlaylistPatch extends VideoUtils {
SAVE_QUEUE, 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 = { public static final QueueManager[] noVideoIdQueueEntries = {
OPEN_QUEUE, OPEN_QUEUE,
//REMOVE_QUEUE, //REMOVE_QUEUE,

View File

@ -1,12 +1,15 @@
package app.revanced.extension.youtube.patches.utils.requests package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
import app.revanced.extension.youtube.patches.utils.requests.CreatePlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -20,10 +23,15 @@ import java.util.concurrent.TimeoutException
class CreatePlaylistRequest private constructor( class CreatePlaylistRequest private constructor(
private val videoId: String, private val videoId: String,
private val playerHeaders: Map<String, String>, private val requestHeader: Map<String, String>,
private val dataSyncId: String,
) { ) {
private val future: Future<Pair<String, String>> = Utils.submitOnBackgroundThread { private val future: Future<Pair<String, String>> = Utils.submitOnBackgroundThread {
fetch(videoId, playerHeaders) fetch(
videoId,
requestHeader,
dataSyncId,
)
} }
val playlistId: Pair<String, String>? val playlistId: Pair<String, String>?
@ -52,14 +60,6 @@ class CreatePlaylistRequest private constructor(
} }
companion object { companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -80,11 +80,19 @@ class CreatePlaylistRequest private constructor(
} }
@JvmStatic @JvmStatic
fun fetchRequestIfNeeded(videoId: String, playerHeaders: Map<String, String>) { fun fetchRequestIfNeeded(
videoId: String,
requestHeader: Map<String, String>,
dataSyncId: String,
) {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
synchronized(cache) { synchronized(cache) {
if (!cache.containsKey(videoId)) { if (!cache.containsKey(videoId)) {
cache[videoId] = CreatePlaylistRequest(videoId, playerHeaders) cache[videoId] = CreatePlaylistRequest(
videoId,
requestHeader,
dataSyncId,
)
} }
} }
} }
@ -100,40 +108,28 @@ class CreatePlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex) Logger.printInfo({ toastMessage }, ex)
} }
private val REQUEST_HEADER_KEYS = arrayOf( private fun sendCreatePlaylistRequest(
"Authorization", // Available only to logged-in users. videoId: String,
"X-GOOG-API-FORMAT-VERSION", requestHeader: Map<String, String>,
"X-Goog-Visitor-Id" dataSyncId: String,
) ): JSONObject? {
private fun sendCreatePlaylistRequest(videoId: String, playerHeaders: Map<String, String>): JSONObject? {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
// 'playlist/create' request does not require PoToken. // 'playlist/create' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching create playlist request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching create playlist request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.CREATE_PLAYLIST, CREATE_PLAYLIST,
clientType clientType,
requestHeader,
dataSyncId,
) )
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) { val requestBody = createPlaylistRequestBody(videoId = videoId)
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody =
PlayerRoutes.createPlaylistRequestBody(
videoId = videoId
)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -159,36 +155,33 @@ class CreatePlaylistRequest private constructor(
return null return null
} }
private fun sendSetVideoIdRequest(videoId: String, playlistId: String, playerHeaders: Map<String, String>): JSONObject? { private fun sendSetVideoIdRequest(
videoId: String,
playlistId: String,
requestHeader: Map<String, String>,
dataSyncId: String,
): JSONObject? {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
// 'playlist/create' request does not require PoToken. // 'playlist/create' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching set video id request for: $playlistId, using client: $clientTypeName" } Logger.printDebug { "Fetching set video id request for: $playlistId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_SET_VIDEO_ID, GET_SET_VIDEO_ID,
clientType clientType,
requestHeader,
dataSyncId,
) )
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) { val requestBody = createApplicationRequestBody(
var value = playerHeaders[key] clientType = clientType,
if (value != null) { videoId = videoId,
connection.setRequestProperty(key, value) playlistId = playlistId
} )
}
val requestBody =
PlayerRoutes.createApplicationRequestBody(
clientType = clientType,
videoId = videoId,
playlistId = playlistId
)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -240,8 +233,8 @@ class CreatePlaylistRequest private constructor(
if (secondaryContentsJsonObject is JSONObject) { if (secondaryContentsJsonObject is JSONObject) {
return secondaryContentsJsonObject return secondaryContentsJsonObject
.getJSONObject("playlistPanelVideoRenderer") .getJSONObject("playlistPanelVideoRenderer")
.getString("playlistSetVideoId") .getString("playlistSetVideoId")
} }
} catch (e: JSONException) { } catch (e: JSONException) {
val jsonForMessage = json.toString() val jsonForMessage = json.toString()
@ -254,12 +247,25 @@ class CreatePlaylistRequest private constructor(
return null return null
} }
private fun fetch(videoId: String, playerHeaders: Map<String, String>): Pair<String, String>? { private fun fetch(
val createPlaylistJson = sendCreatePlaylistRequest(videoId, playerHeaders) videoId: String,
requestHeader: Map<String, String>,
dataSyncId: String,
): Pair<String, String>? {
val createPlaylistJson = sendCreatePlaylistRequest(
videoId,
requestHeader,
dataSyncId
)
if (createPlaylistJson != null) { if (createPlaylistJson != null) {
val playlistId = parseCreatePlaylistResponse(createPlaylistJson) val playlistId = parseCreatePlaylistResponse(createPlaylistJson)
if (playlistId != null) { if (playlistId != null) {
val setVideoIdJson = sendSetVideoIdRequest(videoId, playlistId, playerHeaders) val setVideoIdJson = sendSetVideoIdRequest(
videoId,
playlistId,
requestHeader,
dataSyncId
)
if (setVideoIdJson != null) { if (setVideoIdJson != null) {
val setVideoId = parseSetVideoIdResponse(setVideoIdJson) val setVideoId = parseSetVideoIdResponse(setVideoIdJson)
if (setVideoId != null) { if (setVideoId != null) {

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
import app.revanced.extension.youtube.patches.utils.requests.DeletePlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -20,12 +21,14 @@ import java.util.concurrent.TimeoutException
class DeletePlaylistRequest private constructor( class DeletePlaylistRequest private constructor(
private val playlistId: String, private val playlistId: String,
private val playerHeaders: Map<String, String>, private val requestHeader: Map<String, String>,
private val dataSyncId: String,
) { ) {
private val future: Future<Boolean> = Utils.submitOnBackgroundThread { private val future: Future<Boolean> = Utils.submitOnBackgroundThread {
fetch( fetch(
playlistId, playlistId,
playerHeaders, requestHeader,
dataSyncId,
) )
} }
@ -55,14 +58,6 @@ class DeletePlaylistRequest private constructor(
} }
companion object { companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -85,14 +80,16 @@ class DeletePlaylistRequest private constructor(
@JvmStatic @JvmStatic
fun fetchRequestIfNeeded( fun fetchRequestIfNeeded(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
) { ) {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
synchronized(cache) { synchronized(cache) {
if (!cache.containsKey(playlistId)) { if (!cache.containsKey(playlistId)) {
cache[playlistId] = DeletePlaylistRequest( cache[playlistId] = DeletePlaylistRequest(
playlistId, playlistId,
playerHeaders requestHeader,
dataSyncId,
) )
} }
} }
@ -109,40 +106,28 @@ class DeletePlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex) 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( private fun sendRequest(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
): JSONObject? { ): JSONObject? {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
// 'playlist/delete' request does not require PoToken. // 'playlist/delete' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching delete playlist request, playlistId: $playlistId, using client: $clientTypeName" } Logger.printDebug { "Fetching delete playlist request, playlistId: $playlistId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.DELETE_PLAYLIST, DELETE_PLAYLIST,
clientType, clientType,
requestHeader,
dataSyncId
) )
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) { val requestBody = deletePlaylistRequestBody(playlistId)
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody = PlayerRoutes.deletePlaylistRequestBody(playlistId)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -184,9 +169,14 @@ class DeletePlaylistRequest private constructor(
private fun fetch( private fun fetch(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
): Boolean? { ): Boolean? {
val json = sendRequest(playlistId, playerHeaders) val json = sendRequest(
playlistId,
requestHeader,
dataSyncId,
)
if (json != null) { if (json != null) {
return parseResponse(json) return parseResponse(json)
} }

View File

@ -1,13 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
import app.revanced.extension.youtube.patches.utils.requests.EditPlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.apache.commons.lang3.StringUtils
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -23,14 +23,16 @@ class EditPlaylistRequest private constructor(
private val videoId: String, private val videoId: String,
private val playlistId: String, private val playlistId: String,
private val setVideoId: String?, private val setVideoId: String?,
private val playerHeaders: Map<String, String>, private val requestHeader: Map<String, String>,
private val dataSyncId: String,
) { ) {
private val future: Future<String> = Utils.submitOnBackgroundThread { private val future: Future<String> = Utils.submitOnBackgroundThread {
fetch( fetch(
videoId, videoId,
playlistId, playlistId,
setVideoId, setVideoId,
playerHeaders, requestHeader,
dataSyncId,
) )
} }
@ -60,14 +62,6 @@ class EditPlaylistRequest private constructor(
} }
companion object { companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -99,7 +93,8 @@ class EditPlaylistRequest private constructor(
videoId: String, videoId: String,
playlistId: String, playlistId: String,
setVideoId: String?, setVideoId: String?,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
) { ) {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
synchronized(cache) { synchronized(cache) {
@ -108,7 +103,8 @@ class EditPlaylistRequest private constructor(
videoId, videoId,
playlistId, playlistId,
setVideoId, setVideoId,
playerHeaders requestHeader,
dataSyncId,
) )
} }
} }
@ -125,47 +121,34 @@ class EditPlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex) 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( private fun sendRequest(
videoId: String, videoId: String,
playlistId: String, playlistId: String,
setVideoId: String?, setVideoId: String?,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
): JSONObject? { ): JSONObject? {
Objects.requireNonNull(videoId) Objects.requireNonNull(videoId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
// 'browse/edit_playlist' request does not require PoToken. // 'browse/edit_playlist' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching edit playlist request, videoId: $videoId, playlistId: $playlistId, setVideoId: $setVideoId, using client: $clientTypeName" } Logger.printDebug { "Fetching edit playlist request, videoId: $videoId, playlistId: $playlistId, setVideoId: $setVideoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.EDIT_PLAYLIST, EDIT_PLAYLIST,
clientType clientType,
requestHeader,
dataSyncId
) )
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) { val requestBody = editPlaylistRequestBody(
var value = playerHeaders[key] videoId = videoId,
if (value != null) { playlistId = playlistId,
connection.setRequestProperty(key, value) setVideoId = setVideoId
} )
}
val requestBody =
PlayerRoutes.editPlaylistRequestBody(
videoId = videoId,
playlistId = playlistId,
setVideoId = setVideoId,
)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -197,7 +180,8 @@ class EditPlaylistRequest private constructor(
if (remove) { if (remove) {
return "" return ""
} }
val playlistEditResultsJSONObject = json.getJSONArray("playlistEditResults").get(0) val playlistEditResultsJSONObject =
json.getJSONArray("playlistEditResults").get(0)
if (playlistEditResultsJSONObject is JSONObject) { if (playlistEditResultsJSONObject is JSONObject) {
return playlistEditResultsJSONObject return playlistEditResultsJSONObject
@ -220,11 +204,18 @@ class EditPlaylistRequest private constructor(
videoId: String, videoId: String,
playlistId: String, playlistId: String,
setVideoId: String?, setVideoId: String?,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
): String? { ): String? {
val json = sendRequest(videoId, playlistId, setVideoId, playerHeaders) val json = sendRequest(
videoId,
playlistId,
setVideoId,
requestHeader,
dataSyncId,
)
if (json != null) { if (json != null) {
return parseResponse(json, StringUtils.isNotEmpty(setVideoId)) return parseResponse(json, setVideoId != null && setVideoId.isNotEmpty())
} }
return null return null

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
import app.revanced.extension.youtube.patches.utils.requests.GetPlaylistsRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -20,12 +21,14 @@ import java.util.concurrent.TimeoutException
class GetPlaylistsRequest private constructor( class GetPlaylistsRequest private constructor(
private val playlistId: String, private val playlistId: String,
private val playerHeaders: Map<String, String>, private val requestHeader: Map<String, String>,
private val dataSyncId: String,
) { ) {
private val future: Future<Array<Pair<String, String>>> = Utils.submitOnBackgroundThread { private val future: Future<Array<Pair<String, String>>> = Utils.submitOnBackgroundThread {
fetch( fetch(
playlistId, playlistId,
playerHeaders, requestHeader,
dataSyncId,
) )
} }
@ -55,14 +58,6 @@ class GetPlaylistsRequest private constructor(
} }
companion object { companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -85,14 +80,16 @@ class GetPlaylistsRequest private constructor(
@JvmStatic @JvmStatic
fun fetchRequestIfNeeded( fun fetchRequestIfNeeded(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
) { ) {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
synchronized(cache) { synchronized(cache) {
if (!cache.containsKey(playlistId)) { if (!cache.containsKey(playlistId)) {
cache[playlistId] = GetPlaylistsRequest( cache[playlistId] = GetPlaylistsRequest(
playlistId, playlistId,
playerHeaders requestHeader,
dataSyncId,
) )
} }
} }
@ -109,40 +106,28 @@ class GetPlaylistsRequest private constructor(
Logger.printInfo({ toastMessage }, ex) 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( private fun sendRequest(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
): JSONObject? { ): JSONObject? {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
// 'playlist/get_add_to_playlist' request does not require PoToken. // 'playlist/get_add_to_playlist' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching get playlists request, playlistId: $playlistId, using client: $clientTypeName" } Logger.printDebug { "Fetching get playlists request, playlistId: $playlistId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLISTS, GET_PLAYLISTS,
clientType clientType,
requestHeader,
dataSyncId
) )
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) { val requestBody = getPlaylistsRequestBody(playlistId)
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody = PlayerRoutes.getPlaylistsRequestBody(playlistId)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -223,9 +208,10 @@ class GetPlaylistsRequest private constructor(
private fun fetch( private fun fetch(
playlistId: String, playlistId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
): Array<Pair<String, String>>? { ): Array<Pair<String, String>>? {
val json = sendRequest(playlistId, playerHeaders) val json = sendRequest(playlistId, requestHeader, dataSyncId)
if (json != null) { if (json != null) {
return parseResponse(json) return parseResponse(json)
} }

View File

@ -1,12 +1,13 @@
package app.revanced.extension.youtube.patches.utils.requests package app.revanced.extension.youtube.patches.utils.requests
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.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.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
import app.revanced.extension.youtube.patches.utils.requests.SavePlaylistRequest.Companion.HTTP_TIMEOUT_MILLISECONDS
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
@ -21,13 +22,15 @@ import java.util.concurrent.TimeoutException
class SavePlaylistRequest private constructor( class SavePlaylistRequest private constructor(
private val playlistId: String, private val playlistId: String,
private val libraryId: String, private val libraryId: String,
private val playerHeaders: Map<String, String>, private val requestHeader: Map<String, String>,
private val dataSyncId: String,
) { ) {
private val future: Future<Boolean> = Utils.submitOnBackgroundThread { private val future: Future<Boolean> = Utils.submitOnBackgroundThread {
fetch( fetch(
playlistId, playlistId,
libraryId, libraryId,
playerHeaders, requestHeader,
dataSyncId,
) )
} }
@ -57,14 +60,6 @@ class SavePlaylistRequest private constructor(
} }
companion object { companion object {
/**
* TCP connection and HTTP read timeout.
*/
private const val HTTP_TIMEOUT_MILLISECONDS = 10 * 1000
/**
* Any arbitrarily large value, but must be at least twice [HTTP_TIMEOUT_MILLISECONDS]
*/
private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000 private const val MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000
@GuardedBy("itself") @GuardedBy("itself")
@ -88,14 +83,16 @@ class SavePlaylistRequest private constructor(
fun fetchRequestIfNeeded( fun fetchRequestIfNeeded(
playlistId: String, playlistId: String,
libraryId: String, libraryId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
) { ) {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
synchronized(cache) { synchronized(cache) {
cache[libraryId] = SavePlaylistRequest( cache[libraryId] = SavePlaylistRequest(
playlistId, playlistId,
libraryId, libraryId,
playerHeaders requestHeader,
dataSyncId,
) )
} }
} }
@ -111,43 +108,30 @@ class SavePlaylistRequest private constructor(
Logger.printInfo({ toastMessage }, ex) 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( private fun sendRequest(
playlistId: String, playlistId: String,
libraryId: String, libraryId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
): JSONObject? { ): JSONObject? {
Objects.requireNonNull(playlistId) Objects.requireNonNull(playlistId)
Objects.requireNonNull(libraryId) Objects.requireNonNull(libraryId)
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
// 'browse/edit_playlist' request does not require PoToken. // 'browse/edit_playlist' endpoint does not require PoToken.
val clientType = YouTubeAppClient.ClientType.ANDROID val clientType = YouTubeAppClient.ClientType.ANDROID
val clientTypeName = clientType.name val clientTypeName = clientType.name
Logger.printDebug { "Fetching edit playlist request, playlistId: $playlistId, libraryId: $libraryId, using client: $clientTypeName" } Logger.printDebug { "Fetching edit playlist request, playlistId: $playlistId, libraryId: $libraryId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.EDIT_PLAYLIST, EDIT_PLAYLIST,
clientType clientType,
requestHeader,
dataSyncId
) )
connection.connectTimeout = HTTP_TIMEOUT_MILLISECONDS
connection.readTimeout = HTTP_TIMEOUT_MILLISECONDS
for (key in REQUEST_HEADER_KEYS) { val requestBody = savePlaylistRequestBody(libraryId, playlistId)
var value = playerHeaders[key]
if (value != null) {
connection.setRequestProperty(key, value)
}
}
val requestBody =
PlayerRoutes.savePlaylistRequestBody(libraryId, playlistId)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)
@ -190,9 +174,15 @@ class SavePlaylistRequest private constructor(
private fun fetch( private fun fetch(
playlistId: String, playlistId: String,
libraryId: String, libraryId: String,
playerHeaders: Map<String, String> requestHeader: Map<String, String>,
dataSyncId: String,
): Boolean? { ): Boolean? {
val json = sendRequest(playlistId, libraryId,playerHeaders) val json = sendRequest(
playlistId,
libraryId,
requestHeader,
dataSyncId,
)
if (json != null) { if (json != null) {
return parseResponse(json) return parseResponse(json)
} }

View File

@ -1,17 +1,10 @@
package app.revanced.extension.youtube.patches.video; package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str;
import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class AV1CodecPatch { public class AV1CodecPatch {
private static final int LITERAL_VALUE_AV01 = 1635135811;
private static final int LITERAL_VALUE_DOLBY_VISION = 1685485123;
private static final String VP9_CODEC = "video/x-vnd.on2.vp9"; private static final String VP9_CODEC = "video/x-vnd.on2.vp9";
private static long lastTimeResponse = 0;
/** /**
* Replace the SW AV01 codec to VP9 codec. * Replace the SW AV01 codec to VP9 codec.
@ -22,32 +15,4 @@ public class AV1CodecPatch {
public static String replaceCodec(String original) { public static String replaceCodec(String original) {
return Settings.REPLACE_AV1_CODEC.get() ? VP9_CODEC : original; return Settings.REPLACE_AV1_CODEC.get() ? VP9_CODEC : original;
} }
/**
* Replace the SW AV01 codec request with a Dolby Vision codec request.
* This request is invalid, so it falls back to codecs other than AV01.
* <p>
* Limitation: Fallback process causes about 15-20 seconds of buffering.
*
* @param literalValue literal value of the codec
*/
public static int rejectResponse(int literalValue) {
if (!Settings.REJECT_AV1_CODEC.get())
return literalValue;
Logger.printDebug(() -> "Response: " + literalValue);
if (literalValue != LITERAL_VALUE_AV01)
return literalValue;
final long currentTime = System.currentTimeMillis();
// Ignore the invoke within 20 seconds.
if (currentTime - lastTimeResponse > 20000) {
lastTimeResponse = currentTime;
Utils.showToastShort(str("revanced_reject_av1_codec_toast"));
}
return LITERAL_VALUE_DOLBY_VISION;
}
} }

View File

@ -75,30 +75,30 @@ public class CustomPlaybackSpeedPatch {
return isCustomPlaybackSpeedEnabled() ? 0 : original; return isCustomPlaybackSpeedEnabled() ? 0 : original;
} }
public static String[] getListEntries() { public static String[] getEntries() {
return isCustomPlaybackSpeedEnabled() return isCustomPlaybackSpeedEnabled()
? customSpeedEntries ? customSpeedEntries
: defaultSpeedEntries; : defaultSpeedEntries;
} }
public static String[] getListEntryValues() { public static String[] getEntryValues() {
return isCustomPlaybackSpeedEnabled() return isCustomPlaybackSpeedEnabled()
? customSpeedEntryValues ? customSpeedEntryValues
: defaultSpeedEntryValues; : defaultSpeedEntryValues;
} }
public static String[] getTrimmedListEntries() { public static String[] getTrimmedEntries() {
if (playbackSpeedEntries == null) { if (playbackSpeedEntries == null) {
final String[] playbackSpeedWithAutoEntries = getListEntries(); final String[] playbackSpeedWithAutoEntries = getEntries();
playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length); playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length);
} }
return playbackSpeedEntries; return playbackSpeedEntries;
} }
public static String[] getTrimmedListEntryValues() { public static String[] getTrimmedEntryValues() {
if (playbackSpeedEntryValues == null) { if (playbackSpeedEntryValues == null) {
final String[] playbackSpeedWithAutoEntryValues = getListEntryValues(); final String[] playbackSpeedWithAutoEntryValues = getEntryValues();
playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length); playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length);
} }

View File

@ -1,12 +1,18 @@
package app.revanced.extension.youtube.patches.video; package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str; import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.BooleanUtils;
import 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.Logger;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.utils.PatchStatus; import app.revanced.extension.youtube.patches.utils.PatchStatus;
@ -17,25 +23,70 @@ import app.revanced.extension.youtube.whitelist.Whitelist;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class PlaybackSpeedPatch { public class PlaybackSpeedPatch {
private static final FloatSetting DEFAULT_PLAYBACK_SPEED =
Settings.DEFAULT_PLAYBACK_SPEED;
private static final FloatSetting DEFAULT_PLAYBACK_SPEED_SHORTS =
Settings.DEFAULT_PLAYBACK_SPEED_SHORTS;
private static final boolean DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = private static final boolean DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC =
Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get(); Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get();
private static final long TOAST_DELAY_MILLISECONDS = 750; private static final long TOAST_DELAY_MILLISECONDS = 750;
private static long lastTimeSpeedChanged; private static long lastTimeSpeedChanged;
private static 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. * 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, public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
isLiveStream = newlyLoadedLiveStreamValue; if (isShortsActive()) {
Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId); return;
}
if (videoId.equals(newlyLoadedVideoId)) {
return;
}
videoId = newlyLoadedVideoId;
final float defaultPlaybackSpeed = getDefaultPlaybackSpeed(newlyLoadedChannelId, newlyLoadedVideoId); boolean isMusic = isMusic(newlyLoadedVideoId);
Logger.printDebug(() -> "overridePlaybackSpeed: " + defaultPlaybackSpeed); 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. * Injection point.
* This method is called every second for regular videos and Shorts.
*/ */
public static float getPlaybackSpeedInShorts(final float playbackSpeed) { public static float getPlaybackSpeed(float playbackSpeed) {
if (VideoInformation.lastPlayerResponseIsShort() && boolean isShorts = isShortsActive();
Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get() float defaultPlaybackSpeed = isShorts ? DEFAULT_PLAYBACK_SPEED_SHORTS.get() : DEFAULT_PLAYBACK_SPEED.get();
) {
float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null);
Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed);
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 defaultPlaybackSpeed;
} }
return playbackSpeed;
} }
/** /**
@ -86,51 +151,78 @@ public class PlaybackSpeedPatch {
*/ */
public static void userSelectedPlaybackSpeed(float playbackSpeed) { public static void userSelectedPlaybackSpeed(float playbackSpeed) {
try { try {
if (PatchStatus.RememberPlaybackSpeed() && boolean isShorts = isShortsActive();
Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
// 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);
// Prevent toast spamming if using the 0.05x adjustments. // Saves the user-selected playback speed in the method.
// Show exactly one toast after the user stops interacting with the speed menu. if (isShorts) {
final long now = System.currentTimeMillis(); lastSelectedShortsPlaybackSpeed = playbackSpeed;
lastTimeSpeedChanged = now; } 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; if (PatchStatus.RememberPlaybackSpeed()) {
Utils.runOnMainThreadDelayed(() -> { BooleanSetting rememberPlaybackSpeedLastSelectedSetting = isShorts
if (lastTimeSpeedChanged != now) { ? Settings.REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED
// The user made additional speed adjustments and this call is outdated. : Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED;
return; 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) { if (rememberPlaybackSpeedLastSelectedSetting.get()) {
// User changed to a different speed and immediately changed back. // With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
// Or the user is going past 8.0x in the glitched out 0.05x menu. // then the menu will allow increasing without bounds but the max speed is
return; // still capped to under 8.0x.
} playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
Settings.DEFAULT_PLAYBACK_SPEED.save(finalPlaybackSpeed);
if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) { // Prevent toast spamming if using the 0.05x adjustments.
return; // Show exactly one toast after the user stops interacting with the speed menu.
} final long now = System.currentTimeMillis();
Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x"))); lastTimeSpeedChanged = now;
}, TOAST_DELAY_MILLISECONDS);
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) { } catch (Exception ex) {
Logger.printException(() -> "userSelectedPlaybackSpeed failure", ex); Logger.printException(() -> "userSelectedPlaybackSpeed failure", ex);
} }
} }
private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) { /**
return (isLiveStream || Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) || isMusic(videoId)) * Injection point.
? 1.0f */
: Settings.DEFAULT_PLAYBACK_SPEED.get(); public static void onDismiss() {
synchronized (ignoredPlaybackSpeedVideoIds) {
ignoredPlaybackSpeedVideoIds.remove(videoId);
videoId = "";
}
} }
private static boolean isMusic(@Nullable String videoId) { private static boolean isMusic(String videoId) {
if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && videoId != null) { if (DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC && !videoId.isEmpty()) {
try { try {
MusicRequest request = MusicRequest.getRequestForVideoId(videoId); MusicRequest request = MusicRequest.getRequestForVideoId(videoId);
final boolean isMusic = request != null && BooleanUtils.toBoolean(request.getStream()); final boolean isMusic = request != null && BooleanUtils.toBoolean(request.getStream());

View File

@ -1,6 +1,7 @@
package app.revanced.extension.youtube.patches.video; package app.revanced.extension.youtube.patches.video;
import static app.revanced.extension.shared.utils.StringRef.str; import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.youtube.shared.RootView.isShortsActive;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -14,8 +15,10 @@ import app.revanced.extension.youtube.shared.VideoInformation;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class VideoQualityPatch { public class VideoQualityPatch {
private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2; private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2;
private static final IntegerSetting mobileQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_MOBILE; private static final IntegerSetting shortsQualityMobile = Settings.DEFAULT_VIDEO_QUALITY_MOBILE_SHORTS;
private static final IntegerSetting wifiQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_WIFI; private static final IntegerSetting shortsQualityWifi = Settings.DEFAULT_VIDEO_QUALITY_WIFI_SHORTS;
private static final IntegerSetting videoQualityMobile = Settings.DEFAULT_VIDEO_QUALITY_MOBILE;
private static final IntegerSetting videoQualityWifi = Settings.DEFAULT_VIDEO_QUALITY_WIFI;
@NonNull @NonNull
public static String videoId = ""; public static String videoId = "";
@ -35,12 +38,11 @@ public class VideoQualityPatch {
public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName,
@NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle,
final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) {
if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL) if (PlayerType.getCurrent() != PlayerType.INLINE_MINIMAL &&
return; !videoId.equals(newlyLoadedVideoId)) {
if (videoId.equals(newlyLoadedVideoId)) videoId = newlyLoadedVideoId;
return; setVideoQuality(750);
videoId = newlyLoadedVideoId; }
setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 750);
} }
/** /**
@ -53,42 +55,62 @@ public class VideoQualityPatch {
); );
} }
private static void setVideoQuality(final long delayMillis) { private static void setVideoQuality(long delayMillis) {
final int defaultQuality = Utils.getNetworkType() == Utils.NetworkType.MOBILE boolean isShorts = isShortsActive();
? mobileQualitySetting.get() IntegerSetting defaultQualitySetting = Utils.getNetworkType() == Utils.NetworkType.MOBILE
: wifiQualitySetting.get(); ? isShorts ? shortsQualityMobile : videoQualityMobile
: isShorts ? shortsQualityWifi : videoQualityWifi;
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY) int defaultQuality = defaultQualitySetting.get();
return;
Utils.runOnMainThreadDelayed(() -> { if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality); Utils.runOnMainThreadDelayed(() -> {
Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal); final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality);
VideoInformation.overrideVideoQuality(qualityToUseFinal); Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal);
}, delayMillis VideoInformation.overrideVideoQuality(qualityToUseFinal);
); }, delayMillis
);
}
} }
private static void userSelectedVideoQuality(final int defaultQuality) { private static void userSelectedVideoQuality(final int defaultQuality) {
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) if (defaultQuality != DEFAULT_YOUTUBE_VIDEO_QUALITY) {
return; final Utils.NetworkType networkType = Utils.getNetworkType();
if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY) String networkTypeMessage = networkType == Utils.NetworkType.MOBILE
return; ? str("revanced_remember_video_quality_mobile")
: str("revanced_remember_video_quality_wifi");
final Utils.NetworkType networkType = Utils.getNetworkType(); if (isShortsActive()) {
if (Settings.REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED.get()) {
IntegerSetting defaultQualitySetting = networkType == Utils.NetworkType.MOBILE
? shortsQualityMobile
: shortsQualityWifi;
switch (networkType) { defaultQualitySetting.save(defaultQuality);
case NONE -> {
Utils.showToastShort(str("revanced_remember_video_quality_none")); if (Settings.REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED_TOAST.get()) {
return; Utils.showToastShort(str(
"revanced_remember_video_quality_toast_shorts",
networkTypeMessage, (defaultQuality + "p")
));
}
}
} else {
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) {
IntegerSetting defaultQualitySetting = networkType == Utils.NetworkType.MOBILE
? videoQualityMobile
: videoQualityWifi;
defaultQualitySetting.save(defaultQuality);
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) {
Utils.showToastShort(str(
"revanced_remember_video_quality_toast",
networkTypeMessage, (defaultQuality + "p")
));
}
}
} }
case MOBILE -> mobileQualitySetting.save(defaultQuality);
default -> wifiQualitySetting.save(defaultQuality);
} }
if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
return;
Utils.showToastShort(str("revanced_remember_video_quality_" + networkType.getName(), defaultQuality + "p"));
} }
} }

View File

@ -2,9 +2,13 @@ package app.revanced.extension.youtube.patches.video.requests
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.annotation.GuardedBy import androidx.annotation.GuardedBy
import app.revanced.extension.shared.patches.client.YouTubeAppClient import app.revanced.extension.shared.innertube.client.YouTubeAppClient
import app.revanced.extension.shared.patches.client.YouTubeWebClient import app.revanced.extension.shared.innertube.client.YouTubeWebClient
import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createApplicationRequestBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.createWebInnertubeBody
import app.revanced.extension.shared.innertube.requests.InnerTubeRequestBody.getInnerTubeResponseConnectionFromRoute
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_CATEGORY
import app.revanced.extension.shared.innertube.requests.InnerTubeRoutes.GET_PLAYLIST_PAGE
import app.revanced.extension.shared.requests.Requester import app.revanced.extension.shared.requests.Requester
import app.revanced.extension.shared.utils.Logger import app.revanced.extension.shared.utils.Logger
import app.revanced.extension.shared.utils.Utils import app.revanced.extension.shared.utils.Utils
@ -124,12 +128,12 @@ class MusicRequest private constructor(
Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching playlist request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_PLAYLIST_PAGE, GET_PLAYLIST_PAGE,
clientType clientType
) )
val requestBody = val requestBody =
PlayerRoutes.createApplicationRequestBody( createApplicationRequestBody(
clientType = clientType, clientType = clientType,
videoId = videoId, videoId = videoId,
playlistId = "RD$videoId" playlistId = "RD$videoId"
@ -168,12 +172,11 @@ class MusicRequest private constructor(
Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" } Logger.printDebug { "Fetching microformat request for: $videoId, using client: $clientTypeName" }
try { try {
val connection = PlayerRoutes.getPlayerResponseConnectionFromRoute( val connection = getInnerTubeResponseConnectionFromRoute(
PlayerRoutes.GET_CATEGORY, GET_CATEGORY,
clientType clientType
) )
val requestBody = val requestBody = createWebInnertubeBody(clientType, videoId)
PlayerRoutes.createWebInnertubeBody(clientType, videoId)
connection.setFixedLengthStreamingMode(requestBody.size) connection.setFixedLengthStreamingMode(requestBody.size)
connection.outputStream.write(requestBody) connection.outputStream.write(requestBody)

View File

@ -115,7 +115,7 @@ public class ReturnYouTubeDislike {
private static final Rect middleSeparatorBounds; private static final Rect middleSeparatorBounds;
/** /**
* Left separator horizontal padding for Rolling Number layout. * Horizontal padding between the left and middle separator.
*/ */
public static final int leftSeparatorShapePaddingPixels; public static final int leftSeparatorShapePaddingPixels;
private static final ShapeDrawable leftSeparatorShape; private static final ShapeDrawable leftSeparatorShape;
@ -131,7 +131,7 @@ public class ReturnYouTubeDislike {
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp); (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize); middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0f, dp); leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.4f, dp);
leftSeparatorShape = new ShapeDrawable(new RectShape()); leftSeparatorShape = new ShapeDrawable(new RectShape());
leftSeparatorShape.setBounds(leftSeparatorBounds); leftSeparatorShape.setBounds(leftSeparatorBounds);

View File

@ -42,6 +42,7 @@ import app.revanced.extension.youtube.patches.player.ExitFullscreenPatch.Fullscr
import app.revanced.extension.youtube.patches.player.MiniplayerPatch; import app.revanced.extension.youtube.patches.player.MiniplayerPatch;
import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType; import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType;
import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior; import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior;
import app.revanced.extension.youtube.patches.swipe.SwipeControlsPatch;
import app.revanced.extension.youtube.patches.utils.PatchStatus; import app.revanced.extension.youtube.patches.utils.PatchStatus;
import app.revanced.extension.youtube.shared.PlaylistIdPrefix; import app.revanced.extension.youtube.shared.PlaylistIdPrefix;
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings; import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
@ -150,7 +151,6 @@ public class Settings extends BaseSettings {
new ChangeStartPagePatch.ChangeStartPageTypeAvailability()); new ChangeStartPagePatch.ChangeStartPageTypeAvailability());
public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE); public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE);
public static final BooleanSetting DISABLE_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_splash_animation", PatchStatus.SplashAnimation(), true); public static final BooleanSetting DISABLE_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_splash_animation", PatchStatus.SplashAnimation(), true);
public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", TRUE, true);
public static final BooleanSetting ENABLE_GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_enable_gradient_loading_screen", FALSE, true); public static final BooleanSetting ENABLE_GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_enable_gradient_loading_screen", FALSE, true);
public static final BooleanSetting HIDE_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true); public static final BooleanSetting HIDE_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true);
public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE); public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE);
@ -159,6 +159,7 @@ public class Settings extends BaseSettings {
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message"); public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
public static final BooleanSetting CHANGE_LIVE_RING_CLICK_ACTION = new BooleanSetting("revanced_change_live_ring_click_action", FALSE, true); public static final BooleanSetting CHANGE_LIVE_RING_CLICK_ACTION = new BooleanSetting("revanced_change_live_ring_click_action", FALSE, true);
public static final BooleanSetting DISABLE_LAYOUT_UPDATES = new BooleanSetting("revanced_disable_layout_updates", false, true, "revanced_disable_layout_updates_user_dialog_message"); public static final BooleanSetting DISABLE_LAYOUT_UPDATES = new BooleanSetting("revanced_disable_layout_updates", false, true, "revanced_disable_layout_updates_user_dialog_message");
public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", FALSE, true, "revanced_disable_translucent_status_bar_user_dialog_message");
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", false, true, "revanced_spoof_app_version_user_dialog_message"); public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", false, true, "revanced_spoof_app_version_user_dialog_message");
public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION)); public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", PatchStatus.SpoofAppVersionDefaultString(), true, parent(SPOOF_APP_VERSION));
@ -497,12 +498,13 @@ public class Settings extends BaseSettings {
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL = new BooleanSetting("revanced_shorts_custom_actions_copy_video_url", FALSE, true); public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL = new BooleanSetting("revanced_shorts_custom_actions_copy_video_url", FALSE, true);
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_shorts_custom_actions_external_downloader", FALSE, true); public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_shorts_custom_actions_external_downloader", FALSE, true);
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO = new BooleanSetting("revanced_shorts_custom_actions_open_video", FALSE, true); public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO = new BooleanSetting("revanced_shorts_custom_actions_open_video", FALSE, true);
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG = new BooleanSetting("revanced_shorts_custom_actions_speed_dialog", FALSE, true);
public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_REPEAT_STATE = new BooleanSetting("revanced_shorts_custom_actions_repeat_state", FALSE, true); public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_REPEAT_STATE = new BooleanSetting("revanced_shorts_custom_actions_repeat_state", FALSE, true);
public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU = new BooleanSetting("revanced_enable_shorts_custom_actions_flyout_menu", FALSE, true, public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU = new BooleanSetting("revanced_enable_shorts_custom_actions_flyout_menu", FALSE, true,
parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE)); parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR = new BooleanSetting("revanced_enable_shorts_custom_actions_toolbar", FALSE, true, public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR = new BooleanSetting("revanced_enable_shorts_custom_actions_toolbar", FALSE, true,
parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE)); parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_SPEED_DIALOG, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE));
// Experimental Flags // Experimental Flags
public static final BooleanSetting ENABLE_TIME_STAMP = new BooleanSetting("revanced_enable_shorts_time_stamp", FALSE, true); public static final BooleanSetting ENABLE_TIME_STAMP = new BooleanSetting("revanced_enable_shorts_time_stamp", FALSE, true);
@ -522,9 +524,15 @@ public class Settings extends BaseSettings {
public static final BooleanSetting ENABLE_SWIPE_PRESS_TO_ENGAGE = new BooleanSetting("revanced_enable_swipe_press_to_engage", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); public static final BooleanSetting ENABLE_SWIPE_PRESS_TO_ENGAGE = new BooleanSetting("revanced_enable_swipe_press_to_engage", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting ENABLE_SWIPE_HAPTIC_FEEDBACK = new BooleanSetting("revanced_enable_swipe_haptic_feedback", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); public static final BooleanSetting ENABLE_SWIPE_HAPTIC_FEEDBACK = new BooleanSetting("revanced_enable_swipe_haptic_feedback", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting SWIPE_LOCK_MODE = new BooleanSetting("revanced_swipe_gestures_lock_mode", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); public static final BooleanSetting SWIPE_LOCK_MODE = new BooleanSetting("revanced_swipe_gestures_lock_mode", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting SWIPE_OVERLAY_ALTERNATIVE_UI = new BooleanSetting("revanced_swipe_overlay_alternative_ui", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final BooleanSetting SWIPE_SHOW_CIRCULAR_OVERLAY = new BooleanSetting("revanced_swipe_show_circular_overlay", FALSE, true,
new SwipeControlsPatch.SwipeOverlayModernUIAvailability());
public static final BooleanSetting SWIPE_OVERLAY_MINIMAL_STYLE = new BooleanSetting("revanced_swipe_overlay_minimal_style", FALSE, true,
new SwipeControlsPatch.SwipeOverlayModernUIAvailability());
public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_overlay_text_size", 20, true,
new SwipeControlsPatch.SwipeOverlayTextSizeAvailability());
public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_magnitude_threshold", 0, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_magnitude_threshold", 0, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_ALPHA = new IntegerSetting("revanced_swipe_overlay_background_alpha", 127, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_overlay_text_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_RECT_SIZE = new IntegerSetting("revanced_swipe_overlay_rect_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); public static final IntegerSetting SWIPE_OVERLAY_RECT_SIZE = new IntegerSetting("revanced_swipe_overlay_rect_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME));
@ -543,30 +551,38 @@ public class Settings extends BaseSettings {
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1.0f, false, false); public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1.0f, false, false);
// PreferenceScreen: Video // PreferenceScreen: Video - Codec
public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", -2.0f);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE, true); public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE, true);
public static final BooleanSetting DISABLE_VP9_CODEC = new BooleanSetting("revanced_disable_vp9_codec", FALSE, true);
public static final BooleanSetting REPLACE_AV1_CODEC = new BooleanSetting("revanced_replace_av1_codec", FALSE, true);
// PreferenceScreen: Video - Playback speed
public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", -2.0f);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED));
public static final FloatSetting DEFAULT_PLAYBACK_SPEED_SHORTS = new FloatSetting("revanced_default_playback_speed_shorts", -2.0f);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_shorts_last_selected", TRUE);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_shorts_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_SHORTS_LAST_SELECTED));
public static final BooleanSetting ENABLE_CUSTOM_PLAYBACK_SPEED = new BooleanSetting("revanced_enable_custom_playback_speed", FALSE, true); public static final BooleanSetting ENABLE_CUSTOM_PLAYBACK_SPEED = new BooleanSetting("revanced_enable_custom_playback_speed", FALSE, true);
public static final BooleanSetting CUSTOM_PLAYBACK_SPEED_MENU_TYPE = new BooleanSetting("revanced_custom_playback_speed_menu_type", FALSE, parent(ENABLE_CUSTOM_PLAYBACK_SPEED)); public static final BooleanSetting CUSTOM_PLAYBACK_SPEED_MENU_TYPE = new BooleanSetting("revanced_custom_playback_speed_menu_type", FALSE, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.25\n2.5", true, parent(ENABLE_CUSTOM_PLAYBACK_SPEED)); public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.25\n2.5", true, parent(ENABLE_CUSTOM_PLAYBACK_SPEED));
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE);
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED)); // PreferenceScreen: Video - Video quality
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE); public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_LAST_SELECTED)); public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_LAST_SELECTED));
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE_SHORTS = new IntegerSetting("revanced_default_video_quality_mobile_shorts", -2, true);
public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI_SHORTS = new IntegerSetting("revanced_default_video_quality_wifi_shorts", -2, true);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_shorts_last_selected", TRUE);
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_shorts_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_SHORTS_LAST_SELECTED));
public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE, true); public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE, true);
// Experimental Flags
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", FALSE, true);
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC_TYPE = new BooleanSetting("revanced_disable_default_playback_speed_music_type", FALSE, true, parent(DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC));
public static final BooleanSetting ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS = new BooleanSetting("revanced_enable_default_playback_speed_shorts", FALSE);
public static final BooleanSetting SKIP_PRELOADED_BUFFER = new BooleanSetting("revanced_skip_preloaded_buffer", FALSE, true, "revanced_skip_preloaded_buffer_user_dialog_message"); public static final BooleanSetting SKIP_PRELOADED_BUFFER = new BooleanSetting("revanced_skip_preloaded_buffer", FALSE, true, "revanced_skip_preloaded_buffer_user_dialog_message");
public static final BooleanSetting SKIP_PRELOADED_BUFFER_TOAST = new BooleanSetting("revanced_skip_preloaded_buffer_toast", TRUE); public static final BooleanSetting SKIP_PRELOADED_BUFFER_TOAST = new BooleanSetting("revanced_skip_preloaded_buffer_toast", TRUE);
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true); public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true);
public static final BooleanSetting DISABLE_VP9_CODEC = new BooleanSetting("revanced_disable_vp9_codec", FALSE, true);
public static final BooleanSetting REPLACE_AV1_CODEC = new BooleanSetting("revanced_replace_av1_codec", FALSE, true);
public static final BooleanSetting REJECT_AV1_CODEC = new BooleanSetting("revanced_reject_av1_codec", FALSE, true);
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", FALSE, true);
public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC_TYPE = new BooleanSetting("revanced_disable_default_playback_speed_music_type", FALSE, true, parent(DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC));
// PreferenceScreen: Miscellaneous // PreferenceScreen: Miscellaneous
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
@ -618,24 +634,34 @@ public class Settings extends BaseSettings {
public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY.reVancedKeyValue); public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400"); public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400");
public static final FloatSetting SB_CATEGORY_SPONSOR_OPACITY = new FloatSetting("sb_sponsor_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", SKIP_AUTOMATICALLY.reVancedKeyValue); public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00"); public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00");
public static final FloatSetting SB_CATEGORY_SELF_PROMO_OPACITY = new FloatSetting("sb_selfpromo_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF"); public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF");
public static final FloatSetting SB_CATEGORY_INTERACTION_OPACITY = new FloatSetting("sb_interaction_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue); public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684"); public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684");
public static final FloatSetting SB_CATEGORY_HIGHLIGHT_OPACITY = new FloatSetting("sb_highlight_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF"); public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF");
public static final FloatSetting SB_CATEGORY_INTRO_OPACITY = new FloatSetting("sb_intro_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED"); public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED");
public static final FloatSetting SB_CATEGORY_OUTRO_OPACITY = new FloatSetting("sb_outro_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6"); public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6");
public static final FloatSetting SB_CATEGORY_PREVIEW_OPACITY = new FloatSetting("sb_preview_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF"); public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF");
public static final FloatSetting SB_CATEGORY_FILLER_OPACITY = new FloatSetting("sb_filler_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue); public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900"); public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900");
public static final FloatSetting SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY = new FloatSetting("sb_music_offtopic_opacity", 0.8f);
public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue); public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue);
public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF"); public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF");
public static final FloatSetting SB_CATEGORY_UNSUBMITTED_OPACITY = new FloatSetting("sb_unsubmitted_opacity", 1.0f);
// SB Setting not exported // SB Setting not exported
public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false); public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false);

View File

@ -1,6 +1,7 @@
package app.revanced.extension.youtube.settings.preference; package app.revanced.extension.youtube.settings.preference;
import static com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity.setToolbarLayoutParams; import static com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity.setToolbarLayoutParams;
import static app.revanced.extension.shared.settings.BaseSettings.SPOOF_STREAMING_DATA_TYPE;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog; import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog;
import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary; import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary;
import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier; import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier;
@ -9,6 +10,7 @@ import static app.revanced.extension.shared.utils.Utils.getChildView;
import static app.revanced.extension.shared.utils.Utils.isSDKAbove; import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import static app.revanced.extension.shared.utils.Utils.showToastShort; import static app.revanced.extension.shared.utils.Utils.showToastShort;
import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED; import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED;
import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED_SHORTS;
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT; import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT;
import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT_TYPE; import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT_TYPE;
@ -56,12 +58,14 @@ import java.util.Set;
import java.util.SortedMap; import java.util.SortedMap;
import java.util.TreeMap; import java.util.TreeMap;
import app.revanced.extension.shared.patches.spoof.SpoofStreamingDataPatch;
import app.revanced.extension.shared.settings.BaseSettings; import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.BooleanSetting; import app.revanced.extension.shared.settings.BooleanSetting;
import app.revanced.extension.shared.settings.EnumSetting; import app.revanced.extension.shared.settings.EnumSetting;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.ResourceUtils; import app.revanced.extension.shared.utils.ResourceUtils;
import app.revanced.extension.shared.utils.StringRef;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch; import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@ -117,9 +121,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
} else { } else {
Setting.privateSetValueFromString(setting, listPreference.getValue()); Setting.privateSetValueFromString(setting, listPreference.getValue());
} }
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) { if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries()); listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues()); listPreference.setEntryValues(CustomPlaybackSpeedPatch.getEntryValues());
}
if (setting.equals(SPOOF_STREAMING_DATA_TYPE)) {
listPreference.setEntries(SpoofStreamingDataPatch.getEntries());
listPreference.setEntryValues(SpoofStreamingDataPatch.getEntryValues());
} }
if (!(mPreference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) { if (!(mPreference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
updateListPreferenceSummary(listPreference, setting); updateListPreferenceSummary(listPreference, setting);
@ -134,11 +142,8 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
if (!settingImportInProgress && !showingUserDialogMessage) { if (!settingImportInProgress && !showingUserDialogMessage) {
final Context context = getActivity(); final Context context = getActivity();
if (setting.userDialogMessage != null && if (setting.userDialogMessage != null && !prefIsSetToDefault(mPreference, setting)) {
mPreference instanceof SwitchPreference switchPreference && showSettingUserDialogConfirmation(context, mPreference, setting);
setting.defaultValue instanceof Boolean defaultValue &&
switchPreference.isChecked() != defaultValue) {
showSettingUserDialogConfirmation(context, switchPreference, (BooleanSetting) setting);
} else if (setting.rebootApp) { } else if (setting.rebootApp) {
showRestartDialog(context); showRestartDialog(context);
} }
@ -148,25 +153,56 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
} }
}; };
private void showSettingUserDialogConfirmation(Context context, SwitchPreference switchPreference, BooleanSetting setting) { /**
* @return If the preference is currently set to the default value of the Setting.
*/
private boolean prefIsSetToDefault(Preference pref, Setting<?> setting) {
Object defaultValue = setting.defaultValue;
if (pref instanceof SwitchPreference switchPref) {
return switchPref.isChecked() == (Boolean) defaultValue;
}
String defaultValueString = defaultValue.toString();
if (pref instanceof EditTextPreference editPreference) {
return editPreference.getText().equals(defaultValueString);
}
if (pref instanceof ListPreference listPref) {
return listPref.getValue().equals(defaultValueString);
}
throw new IllegalStateException("Must override method to handle "
+ "preference type: " + pref.getClass());
}
private void showSettingUserDialogConfirmation(Context context, Preference pref, Setting<?> setting) {
Utils.verifyOnMainThread(); Utils.verifyOnMainThread();
showingUserDialogMessage = true; final StringRef userDialogMessage = setting.userDialogMessage;
assert setting.userDialogMessage != null; if (context != null && userDialogMessage != null) {
new AlertDialog.Builder(context) showingUserDialogMessage = true;
.setTitle(str("revanced_extended_confirm_user_dialog_title"))
.setMessage(setting.userDialogMessage.toString()) new AlertDialog.Builder(context)
.setPositiveButton(android.R.string.ok, (dialog, id) -> { .setTitle(str("revanced_extended_confirm_user_dialog_title"))
if (setting.rebootApp) { .setMessage(userDialogMessage.toString())
showRestartDialog(context); .setPositiveButton(android.R.string.ok, (dialog, id) -> {
} if (setting.rebootApp) {
}) showRestartDialog(context);
.setNegativeButton(android.R.string.cancel, (dialog, id) -> { }
switchPreference.setChecked(setting.defaultValue); // Recursive call that resets the Setting value. })
}) .setNegativeButton(android.R.string.cancel, (dialog, id) -> {
.setOnDismissListener(dialog -> showingUserDialogMessage = false) // Restore whatever the setting was before the change.
.setCancelable(false) if (setting instanceof BooleanSetting booleanSetting &&
.show(); pref instanceof SwitchPreference switchPreference) {
switchPreference.setChecked(booleanSetting.defaultValue);
} else if (setting instanceof EnumSetting<?> enumSetting &&
pref instanceof ListPreference listPreference) {
listPreference.setValue(enumSetting.defaultValue.toString());
updateListPreferenceSummary(listPreference, setting);
}
})
.setOnDismissListener(dialog -> showingUserDialogMessage = false)
.setCancelable(false)
.show();
}
} }
static PreferenceManager mPreferenceManager; static PreferenceManager mPreferenceManager;
@ -304,9 +340,13 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
} else if (preference instanceof EditTextPreference editTextPreference) { } else if (preference instanceof EditTextPreference editTextPreference) {
editTextPreference.setText(setting.get().toString()); editTextPreference.setText(setting.get().toString());
} else if (preference instanceof ListPreference listPreference) { } else if (preference instanceof ListPreference listPreference) {
if (setting.equals(DEFAULT_PLAYBACK_SPEED)) { if (setting.equals(DEFAULT_PLAYBACK_SPEED) || setting.equals(DEFAULT_PLAYBACK_SPEED_SHORTS)) {
listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries()); listPreference.setEntries(CustomPlaybackSpeedPatch.getEntries());
listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues()); listPreference.setEntryValues(CustomPlaybackSpeedPatch.getEntryValues());
}
if (setting.equals(SPOOF_STREAMING_DATA_TYPE)) {
listPreference.setEntries(SpoofStreamingDataPatch.getEntries());
listPreference.setEntryValues(SpoofStreamingDataPatch.getEntryValues());
} }
if (!(preference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) { if (!(preference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) {
updateListPreferenceSummary(listPreference, setting); updateListPreferenceSummary(listPreference, setting);
@ -337,6 +377,7 @@ public class ReVancedPreferenceFragment extends PreferenceFragment {
@Override @Override
public void onDestroy() { public void onDestroy() {
mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener); mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener);
Utils.resetLocalizedContext();
super.onDestroy(); super.onDestroy();
} }

View File

@ -6,6 +6,8 @@ import static app.revanced.extension.shared.utils.Utils.isSDKAbove;
import android.preference.Preference; import android.preference.Preference;
import android.preference.SwitchPreference; import android.preference.SwitchPreference;
import java.util.Date;
import app.revanced.extension.shared.settings.Setting; import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch; import app.revanced.extension.youtube.patches.general.ChangeFormFactorPatch;
import app.revanced.extension.youtube.patches.utils.PatchStatus; import app.revanced.extension.youtube.patches.utils.PatchStatus;
@ -44,8 +46,10 @@ public class ReVancedSettingsPreference extends ReVancedPreferenceFragment {
AmbientModePreferenceLinks(); AmbientModePreferenceLinks();
FullScreenPanelPreferenceLinks(); FullScreenPanelPreferenceLinks();
NavigationPreferenceLinks(); NavigationPreferenceLinks();
PatchInformationPreferenceLinks();
RYDPreferenceLinks(); RYDPreferenceLinks();
SeekBarPreferenceLinks(); SeekBarPreferenceLinks();
ShortsPreferenceLinks();
SpeedOverlayPreferenceLinks(); SpeedOverlayPreferenceLinks();
QuickActionsPreferenceLinks(); QuickActionsPreferenceLinks();
TabletLayoutLinks(); TabletLayoutLinks();
@ -142,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 * Enable/Disable Preference related to RYD settings
*/ */
@ -186,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 * Enable/Disable Preference related to Speed overlay settings
*/ */

View File

@ -1,6 +1,7 @@
package app.revanced.extension.youtube.settings.preference; package app.revanced.extension.youtube.settings.preference;
import static app.revanced.extension.shared.utils.StringRef.str; import static app.revanced.extension.shared.utils.StringRef.str;
import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
@ -12,41 +13,51 @@ import android.text.InputType;
import android.text.TextWatcher; import android.text.TextWatcher;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout; import android.widget.GridLayout;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView; import android.widget.TextView;
import java.util.Locale;
import java.util.Objects; import java.util.Objects;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour; import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory; import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
@SuppressWarnings({"unused", "deprecation"}) @SuppressWarnings({"unused", "deprecation"})
public class SegmentCategoryListPreference extends ListPreference { public class SegmentCategoryListPreference extends ListPreference {
private SegmentCategory mCategory; private SegmentCategory category;
private EditText mEditText; private TextView colorDotView;
private int mClickedDialogEntryIndex; private EditText colorEditText;
private EditText opacityEditText;
/**
* #RRGGBB
*/
private int categoryColor;
/**
* [0, 1]
*/
private float categoryOpacity;
private int selectedDialogEntryIndex;
private void init() { private void init() {
final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey()); final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey());
final boolean isHighlightCategory = segmentCategory == SegmentCategory.HIGHLIGHT; category = Objects.requireNonNull(segmentCategory);
mCategory = Objects.requireNonNull(segmentCategory);
// Edit: Using preferences to sync together multiple pieces // Edit: Using preferences to sync together multiple pieces
// of code together is messy and should be rethought. // of code is messy and should be rethought.
setKey(segmentCategory.behaviorSetting.key); setKey(segmentCategory.behaviorSetting.key);
setDefaultValue(segmentCategory.behaviorSetting.defaultValue); setDefaultValue(segmentCategory.behaviorSetting.defaultValue);
final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT;
setEntries(isHighlightCategory setEntries(isHighlightCategory
? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce() ? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce()
: CategoryBehaviour.getBehaviorDescriptions()); : CategoryBehaviour.getBehaviorDescriptions());
setEntryValues(isHighlightCategory setEntryValues(isHighlightCategory
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce() ? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
: CategoryBehaviour.getBehaviorKeyValues()); : CategoryBehaviour.getBehaviorKeyValues());
updateTitle();
updateTitleFromCategory();
} }
public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
@ -73,28 +84,41 @@ public class SegmentCategoryListPreference extends ListPreference {
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
try { try {
Utils.setEditTextDialogTheme(builder); Utils.setEditTextDialogTheme(builder);
super.onPrepareDialogBuilder(builder);
categoryColor = category.getColorNoOpacity();
categoryOpacity = category.getOpacity();
Context context = builder.getContext(); Context context = builder.getContext();
TableLayout table = new TableLayout(context); GridLayout gridLayout = new GridLayout(context);
table.setOrientation(LinearLayout.HORIZONTAL); gridLayout.setPadding(70, 0, 150, 0); // Padding for the entire layout.
table.setPadding(70, 0, 150, 0); gridLayout.setColumnCount(3);
gridLayout.setRowCount(2);
TableRow row = new TableRow(context);
GridLayout.LayoutParams gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(0); // First row.
gridParams.columnSpec = GridLayout.spec(0); // First column.
TextView colorTextLabel = new TextView(context); TextView colorTextLabel = new TextView(context);
colorTextLabel.setText(str("revanced_sb_color_dot_label")); colorTextLabel.setText(str("revanced_sb_color_dot_label"));
row.addView(colorTextLabel); colorTextLabel.setLayoutParams(gridParams);
gridLayout.addView(colorTextLabel);
TextView colorDotView = new TextView(context); gridParams = new GridLayout.LayoutParams();
colorDotView.setText(mCategory.getCategoryColorDot()); gridParams.rowSpec = GridLayout.spec(0); // First row.
colorDotView.setPadding(30, 0, 30, 0); gridParams.columnSpec = GridLayout.spec(1); // Second column.
row.addView(colorDotView); gridParams.setMargins(0, 0, 10, 0);
colorDotView = new TextView(context);
colorDotView.setLayoutParams(gridParams);
gridLayout.addView(colorDotView);
updateCategoryColorDot();
mEditText = new EditText(context); gridParams = new GridLayout.LayoutParams();
mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS); gridParams.rowSpec = GridLayout.spec(0); // First row.
mEditText.setText(mCategory.colorString()); gridParams.columnSpec = GridLayout.spec(2); // Third column.
mEditText.addTextChangedListener(new TextWatcher() { colorEditText = new EditText(context);
colorEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
colorEditText.setTextLocale(Locale.US);
colorEditText.setText(category.getColorString());
colorEditText.addTextChangedListener(new TextWatcher() {
@Override @Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { public void beforeTextChanged(CharSequence s, int start, int count, int after) {
} }
@ -104,44 +128,111 @@ public class SegmentCategoryListPreference extends ListPreference {
} }
@Override @Override
public void afterTextChanged(Editable s) { public void afterTextChanged(Editable edit) {
try { try {
String colorString = s.toString(); String colorString = edit.toString();
final int colorStringLength = colorString.length();
if (!colorString.startsWith("#")) { if (!colorString.startsWith("#")) {
s.insert(0, "#"); // recursively calls back into this method edit.insert(0, "#"); // Recursively calls back into this method.
return; return;
} }
if (colorString.length() > 7) {
s.delete(7, colorString.length()); final int maxColorStringLength = 7; // #RRGGBB
if (colorStringLength > maxColorStringLength) {
edit.delete(maxColorStringLength, colorStringLength);
return; return;
} }
final int color = Color.parseColor(colorString);
colorDotView.setText(SegmentCategory.getCategoryColorDot(color)); categoryColor = Color.parseColor(colorString);
updateCategoryColorDot();
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
// ignore // Ignore.
} }
} }
}); });
mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f)); colorEditText.setLayoutParams(gridParams);
row.addView(mEditText); gridLayout.addView(colorEditText);
table.addView(row); gridParams = new GridLayout.LayoutParams();
builder.setView(table); gridParams.rowSpec = GridLayout.spec(1); // Second row.
builder.setTitle(mCategory.title.toString()); gridParams.columnSpec = GridLayout.spec(0, 1); // First and second column.
TextView opacityLabel = new TextView(context);
opacityLabel.setText(str("revanced_sb_color_opacity_label"));
opacityLabel.setLayoutParams(gridParams);
gridLayout.addView(opacityLabel);
gridParams = new GridLayout.LayoutParams();
gridParams.rowSpec = GridLayout.spec(1); // Second row.
gridParams.columnSpec = GridLayout.spec(2); // Third column.
opacityEditText = new EditText(context);
opacityEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
opacityEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable edit) {
try {
String editString = edit.toString();
final int opacityStringLength = editString.length();
final int maxOpacityStringLength = 4; // [0.00, 1.00]
if (opacityStringLength > maxOpacityStringLength) {
edit.delete(maxOpacityStringLength, opacityStringLength);
return;
}
final float opacity = opacityStringLength == 0
? 0
: Float.parseFloat(editString);
if (opacity < 0) {
categoryOpacity = 0;
edit.replace(0, opacityStringLength, "0");
return;
} else if (opacity > 1.0f) {
categoryOpacity = 1;
edit.replace(0, opacityStringLength, "1.0");
return;
} else if (!editString.endsWith(".")) {
// Ignore "0." and "1." until the user finishes entering a valid number.
categoryOpacity = opacity;
}
updateCategoryColorDot();
} catch (NumberFormatException ex) {
// Should never happen.
Logger.printException(() -> "Could not parse opacity string", ex);
}
}
});
opacityEditText.setLayoutParams(gridParams);
gridLayout.addView(opacityEditText);
updateOpacityText();
builder.setView(gridLayout);
builder.setTitle(category.title.toString());
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> onClick(dialog, DialogInterface.BUTTON_POSITIVE)); builder.setPositiveButton(android.R.string.ok, (dialog, which) -> onClick(dialog, DialogInterface.BUTTON_POSITIVE));
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> { builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
try { try {
mCategory.resetColor(); category.resetColorAndOpacity();
updateTitle(); updateTitleFromCategory();
Utils.showToastShort(str("revanced_sb_color_reset")); Utils.showToastShort(str("revanced_sb_color_reset"));
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "setNeutralButton failure", ex); Logger.printException(() -> "setNeutralButton failure", ex);
} }
}); });
builder.setNegativeButton(android.R.string.cancel, null); builder.setNegativeButton(android.R.string.cancel, null);
mClickedDialogEntryIndex = findIndexOfValue(getValue());
builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which); selectedDialogEntryIndex = findIndexOfValue(getValue());
builder.setSingleChoiceItems(getEntries(), selectedDialogEntryIndex,
(dialog, which) -> selectedDialogEntryIndex = which);
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "onPrepareDialogBuilder failure", ex); Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
} }
@ -150,31 +241,50 @@ public class SegmentCategoryListPreference extends ListPreference {
@Override @Override
protected void onDialogClosed(boolean positiveResult) { protected void onDialogClosed(boolean positiveResult) {
try { try {
if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) { if (positiveResult && selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
String value = getEntryValues()[mClickedDialogEntryIndex].toString(); String value = getEntryValues()[selectedDialogEntryIndex].toString();
if (callChangeListener(value)) { if (callChangeListener(value)) {
setValue(value); setValue(value);
mCategory.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value))); category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
SegmentCategory.updateEnabledCategories(); SegmentCategory.updateEnabledCategories();
} }
String colorString = mEditText.getText().toString();
try { try {
if (!colorString.equals(mCategory.colorString())) { String colorString = colorEditText.getText().toString();
mCategory.setColor(colorString); if (!colorString.equals(category.getColorString()) || categoryOpacity != category.getOpacity()) {
category.setColor(colorString);
category.setOpacity(categoryOpacity);
Utils.showToastShort(str("revanced_sb_color_changed")); Utils.showToastShort(str("revanced_sb_color_changed"));
} }
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
Utils.showToastShort(str("revanced_sb_color_invalid")); Utils.showToastShort(str("revanced_sb_color_invalid"));
} }
updateTitle();
updateTitleFromCategory();
} }
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "onDialogClosed failure", ex); Logger.printException(() -> "onDialogClosed failure", ex);
} }
} }
private void updateTitle() { private void applyOpacityToCategoryColor() {
setTitle(mCategory.getTitleWithColorDot()); categoryColor = applyOpacityToColor(categoryColor, categoryOpacity);
setEnabled(Settings.SB_ENABLED.get()); }
private void updateTitleFromCategory() {
categoryColor = category.getColorNoOpacity();
categoryOpacity = category.getOpacity();
applyOpacityToCategoryColor();
setTitle(category.getTitleWithColorDot(categoryColor));
}
private void updateCategoryColorDot() {
applyOpacityToCategoryColor();
colorDotView.setText(SegmentCategory.getCategoryColorDot(categoryColor));
}
private void updateOpacityText() {
opacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity));
} }
} }

View File

@ -233,6 +233,7 @@ public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
statsCategory = new PreferenceCategory(mActivity); statsCategory = new PreferenceCategory(mActivity);
statsCategory.setLayoutResource(preferencesCategoryLayout); statsCategory.setLayoutResource(preferencesCategoryLayout);
statsCategory.setTitle(str("revanced_sb_stats")); statsCategory.setTitle(str("revanced_sb_stats"));
statsCategory.setEnabled(Settings.SB_ENABLED.get());
mPreferenceScreen.addPreference(statsCategory); mPreferenceScreen.addPreference(statsCategory);
fetchAndDisplayStats(); fetchAndDisplayStats();
@ -261,7 +262,6 @@ public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment {
final String key = category.keyValue; final String key = category.keyValue;
if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) { if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) {
segmentCategoryListPreference.setTitle(category.getTitleWithColorDot()); segmentCategoryListPreference.setTitle(category.getTitleWithColorDot());
segmentCategoryListPreference.setEnabled(Settings.SB_ENABLED.get());
} }
} }
} catch (Exception ex) { } catch (Exception ex) {

View File

@ -58,6 +58,10 @@ public final class RootView {
return PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get(); return PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get();
} }
public static boolean isShortsActive() {
return ShortsPlayerState.getCurrent().isOpen();
}
/** /**
* Get current BrowseId. * Get current BrowseId.
* Rest of the implementation added by patch. * Rest of the implementation added by patch.

View File

@ -48,4 +48,12 @@ enum class ShortsPlayerState {
fun isClosed(): Boolean { fun isClosed(): Boolean {
return this == CLOSED return this == CLOSED
} }
/**
* Check if the shorts player is [OPEN].
* Useful for checking if a shorts player is open.
*/
fun isOpen(): Boolean {
return this == OPEN
}
} }

View File

@ -139,7 +139,7 @@ public class SponsorBlockSettings {
for (SegmentCategory category : categories) { for (SegmentCategory category : categories) {
JSONObject categoryObject = new JSONObject(); JSONObject categoryObject = new JSONObject();
String categoryKey = category.keyValue; String categoryKey = category.keyValue;
categoryObject.put("color", category.colorString()); categoryObject.put("color", category.getColorString());
barTypesObject.put(categoryKey, categoryObject); barTypesObject.put(categoryKey, categoryObject);
if (category.behaviour != CategoryBehaviour.IGNORE) { if (category.behaviour != CategoryBehaviour.IGNORE) {

View File

@ -6,7 +6,12 @@ import android.annotation.TargetApi;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.text.Html; import android.graphics.Color;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.text.style.StyleSpan;
import android.widget.EditText; import android.widget.EditText;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -32,11 +37,9 @@ import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController
/** /**
* Not thread safe. All fields/methods must be accessed from the main thread. * Not thread safe. All fields/methods must be accessed from the main thread.
*
* @noinspection deprecation
*/ */
public class SponsorBlockUtils { public class SponsorBlockUtils {
private static final String LOCKED_COLOR = "#FFC83D"; private static final int LOCKED_COLOR = Color.parseColor("#FFC83D");
private static final String MANUAL_EDIT_TIME_TEXT_HINT = "hh:mm:ss.sss"; private static final String MANUAL_EDIT_TIME_TEXT_HINT = "hh:mm:ss.sss";
private static final Pattern manualEditTimePattern private static final Pattern manualEditTimePattern
@ -162,28 +165,34 @@ public class SponsorBlockUtils {
SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT) SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT)
? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category ? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category
: SegmentVote.values(); : SegmentVote.values();
CharSequence[] items = new CharSequence[voteOptions.length]; final int voteOptionsLength = voteOptions.length;
final boolean userIsVip = Settings.SB_USER_IS_VIP.get();
CharSequence[] items = new CharSequence[voteOptionsLength];
for (int i = 0; i < voteOptions.length; i++) { for (int i = 0; i < voteOptionsLength; i++) {
SegmentVote voteOption = voteOptions[i]; SegmentVote voteOption = voteOptions[i];
String title = voteOption.title.toString(); CharSequence title = voteOption.title.toString();
if (Settings.SB_USER_IS_VIP.get() && segment.isLocked && voteOption.shouldHighlight) { if (userIsVip && segment.isLocked && voteOption.highlightIfVipAndVideoIsLocked) {
items[i] = Html.fromHtml(String.format("<font color=\"%s\">%s</font>", LOCKED_COLOR, title)); SpannableString coloredTitle = new SpannableString(title);
} else { coloredTitle.setSpan(new ForegroundColorSpan(LOCKED_COLOR),
items[i] = title; 0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
title = coloredTitle;
} }
items[i] = title;
} }
new AlertDialog.Builder(context) new AlertDialog.Builder(context).setItems(items, (dialog1, which1) -> {
.setItems(items, (dialog1, which1) -> { SegmentVote voteOption = voteOptions[which1];
SegmentVote voteOption = voteOptions[which1]; switch (voteOption) {
switch (voteOption) { case UPVOTE:
case UPVOTE, DOWNVOTE -> case DOWNVOTE:
SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption); SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption);
case CATEGORY_CHANGE -> onNewCategorySelect(segment, context); break;
} case CATEGORY_CHANGE:
}) onNewCategorySelect(segment, context);
.show(); break;
}
}).show();
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "segmentVoteClickListener failure", ex); Logger.printException(() -> "segmentVoteClickListener failure", ex);
} }
@ -287,22 +296,33 @@ public class SponsorBlockUtils {
if (segment.category == SegmentCategory.UNSUBMITTED) { if (segment.category == SegmentCategory.UNSUBMITTED) {
continue; continue;
} }
StringBuilder htmlBuilder = new StringBuilder();
htmlBuilder.append(String.format("<b><font color=\"#%06X\">⬤</font> %s<br>", SpannableStringBuilder spannableBuilder = new SpannableStringBuilder();
segment.category.color, segment.category.title));
htmlBuilder.append(formatSegmentTime(segment.start)); spannableBuilder.append(segment.category.getTitleWithColorDot());
if (segment.category != SegmentCategory.HIGHLIGHT) { spannableBuilder.append('\n');
htmlBuilder.append(" to ").append(formatSegmentTime(segment.end));
String startTime = formatSegmentTime(segment.start);
if (segment.category == SegmentCategory.HIGHLIGHT) {
spannableBuilder.append(startTime);
} else {
String toFromString = str("revanced_sb_vote_segment_time_to_from",
startTime, formatSegmentTime(segment.end));
spannableBuilder.append(toFromString);
} }
htmlBuilder.append("</b>");
if (i + 1 != numberOfSegments) // prevents trailing new line after last segment if (i + 1 != numberOfSegments) {
htmlBuilder.append("<br>"); // prevents trailing new line after last segment
titles[i] = Html.fromHtml(htmlBuilder.toString()); spannableBuilder.append('\n');
}
spannableBuilder.setSpan(new StyleSpan(android.graphics.Typeface.BOLD),
0, spannableBuilder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
titles[i] = spannableBuilder;
} }
new AlertDialog.Builder(context) new AlertDialog.Builder(context).setItems(titles, segmentVoteClickListener).show();
.setItems(titles, segmentVoteClickListener)
.show();
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "onVotingClicked failure", ex); Logger.printException(() -> "onVotingClicked failure", ex);
} }

View File

@ -3,30 +3,41 @@ package app.revanced.extension.youtube.sponsorblock.objects;
import static app.revanced.extension.shared.utils.StringRef.sf; import static app.revanced.extension.shared.utils.StringRef.sf;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_COLOR; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_COLOR; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_COLOR; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_COLOR; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_COLOR; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_COLOR; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_COLOR; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO_COLOR; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_COLOR; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_OPACITY;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_COLOR; import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_COLOR;
import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_OPACITY;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Paint; import android.graphics.Paint;
import android.text.Html; import android.text.Spannable;
import android.text.Spanned; import android.text.SpannableString;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -34,45 +45,46 @@ import androidx.annotation.Nullable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import app.revanced.extension.shared.settings.FloatSetting;
import app.revanced.extension.shared.settings.StringSetting; import app.revanced.extension.shared.settings.StringSetting;
import app.revanced.extension.shared.utils.Logger; import app.revanced.extension.shared.utils.Logger;
import app.revanced.extension.shared.utils.StringRef; import app.revanced.extension.shared.utils.StringRef;
import app.revanced.extension.shared.utils.Utils; import app.revanced.extension.shared.utils.Utils;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings({"deprecation", "StaticFieldLeak"}) @SuppressWarnings("StaticFieldLeak")
public enum SegmentCategory { public enum SegmentCategory {
SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"), SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"),
SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR), SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR, SB_CATEGORY_SPONSOR_OPACITY),
SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"), SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"),
SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR), SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR, SB_CATEGORY_SELF_PROMO_OPACITY),
INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"), INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"),
SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR), SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR, SB_CATEGORY_INTERACTION_OPACITY),
/** /**
* Unique category that is treated differently than the rest. * Unique category that is treated differently than the rest.
*/ */
HIGHLIGHT("poi_highlight", sf("revanced_sb_segments_highlight"), sf("revanced_sb_skip_button_highlight"), sf("revanced_sb_skipped_highlight"), HIGHLIGHT("poi_highlight", sf("revanced_sb_segments_highlight"), sf("revanced_sb_skip_button_highlight"), sf("revanced_sb_skipped_highlight"),
SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR), SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR, SB_CATEGORY_HIGHLIGHT_OPACITY),
INTRO("intro", sf("revanced_sb_segments_intro"), INTRO("intro", sf("revanced_sb_segments_intro"),
sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"), sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"),
sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"), sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"),
SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR), SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR, SB_CATEGORY_INTRO_OPACITY),
OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"), OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"),
SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR), SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR, SB_CATEGORY_OUTRO_OPACITY),
PREVIEW("preview", sf("revanced_sb_segments_preview"), PREVIEW("preview", sf("revanced_sb_segments_preview"),
sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"), sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"),
sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"), sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"),
SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR), SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR, SB_CATEGORY_PREVIEW_OPACITY),
FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"), FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"),
SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR), SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR, SB_CATEGORY_FILLER_OPACITY),
MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"), MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"),
SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR), SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR, SB_CATEGORY_MUSIC_OFFTOPIC_OPACITY),
UNSUBMITTED("unsubmitted", StringRef.empty, sf("revanced_sb_skip_button_unsubmitted"), sf("revanced_sb_skipped_unsubmitted"), UNSUBMITTED("unsubmitted", StringRef.empty, sf("revanced_sb_skip_button_unsubmitted"), sf("revanced_sb_skipped_unsubmitted"),
SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR), SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR, SB_CATEGORY_UNSUBMITTED_OPACITY);
;
private static final StringRef skipSponsorTextCompact = sf("revanced_sb_skip_button_compact"); private static final StringRef skipSponsorTextCompact = sf("revanced_sb_skip_button_compact");
private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight"); private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight");
@ -111,12 +123,10 @@ public enum SegmentCategory {
mValuesMap.put(value.keyValue, value); mValuesMap.put(value.keyValue, value);
} }
@NonNull
public static SegmentCategory[] categoriesWithoutUnsubmitted() { public static SegmentCategory[] categoriesWithoutUnsubmitted() {
return categoriesWithoutUnsubmitted; return categoriesWithoutUnsubmitted;
} }
@NonNull
public static SegmentCategory[] categoriesWithoutHighlights() { public static SegmentCategory[] categoriesWithoutHighlights() {
return categoriesWithoutHighlights; return categoriesWithoutHighlights;
} }
@ -127,7 +137,7 @@ public enum SegmentCategory {
} }
/** /**
* Must be called if behavior of any category is changed * Must be called if behavior of any category is changed.
*/ */
public static void updateEnabledCategories() { public static void updateEnabledCategories() {
Utils.verifyOnMainThread(); Utils.verifyOnMainThread();
@ -154,30 +164,32 @@ public enum SegmentCategory {
updateEnabledCategories(); updateEnabledCategories();
} }
@NonNull public static int applyOpacityToColor(int color, float opacity) {
public final String keyValue; if (opacity < 0 || opacity > 1.0f) {
@NonNull throw new IllegalArgumentException("Invalid opacity: " + opacity);
public final StringSetting behaviorSetting; }
@NonNull final int opacityInt = (int) (255 * opacity);
private final StringSetting colorSetting; return (color & 0x00FFFFFF) | (opacityInt << 24);
}
public final String keyValue;
public final StringSetting behaviorSetting; // TODO: Replace with EnumSetting.
private final StringSetting colorSetting;
private final FloatSetting opacitySetting;
@NonNull
public final StringRef title; public final StringRef title;
/** /**
* Skip button text, if the skip occurs in the first quarter of the video * Skip button text, if the skip occurs in the first quarter of the video
*/ */
@NonNull
public final StringRef skipButtonTextBeginning; public final StringRef skipButtonTextBeginning;
/** /**
* Skip button text, if the skip occurs in the middle half of the video * Skip button text, if the skip occurs in the middle half of the video
*/ */
@NonNull
public final StringRef skipButtonTextMiddle; public final StringRef skipButtonTextMiddle;
/** /**
* Skip button text, if the skip occurs in the last quarter of the video * Skip button text, if the skip occurs in the last quarter of the video
*/ */
@NonNull
public final StringRef skipButtonTextEnd; public final StringRef skipButtonTextEnd;
/** /**
* Skipped segment toast, if the skip occurred in the first quarter of the video * Skipped segment toast, if the skip occurred in the first quarter of the video
@ -198,10 +210,7 @@ public enum SegmentCategory {
@NonNull @NonNull
public final Paint paint; public final Paint paint;
/** private int color;
* Value must be changed using {@link #setColor(String)}.
*/
public int color;
/** /**
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}. * Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
@ -213,17 +222,20 @@ public enum SegmentCategory {
SegmentCategory(String keyValue, StringRef title, SegmentCategory(String keyValue, StringRef title,
StringRef skipButtonText, StringRef skipButtonText,
StringRef skippedToastText, StringRef skippedToastText,
StringSetting behavior, StringSetting color) { StringSetting behavior,
StringSetting color, FloatSetting opacity) {
this(keyValue, title, this(keyValue, title,
skipButtonText, skipButtonText, skipButtonText, skipButtonText, skipButtonText, skipButtonText,
skippedToastText, skippedToastText, skippedToastText, skippedToastText, skippedToastText, skippedToastText,
behavior, color); behavior,
color, opacity);
} }
SegmentCategory(String keyValue, StringRef title, SegmentCategory(String keyValue, StringRef title,
StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd, StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd,
StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd, StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd,
StringSetting behavior, StringSetting color) { StringSetting behavior,
StringSetting color, FloatSetting opacity) {
this.keyValue = Objects.requireNonNull(keyValue); this.keyValue = Objects.requireNonNull(keyValue);
this.title = Objects.requireNonNull(title); this.title = Objects.requireNonNull(title);
this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning); this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning);
@ -234,6 +246,7 @@ public enum SegmentCategory {
this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd); this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd);
this.behaviorSetting = Objects.requireNonNull(behavior); this.behaviorSetting = Objects.requireNonNull(behavior);
this.colorSetting = Objects.requireNonNull(color); this.colorSetting = Objects.requireNonNull(color);
this.opacitySetting = Objects.requireNonNull(opacity);
this.paint = new Paint(); this.paint = new Paint();
loadFromSettings(); loadFromSettings();
} }
@ -250,11 +263,14 @@ public enum SegmentCategory {
this.behaviour = savedBehavior; this.behaviour = savedBehavior;
String colorString = colorSetting.get(); String colorString = colorSetting.get();
final float opacity = opacitySetting.get();
try { try {
setColor(colorString); setColor(colorString);
setOpacity(opacity);
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "Invalid color: " + colorString, ex); Logger.printException(() -> "Invalid color: " + colorString + " opacity: " + opacity, ex);
colorSetting.resetToDefault(); colorSetting.resetToDefault();
opacitySetting.resetToDefault();
loadFromSettings(); loadFromSettings();
} }
} }
@ -264,45 +280,77 @@ public enum SegmentCategory {
this.behaviorSetting.save(behaviour.reVancedKeyValue); this.behaviorSetting.save(behaviour.reVancedKeyValue);
} }
/** private void updateColor() {
* @return HTML color format string color = applyOpacityToColor(color, opacitySetting.get());
*/
@NonNull
public String colorString() {
return String.format("#%06X", color);
}
public void setColor(@NonNull String colorString) throws IllegalArgumentException {
final int color = Color.parseColor(colorString) & 0xFFFFFF;
this.color = color;
paint.setColor(color); paint.setColor(color);
paint.setAlpha(255);
colorSetting.save(colorString); // Save after parsing.
} }
public void resetColor() { /**
* @param opacity Segment color opacity between [0, 1].
*/
public void setOpacity(float opacity) throws IllegalArgumentException {
if (opacity < 0 || opacity > 1) {
throw new IllegalArgumentException("Invalid opacity: " + opacity);
}
opacitySetting.save(opacity);
updateColor();
}
public float getOpacity() {
return opacitySetting.get();
}
public void resetColorAndOpacity() {
setColor(colorSetting.defaultValue); setColor(colorSetting.defaultValue);
setOpacity(opacitySetting.defaultValue);
} }
@NonNull /**
private static String getCategoryColorDotHTML(int color) { * @param colorString Segment color with #RRGGBB format.
color &= 0xFFFFFF; */
return String.format("<font color=\"#%06X\">⬤</font>", color); public void setColor(String colorString) throws IllegalArgumentException {
color = Color.parseColor(colorString);
colorSetting.save(colorString);
updateColor();
} }
@NonNull /**
public static Spanned getCategoryColorDot(int color) { * @return Integer color of #RRGGBB format.
return Html.fromHtml(getCategoryColorDotHTML(color)); */
public int getColorNoOpacity() {
return color & 0x00FFFFFF;
} }
@NonNull /**
public Spanned getCategoryColorDot() { * @return Hex color string of #RRGGBB format with no opacity level.
*/
public String getColorString() {
return String.format(Locale.US, "#%06X", getColorNoOpacity());
}
private static SpannableString getCategoryColorDotSpan(String text, int color) {
SpannableString dotSpan = new SpannableString('⬤' + text);
dotSpan.setSpan(new ForegroundColorSpan(color), 0, 1,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return dotSpan;
}
public static SpannableString getCategoryColorDot(int color) {
return getCategoryColorDotSpan("", color);
}
public SpannableString getCategoryColorDot() {
return getCategoryColorDot(color); return getCategoryColorDot(color);
} }
@NonNull public SpannableString getTitleWithColorDot(int categoryColor) {
public Spanned getTitleWithColorDot() { return getCategoryColorDotSpan(" " + title, categoryColor);
return Html.fromHtml(getCategoryColorDotHTML(color) + " " + title); }
public SpannableString getTitleWithColorDot() {
return getTitleWithColorDot(color);
} }
/** /**
@ -310,7 +358,6 @@ public enum SegmentCategory {
* @param videoLength length of the video * @param videoLength length of the video
* @return the skip button text * @return the skip button text
*/ */
@NonNull
StringRef getSkipButtonText(long segmentStartTime, long videoLength) { StringRef getSkipButtonText(long segmentStartTime, long videoLength) {
if (Settings.SB_COMPACT_SKIP_BUTTON.get()) { if (Settings.SB_COMPACT_SKIP_BUTTON.get()) {
return (this == SegmentCategory.HIGHLIGHT) return (this == SegmentCategory.HIGHLIGHT)
@ -319,7 +366,7 @@ public enum SegmentCategory {
} }
if (videoLength == 0) { if (videoLength == 0) {
return skipButtonTextBeginning; // video is still loading. Assume it's the beginning return skipButtonTextBeginning; // Video is still loading. Assume it's the beginning.
} }
final float position = segmentStartTime / (float) videoLength; final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) { if (position < 0.25f) {
@ -335,10 +382,9 @@ public enum SegmentCategory {
* @param videoLength length of the video * @param videoLength length of the video
* @return 'skipped segment' toast message * @return 'skipped segment' toast message
*/ */
@NonNull
StringRef getSkippedToastText(long segmentStartTime, long videoLength) { StringRef getSkippedToastText(long segmentStartTime, long videoLength) {
if (videoLength == 0) { if (videoLength == 0) {
return skippedToastBeginning; // video is still loading. Assume it's the beginning return skippedToastBeginning; // Video is still loading. Assume it's the beginning.
} }
final float position = segmentStartTime / (float) videoLength; final float position = segmentStartTime / (float) videoLength;
if (position < 0.25f) { if (position < 0.25f) {

View File

@ -24,12 +24,15 @@ public class SponsorSegment implements Comparable<SponsorSegment> {
@NonNull @NonNull
public final StringRef title; public final StringRef title;
public final int apiVoteType; public final int apiVoteType;
public final boolean shouldHighlight; /**
* If the option should be highlighted for VIP users.
*/
public final boolean highlightIfVipAndVideoIsLocked;
SegmentVote(@NonNull StringRef title, int apiVoteType, boolean shouldHighlight) { SegmentVote(@NonNull StringRef title, int apiVoteType, boolean highlightIfVipAndVideoIsLocked) {
this.title = title; this.title = title;
this.apiVoteType = apiVoteType; this.apiVoteType = apiVoteType;
this.shouldHighlight = shouldHighlight; this.highlightIfVipAndVideoIsLocked = highlightIfVipAndVideoIsLocked;
} }
} }

View File

@ -125,7 +125,7 @@ class SwipeControlsConfigurationProvider(
* get the background color for text on the overlay, as a color int * get the background color for text on the overlay, as a color int
*/ */
val overlayTextBackgroundColor: Int val overlayTextBackgroundColor: Int
get() = Color.argb(Settings.SWIPE_OVERLAY_BACKGROUND_ALPHA.get(), 0, 0, 0) get() = overlayBackgroundOpacity
/** /**
* get the foreground color for text on the overlay, as a color int * get the foreground color for text on the overlay, as a color int
@ -133,6 +133,59 @@ class SwipeControlsConfigurationProvider(
val overlayForegroundColor: Int val overlayForegroundColor: Int
get() = Color.WHITE get() = Color.WHITE
/**
* Gets the opacity value (0-100%) is converted to an alpha value (0-255) for transparency.
* If the opacity value is out of range, it resets to the default and displays a warning message.
*/
val overlayBackgroundOpacity: Int
get() {
var opacity = validateValue(
Settings.SWIPE_OVERLAY_BACKGROUND_OPACITY,
0,
100,
"revanced_swipe_overlay_background_opacity_invalid_toast"
)
opacity = opacity * 255 / 100
return Color.argb(opacity, 0, 0, 0)
}
/**
* The color of the progress overlay.
*/
val overlayProgressColor: Int
get() = 0xBFFFFFFF.toInt()
/**
* The color used for the background of the progress overlay fill.
*/
val overlayFillBackgroundPaint: Int
get() = 0x80D3D3D3.toInt()
/**
* The color used for the text and icons in the overlay.
*/
val overlayTextColor: Int
get() = Color.WHITE
/**
* A flag that determines whether to use the alternate UI.
*/
val isAlternativeUI: Boolean
get() = Settings.SWIPE_OVERLAY_ALTERNATIVE_UI.get()
/**
* A flag that determines if the overlay should only show the icon.
*/
val overlayShowOverlayMinimalStyle: Boolean
get() = isAlternativeUI && Settings.SWIPE_OVERLAY_MINIMAL_STYLE.get()
/**
* A flag that determines if the progress bar should be circular.
*/
val isCircularProgressBar: Boolean
get() = isAlternativeUI && Settings.SWIPE_SHOW_CIRCULAR_OVERLAY.get()
// endregion // endregion
// region behaviour // region behaviour

View File

@ -24,7 +24,7 @@ import java.lang.ref.WeakReference
* The main controller for volume and brightness swipe controls. * The main controller for volume and brightness swipe controls.
* note that the superclass is overwritten to the superclass of the MainActivity at patch time * note that the superclass is overwritten to the superclass of the MainActivity at patch time
* *
* @smali Lapp/revanced/integrations/youtube/swipecontrols/SwipeControlsHostActivity; * @smali Lapp/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity;
*/ */
class SwipeControlsHostActivity : Activity() { class SwipeControlsHostActivity : Activity() {
/** /**

View File

@ -1,14 +1,18 @@
package app.revanced.extension.youtube.swipecontrols.views package app.revanced.extension.youtube.swipecontrols.views
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.AttributeSet
import android.util.TypedValue import android.util.TypedValue
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import app.revanced.extension.shared.utils.ResourceUtils.ResourceType import app.revanced.extension.shared.utils.ResourceUtils.ResourceType
@ -17,6 +21,7 @@ import app.revanced.extension.shared.utils.StringRef.str
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider
import app.revanced.extension.youtube.swipecontrols.misc.SwipeControlsOverlay import app.revanced.extension.youtube.swipecontrols.misc.SwipeControlsOverlay
import app.revanced.extension.youtube.swipecontrols.misc.applyDimension import app.revanced.extension.youtube.swipecontrols.misc.applyDimension
import kotlin.math.min
import kotlin.math.round import kotlin.math.round
/** /**
@ -33,36 +38,82 @@ class SwipeControlsOverlayLayout(
*/ */
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context)) constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context))
private val feedbackTextView: TextView
private val autoBrightnessIcon: Drawable private val autoBrightnessIcon: Drawable
private val lowBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_low")
private val mediumBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_medium")
private val highBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_high")
private val fullBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_full")
private val manualBrightnessIcon: Drawable private val manualBrightnessIcon: Drawable
private val mutedVolumeIcon: Drawable private val mutedVolumeIcon: Drawable
private val lowVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_low")
private val normalVolumeIcon: Drawable private val normalVolumeIcon: Drawable
private val feedbackTextView: TextView
private val fullVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_high")
private fun getDrawable(name: String, width: Int, height: Int): Drawable { private val circularProgressView: CircularProgressView = CircularProgressView(
return resources.getDrawable( context,
config.overlayBackgroundOpacity,
config.overlayShowOverlayMinimalStyle,
config.overlayProgressColor,
config.overlayFillBackgroundPaint,
config.overlayTextColor
).apply {
layoutParams = LayoutParams(300, 300).apply {
addRule(CENTER_IN_PARENT, TRUE)
}
visibility = GONE // Initially hidden
}
private val horizontalProgressView: HorizontalProgressView
private val isAlternativeUI: Boolean = config.isAlternativeUI
private fun getDrawable(name: String, width: Int? = null, height: Int? = null): Drawable {
val drawable = resources.getDrawable(
getIdentifier(name, ResourceType.DRAWABLE, context), getIdentifier(name, ResourceType.DRAWABLE, context),
context.theme context.theme,
).apply { )
setTint(config.overlayForegroundColor)
setBounds( if (width != null && height != null) {
drawable.setTint(config.overlayForegroundColor)
drawable.setBounds(
0, 0,
0, 0,
width, width,
height, height,
) )
} else {
drawable.setTint(config.overlayTextColor)
} }
return drawable
} }
init { init {
// Initialize horizontal progress bar
val screenWidth = resources.displayMetrics.widthPixels
val layoutWidth = (screenWidth * 2 / 3).toInt() // 2/3 of screen width
horizontalProgressView = HorizontalProgressView(
context,
config.overlayBackgroundOpacity,
config.overlayShowOverlayMinimalStyle,
config.overlayProgressColor,
config.overlayFillBackgroundPaint,
config.overlayTextColor
).apply {
layoutParams = LayoutParams(layoutWidth, 100).apply {
addRule(CENTER_HORIZONTAL)
topMargin = 40 // Top margin
}
visibility = GONE // Initially hidden
}
// init views // init views
val feedbackYTextViewPadding = 5.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP) val feedbackYTextViewPadding = 5.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
val feedbackXTextViewPadding = 12.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP) val feedbackXTextViewPadding = 12.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP) val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP)
feedbackTextView = TextView(context).apply { feedbackTextView = TextView(context).apply {
layoutParams = LayoutParams( layoutParams = LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT,
).apply { ).apply {
addRule(CENTER_IN_PARENT, TRUE) addRule(CENTER_IN_PARENT, TRUE)
setPadding( setPadding(
@ -81,19 +132,36 @@ class SwipeControlsOverlayLayout(
compoundDrawablePadding = compoundIconPadding compoundDrawablePadding = compoundIconPadding
visibility = GONE visibility = GONE
} }
addView(feedbackTextView)
// get icons scaled, assuming square icons if (isAlternativeUI) {
val iconHeight = round(feedbackTextView.lineHeight * .8).toInt() addView(circularProgressView)
autoBrightnessIcon = getDrawable("ic_sc_brightness_auto", iconHeight, iconHeight) addView(horizontalProgressView)
manualBrightnessIcon = getDrawable("ic_sc_brightness_manual", iconHeight, iconHeight)
mutedVolumeIcon = getDrawable("ic_sc_volume_mute", iconHeight, iconHeight) autoBrightnessIcon = getDrawable("revanced_ic_sc_brightness_auto")
normalVolumeIcon = getDrawable("ic_sc_volume_normal", iconHeight, iconHeight) manualBrightnessIcon = getDrawable("revanced_ic_sc_brightness_manual")
mutedVolumeIcon = getDrawable("revanced_ic_sc_volume_mute")
normalVolumeIcon = getDrawable("revanced_ic_sc_volume_normal")
} else {
addView(feedbackTextView)
// get icons scaled, assuming square icons
val iconHeight = round(feedbackTextView.lineHeight * .8).toInt()
autoBrightnessIcon =
getDrawable("revanced_ic_sc_brightness_auto", iconHeight, iconHeight)
manualBrightnessIcon =
getDrawable("revanced_ic_sc_brightness_manual", iconHeight, iconHeight)
mutedVolumeIcon = getDrawable("revanced_ic_sc_volume_mute", iconHeight, iconHeight)
normalVolumeIcon = getDrawable("revanced_ic_sc_volume_normal", iconHeight, iconHeight)
}
} }
private val feedbackHideHandler = Handler(Looper.getMainLooper()) private val feedbackHideHandler = Handler(Looper.getMainLooper())
private val feedbackHideCallback = Runnable { private val feedbackHideCallback = Runnable {
feedbackTextView.visibility = View.GONE if (isAlternativeUI) {
circularProgressView.visibility = GONE
horizontalProgressView.visibility = GONE
} else {
feedbackTextView.visibility = GONE
}
} }
/** /**
@ -117,21 +185,81 @@ class SwipeControlsOverlayLayout(
} }
} }
/**
* Displays the progress bar with the appropriate value, icon, and type (brightness or volume).
*/
private fun showFeedbackView(
value: String,
progress: Int,
max: Int,
icon: Drawable,
isBrightness: Boolean
) {
feedbackHideHandler.removeCallbacks(feedbackHideCallback)
feedbackHideHandler.postDelayed(feedbackHideCallback, config.overlayShowTimeoutMillis)
val viewToShow =
if (config.isCircularProgressBar) circularProgressView else horizontalProgressView
viewToShow.apply {
setProgress(progress, max, value, isBrightness)
this.icon = icon
visibility = VISIBLE
}
}
override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) { override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
showFeedbackView( if (isAlternativeUI) {
"$newVolume", val volumePercentage = (newVolume.toFloat() / maximumVolume) * 100
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon, val icon = when {
) newVolume == 0 -> mutedVolumeIcon
volumePercentage < 33 -> lowVolumeIcon
volumePercentage < 66 -> normalVolumeIcon
else -> fullVolumeIcon
}
showFeedbackView("$newVolume", newVolume, maximumVolume, icon, isBrightness = false)
} else {
showFeedbackView(
"$newVolume",
if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon,
)
}
} }
override fun onBrightnessChanged(brightness: Double) { override fun onBrightnessChanged(brightness: Double) {
if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) { if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) {
showFeedbackView( if (isAlternativeUI) {
str("revanced_swipe_lowest_value_auto_brightness_overlay_text"), showFeedbackView(
autoBrightnessIcon, str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
) 0,
100,
autoBrightnessIcon,
isBrightness = true,
)
} else {
showFeedbackView(
str("revanced_swipe_lowest_value_auto_brightness_overlay_text"),
autoBrightnessIcon,
)
}
} else if (brightness >= 0) { } else if (brightness >= 0) {
showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon) if (isAlternativeUI) {
val brightnessValue = round(brightness).toInt()
val icon = when {
brightnessValue < 25 -> lowBrightnessIcon
brightnessValue < 50 -> mediumBrightnessIcon
brightnessValue < 75 -> highBrightnessIcon
else -> fullBrightnessIcon
}
showFeedbackView(
"$brightnessValue%",
brightnessValue,
100,
icon,
isBrightness = true
)
} else {
showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon)
}
} }
} }
@ -145,3 +273,255 @@ class SwipeControlsOverlayLayout(
} }
} }
} }
/**
* Abstract base class for progress views.
*/
abstract class AbstractProgressView(
context: Context,
overlayBackgroundOpacity: Int,
protected val overlayShowOverlayMinimalStyle: Boolean,
overlayProgressColor: Int,
overlayFillBackgroundPaint: Int,
protected val overlayTextColor: Int,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// Combined paint creation function for both fill and stroke styles
private fun createPaint(
color: Int,
style: Paint.Style = Paint.Style.FILL,
strokeCap: Paint.Cap = Paint.Cap.BUTT,
strokeWidth: Float = 0f
) = Paint(Paint.ANTI_ALIAS_FLAG).apply {
this.style = style
this.color = color
this.strokeCap = strokeCap
this.strokeWidth = strokeWidth
}
// Initialize paints
val backgroundPaint = createPaint(overlayBackgroundOpacity, style = Paint.Style.FILL)
val progressPaint = createPaint(
overlayProgressColor,
style = Paint.Style.STROKE,
strokeCap = Paint.Cap.ROUND,
strokeWidth = 20f
)
val fillBackgroundPaint = createPaint(overlayFillBackgroundPaint, style = Paint.Style.FILL)
val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = overlayTextColor
textAlign = Paint.Align.CENTER
textSize = 40f // Can adjust based on need
}
protected var progress = 0
protected var maxProgress = 100
protected var displayText: String = "0"
protected var isBrightness = true
var icon: Drawable? = null
fun setProgress(value: Int, max: Int, text: String, isBrightnessMode: Boolean) {
progress = value
maxProgress = max
displayText = text
isBrightness = isBrightnessMode
invalidate()
}
override fun onDraw(canvas: Canvas) {
// Base class implementation can be empty
}
}
/**
* Custom view for rendering a circular progress indicator with icons and text.
*/
@SuppressLint("ViewConstructor")
class CircularProgressView(
context: Context,
overlayBackgroundOpacity: Int,
overlayShowOverlayMinimalStyle: Boolean,
overlayProgressColor: Int,
overlayFillBackgroundPaint: Int,
overlayTextColor: Int,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractProgressView(
context,
overlayBackgroundOpacity,
overlayShowOverlayMinimalStyle,
overlayProgressColor,
overlayFillBackgroundPaint,
overlayTextColor,
attrs,
defStyleAttr
) {
private val rectF = RectF()
init {
textPaint.textSize = 40f // Override default text size for circular view
progressPaint.strokeWidth = 20f
fillBackgroundPaint.strokeWidth = 20f
progressPaint.strokeCap = Paint.Cap.ROUND
fillBackgroundPaint.strokeCap = Paint.Cap.BUTT
progressPaint.style = Paint.Style.STROKE
fillBackgroundPaint.style = Paint.Style.STROKE
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val size = min(width, height).toFloat()
rectF.set(20f, 20f, size - 20f, size - 20f)
canvas.drawOval(rectF, fillBackgroundPaint) // Draw the outer ring.
canvas.drawCircle(
width / 2f,
height / 2f,
size / 3,
backgroundPaint
) // Draw the inner circle.
// Select the paint for drawing based on whether it's brightness or volume.
val sweepAngle = (progress.toFloat() / maxProgress) * 360
canvas.drawArc(rectF, -90f, sweepAngle, false, progressPaint) // Draw the progress arc.
// Draw the icon in the center.
icon?.let {
val iconSize = if (overlayShowOverlayMinimalStyle) 100 else 80
val iconX = (width - iconSize) / 2
val iconY = (height / 2) - if (overlayShowOverlayMinimalStyle) 50 else 80
it.setBounds(iconX, iconY, iconX + iconSize, iconY + iconSize)
it.draw(canvas)
}
// If not a minimal style mode, draw the text inside the ring.
if (!overlayShowOverlayMinimalStyle) {
canvas.drawText(displayText, width / 2f, height / 2f + 60f, textPaint)
}
}
}
/**
* Custom view for rendering a rectangular progress bar with icons and text.
*/
@SuppressLint("ViewConstructor")
class HorizontalProgressView(
context: Context,
overlayBackgroundOpacity: Int,
overlayShowOverlayMinimalStyle: Boolean,
overlayProgressColor: Int,
overlayFillBackgroundPaint: Int,
overlayTextColor: Int,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : AbstractProgressView(
context,
overlayBackgroundOpacity,
overlayShowOverlayMinimalStyle,
overlayProgressColor,
overlayFillBackgroundPaint,
overlayTextColor,
attrs,
defStyleAttr
) {
private val iconSize = 60f
private val padding = 40f
init {
textPaint.textSize = 36f // Override default text size for horizontal view
progressPaint.strokeWidth = 0f
progressPaint.strokeCap = Paint.Cap.BUTT
progressPaint.style = Paint.Style.FILL
fillBackgroundPaint.style = Paint.Style.FILL
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val width = width.toFloat()
val height = height.toFloat()
// Radius for rounded corners
val cornerRadius = min(width, height) / 2
// Calculate the total width for the elements
val minimalElementWidth = 5 * padding + iconSize
// Calculate the starting point (X) to center the elements
val minimalStartX = (width - minimalElementWidth) / 2
// Draw the background
if (!overlayShowOverlayMinimalStyle) {
canvas.drawRoundRect(0f, 0f, width, height, cornerRadius, cornerRadius, backgroundPaint)
} else {
canvas.drawRoundRect(
minimalStartX,
0f,
minimalStartX + minimalElementWidth,
height,
cornerRadius,
cornerRadius,
backgroundPaint
)
}
if (!overlayShowOverlayMinimalStyle) {
// Draw the fill background
val startX = 2 * padding + iconSize
val endX = width - 4 * padding
val fillWidth = endX - startX
canvas.drawRoundRect(
startX,
height / 2 - 5f,
endX,
height / 2 + 5f,
10f, 10f,
fillBackgroundPaint
)
// Draw the progress
val progressWidth = (progress.toFloat() / maxProgress) * fillWidth
canvas.drawRoundRect(
startX,
height / 2 - 5f,
startX + progressWidth,
height / 2 + 5f,
10f, 10f,
progressPaint
)
}
// Draw the icon
icon?.let {
val iconX = if (!overlayShowOverlayMinimalStyle) {
padding
} else {
padding + minimalStartX
}
val iconY = height / 2 - iconSize / 2
it.setBounds(
iconX.toInt(),
iconY.toInt(),
(iconX + iconSize).toInt(),
(iconY + iconSize).toInt()
)
it.draw(canvas)
}
// Draw the text on the right
val textX = if (!overlayShowOverlayMinimalStyle) {
width - 2 * padding
} else {
minimalStartX + minimalElementWidth - 2 * padding
}
val textY = height / 2 + textPaint.textSize / 3
// Draw the text
canvas.drawText(displayText, textX, textY, textPaint)
}
}

View File

@ -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);
}
}
}

View File

@ -31,14 +31,20 @@ import app.revanced.extension.shared.utils.PackageUtils;
import app.revanced.extension.youtube.settings.Settings; import app.revanced.extension.youtube.settings.Settings;
public class ExtendedUtils extends PackageUtils { public class ExtendedUtils extends PackageUtils {
private static boolean isVersionOrGreater(String version) {
return getAppVersionName().compareTo(version) >= 0;
}
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static final boolean IS_19_17_OR_GREATER = getAppVersionName().compareTo("19.17.00") >= 0; public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00");
public static final boolean IS_19_20_OR_GREATER = getAppVersionName().compareTo("19.20.00") >= 0; public static final boolean IS_19_20_OR_GREATER = isVersionOrGreater("19.20.00");
public static final boolean IS_19_21_OR_GREATER = getAppVersionName().compareTo("19.21.00") >= 0; public static final boolean IS_19_21_OR_GREATER = isVersionOrGreater("19.21.00");
public static final boolean IS_19_26_OR_GREATER = getAppVersionName().compareTo("19.26.00") >= 0; public static final boolean IS_19_26_OR_GREATER = isVersionOrGreater("19.26.00");
public static final boolean IS_19_28_OR_GREATER = getAppVersionName().compareTo("19.28.00") >= 0; public static final boolean IS_19_28_OR_GREATER = isVersionOrGreater("19.28.00");
public static final boolean IS_19_29_OR_GREATER = getAppVersionName().compareTo("19.29.00") >= 0; public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00");
public static final boolean IS_19_34_OR_GREATER = getAppVersionName().compareTo("19.34.00") >= 0; public static final boolean IS_19_34_OR_GREATER = isVersionOrGreater("19.34.00");
public static final boolean IS_20_09_OR_GREATER = isVersionOrGreater("20.09.00");
public static int validateValue(IntegerSetting settings, int min, int max, String message) { public static int validateValue(IntegerSetting settings, int min, int max, String message) {
int value = settings.get(); int value = settings.get();

View File

@ -133,13 +133,25 @@ public class VideoUtils extends IntentUtils {
} }
public static void openPlaylist(@NonNull String playlistId, @NonNull String videoId) { 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(); final StringBuilder sb = new StringBuilder();
if (videoId.isEmpty()) { if (videoId.isEmpty()) {
sb.append(getPlaylistUrl(playlistId)); sb.append(getPlaylistUrl(playlistId));
} else { } else {
sb.append(getVideoScheme(videoId, false)); sb.append(VIDEO_URL);
sb.append("&list="); sb.append(videoId);
sb.append("?list=");
sb.append(playlistId); sb.append(playlistId);
if (withTimestamp) {
final long currentVideoTimeInSeconds = VideoInformation.getVideoTimeInSeconds();
if (currentVideoTimeInSeconds > 0) {
sb.append("&t=");
sb.append(currentVideoTimeInSeconds);
}
}
} }
launchView(sb.toString(), getContext().getPackageName()); launchView(sb.toString(), getContext().getPackageName());
} }
@ -193,8 +205,8 @@ public class VideoUtils extends IntentUtils {
} }
public static void showPlaybackSpeedDialog(@NonNull Context context) { public static void showPlaybackSpeedDialog(@NonNull Context context) {
final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedListEntries(); final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedEntries();
final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedListEntryValues(); final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedEntryValues();
final float playbackSpeed = VideoInformation.getPlaybackSpeed(); final float playbackSpeed = VideoInformation.getPlaybackSpeed();
final int index = Arrays.binarySearch(playbackSpeedEntryValues, String.valueOf(playbackSpeed)); final int index = Arrays.binarySearch(playbackSpeedEntryValues, String.valueOf(playbackSpeed));
@ -202,6 +214,7 @@ public class VideoUtils extends IntentUtils {
new AlertDialog.Builder(context) new AlertDialog.Builder(context)
.setSingleChoiceItems(playbackSpeedEntries, index, (mDialog, mIndex) -> { .setSingleChoiceItems(playbackSpeedEntries, index, (mDialog, mIndex) -> {
final float selectedPlaybackSpeed = Float.parseFloat(playbackSpeedEntryValues[mIndex] + "f"); final float selectedPlaybackSpeed = Float.parseFloat(playbackSpeedEntryValues[mIndex] + "f");
VideoInformation.setPlaybackSpeed(selectedPlaybackSpeed);
VideoInformation.overridePlaybackSpeed(selectedPlaybackSpeed); VideoInformation.overridePlaybackSpeed(selectedPlaybackSpeed);
userSelectedPlaybackSpeed(selectedPlaybackSpeed); userSelectedPlaybackSpeed(selectedPlaybackSpeed);
mDialog.dismiss(); mDialog.dismiss();
@ -268,6 +281,13 @@ public class VideoUtils extends IntentUtils {
return !isExternalDownloaderLaunched.get() && original; 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. * Rest of the implementation added by patch.
*/ */

View File

@ -1,7 +1,9 @@
package com.google.android.apps.youtube.app.settings.videoquality; package com.google.android.apps.youtube.app.settings.videoquality;
import android.annotation.SuppressLint;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.res.Resources;
import android.os.Bundle; import android.os.Bundle;
import android.util.TypedValue; import android.util.TypedValue;
import android.view.View; import android.view.View;
@ -25,8 +27,8 @@ import app.revanced.extension.youtube.utils.ThemeUtils;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public class VideoQualitySettingsActivity extends Activity { public class VideoQualitySettingsActivity extends Activity {
private static final String rvxSettingsLabel = ResourceUtils.getString("revanced_extended_settings_title"); private static String rvxSettingsLabel;
private static final String searchLabel = ResourceUtils.getString("revanced_extended_settings_search_title"); private static String searchLabel;
private static WeakReference<SearchView> searchViewRef = new WeakReference<>(null); private static WeakReference<SearchView> searchViewRef = new WeakReference<>(null);
private static WeakReference<ImageView> closeButtonRef = new WeakReference<>(null); private static WeakReference<ImageView> closeButtonRef = new WeakReference<>(null);
private ReVancedPreferenceFragment fragment; private ReVancedPreferenceFragment fragment;
@ -71,6 +73,10 @@ public class VideoQualitySettingsActivity extends Activity {
return; return;
} }
// Set label
rvxSettingsLabel = getString("revanced_extended_settings_title");
searchLabel = getString("revanced_extended_settings_search_title");
// Set toolbar // Set toolbar
setToolbar(); setToolbar();
@ -85,6 +91,14 @@ public class VideoQualitySettingsActivity extends Activity {
} }
} }
@SuppressLint("DiscouragedApi")
private String getString(String str) {
Context baseContext = getBaseContext();
Resources resources = baseContext.getResources();
int identifier = resources.getIdentifier(str, "string", baseContext.getPackageName());
return resources.getString(identifier);
}
private void filterPreferences(String query) { private void filterPreferences(String query) {
if (fragment == null) return; if (fragment == null) return;
fragment.filterPreferences(query); fragment.filterPreferences(query);

View File

@ -4,5 +4,5 @@ org.gradle.parallel = true
android.useAndroidX = true android.useAndroidX = true
kotlin.code.style = official kotlin.code.style = official
kotlin.jvm.target.validation.mode = IGNORE kotlin.jvm.target.validation.mode = IGNORE
version = 5.6.1-dev.1 version = 5.6.2

View File

@ -55,7 +55,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -95,7 +95,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -133,7 +133,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -187,7 +187,8 @@
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [ "options": [
@ -242,7 +243,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -284,7 +285,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -435,7 +436,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [ "options": [
@ -483,7 +484,8 @@
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [ "options": [
@ -550,7 +552,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [ "options": [
@ -659,7 +661,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [ "options": [
@ -692,7 +694,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [ "options": [
@ -764,7 +766,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -786,7 +788,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -808,7 +810,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -849,7 +851,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -897,7 +899,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -975,7 +977,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1022,13 +1024,13 @@
"description": "Adds an option to disable the popup that appears when taking a screenshot.", "description": "Adds an option to disable the popup that appears when taking a screenshot.",
"use": true, "use": true,
"dependencies": [ "dependencies": [
"Settings for Reddit", "Settings for Reddit"
"ResourcePatch"
], ],
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [] "options": []
@ -1069,7 +1071,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1109,7 +1111,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1167,7 +1169,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1195,7 +1197,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1260,7 +1262,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [ "options": [
@ -1307,6 +1309,15 @@
"Clone": "com.rvx.android.apps.youtube.music", "Clone": "com.rvx.android.apps.youtube.music",
"Default": "app.rvx.android.apps.youtube.music" "Default": "app.rvx.android.apps.youtube.music"
} }
},
{
"key": "patchAllManifest",
"title": "Patch all manifest components",
"description": "Patch all permissions, intents and content provider authorities supported by GmsCore.",
"required": true,
"type": "kotlin.Boolean",
"default": true,
"values": null
} }
] ]
}, },
@ -1371,6 +1382,15 @@
"Clone": "com.rvx.android.apps.youtube.music", "Clone": "com.rvx.android.apps.youtube.music",
"Default": "app.rvx.android.apps.youtube.music" "Default": "app.rvx.android.apps.youtube.music"
} }
},
{
"key": "patchAllManifest",
"title": "Patch all manifest components",
"description": "Patch all permissions, intents and content provider authorities supported by GmsCore.",
"required": true,
"type": "kotlin.Boolean",
"default": true,
"values": null
} }
] ]
}, },
@ -1384,7 +1404,8 @@
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [] "options": []
@ -1442,7 +1463,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1468,7 +1489,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1517,7 +1538,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1532,7 +1553,8 @@
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [] "options": []
@ -1645,7 +1667,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1683,7 +1705,8 @@
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [] "options": []
@ -1705,7 +1728,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1768,7 +1791,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1783,7 +1806,8 @@
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [] "options": []
@ -1942,7 +1966,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -1980,7 +2004,8 @@
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [] "options": []
@ -1996,7 +2021,8 @@
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [] "options": []
@ -2110,7 +2136,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -2151,7 +2177,8 @@
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [] "options": []
@ -2172,7 +2199,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -2202,12 +2229,14 @@
"description": "Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically.", "description": "Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically.",
"use": true, "use": true,
"dependencies": [ "dependencies": [
"Settings for Reddit" "Settings for Reddit",
"ResourcePatch"
], ],
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [] "options": []
@ -2229,7 +2258,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -2269,7 +2298,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -2291,7 +2320,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -2336,7 +2365,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -2377,7 +2406,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -2392,7 +2421,8 @@
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [] "options": []
@ -2451,7 +2481,8 @@
"compatiblePackages": { "compatiblePackages": {
"com.reddit.frontpage": [ "com.reddit.frontpage": [
"2024.17.0", "2024.17.0",
"2025.05.1" "2025.05.1",
"2025.12.1"
] ]
}, },
"options": [ "options": [
@ -2496,7 +2527,7 @@
"description": "The settings menu name that the RVX settings menu should be above.", "description": "The settings menu name that the RVX settings menu should be above.",
"required": true, "required": true,
"type": "kotlin.String", "type": "kotlin.String",
"default": "@string/about_key", "default": "@string/parent_tools_key",
"values": { "values": {
"Parent settings": "@string/parent_tools_key", "Parent settings": "@string/parent_tools_key",
"General": "@string/general_key", "General": "@string/general_key",
@ -2552,7 +2583,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [ "options": [
@ -2708,7 +2739,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -2765,7 +2796,10 @@
"compatiblePackages": { "compatiblePackages": {
"com.google.android.apps.youtube.music": [ "com.google.android.apps.youtube.music": [
"6.51.53", "6.51.53",
"7.16.53" "7.16.53",
"7.25.53",
"8.05.51",
"8.10.52"
] ]
}, },
"options": [] "options": []
@ -2809,7 +2843,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -2834,7 +2868,17 @@
"19.47.53" "19.47.53"
] ]
}, },
"options": [] "options": [
{
"key": "useIOSClient",
"title": "Use iOS client",
"description": "Add setting to set iOS client (Deprecated) as default client.",
"required": false,
"type": "kotlin.Boolean",
"default": false,
"values": null
}
]
}, },
{ {
"name": "Swipe controls", "name": "Swipe controls",
@ -3000,7 +3044,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [ "options": [
@ -3051,7 +3095,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []
@ -3070,6 +3114,7 @@
"BytecodePatch", "BytecodePatch",
"BytecodePatch", "BytecodePatch",
"BytecodePatch", "BytecodePatch",
"BytecodePatch",
"ResourcePatch" "ResourcePatch"
], ],
"compatiblePackages": { "compatiblePackages": {
@ -3143,7 +3188,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [ "options": [
@ -3181,7 +3226,7 @@
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51" "8.12.53"
] ]
}, },
"options": [] "options": []

View File

@ -257,6 +257,7 @@ public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdP
public static final fun getBottomSheetRecyclerView ()J public static final fun getBottomSheetRecyclerView ()J
public static final fun getButtonContainer ()J public static final fun getButtonContainer ()J
public static final fun getButtonIconPaddingMedium ()J public static final fun getButtonIconPaddingMedium ()J
public static final fun getChannelHandle ()J
public static final fun getChipCloud ()J public static final fun getChipCloud ()J
public static final fun getColorGrey ()J public static final fun getColorGrey ()J
public static final fun getDarkBackground ()J public static final fun getDarkBackground ()J
@ -458,7 +459,7 @@ public final class app/revanced/patches/reddit/utils/extension/SharedExtensionPa
} }
public final class app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatchKt { 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 { public final class app/revanced/patches/reddit/utils/settings/SettingsPatchKt {
@ -555,10 +556,9 @@ public final class app/revanced/patches/shared/mapping/ResourceElement {
} }
public final class app/revanced/patches/shared/mapping/ResourceMappingPatchKt { public final class app/revanced/patches/shared/mapping/ResourceMappingPatchKt {
public static final fun get (Ljava/util/List;Lapp/revanced/patches/shared/mapping/ResourceType;Ljava/lang/String;)J public static final fun getResourceId (Lapp/revanced/patches/shared/mapping/ResourceType;Ljava/lang/String;)J
public static final fun get (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)J public static final fun getResourceId (Ljava/lang/String;Ljava/lang/String;)J
public static final fun getResourceMappingPatch ()Lapp/revanced/patcher/patch/ResourcePatch; public static final fun getResourceMappingPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
public static final fun getResourceMappings ()Ljava/util/List;
} }
public final class app/revanced/patches/shared/mapping/ResourceType : java/lang/Enum { public final class app/revanced/patches/shared/mapping/ResourceType : java/lang/Enum {
@ -875,6 +875,7 @@ public final class app/revanced/patches/youtube/player/seekbar/SeekbarComponents
public final class app/revanced/patches/youtube/shorts/components/FingerprintsKt { public final class app/revanced/patches/youtube/shorts/components/FingerprintsKt {
public static final fun indexOfAddLiveHeaderElementsContainerInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I public static final fun indexOfAddLiveHeaderElementsContainerInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
public static final fun indexOfDismissInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I
} }
public final class app/revanced/patches/youtube/shorts/components/ShortsComponentPatchKt { public final class app/revanced/patches/youtube/shorts/components/ShortsComponentPatchKt {
@ -894,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 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 final class app/revanced/patches/youtube/utils/bottomsheet/BottomSheetHookPatchKt {
public static final fun getBottomSheetHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getBottomSheetHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
} }
@ -906,6 +911,10 @@ public final class app/revanced/patches/youtube/utils/controlsoverlay/ControlsOv
public static final fun getControlsOverlayConfigPatch ()Lapp/revanced/patcher/patch/BytecodePatch; 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 final class app/revanced/patches/youtube/utils/engagement/EngagementPanelHookPatchKt {
public static final fun getEngagementPanelHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; public static final fun getEngagementPanelHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
} }
@ -1038,6 +1047,7 @@ public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPa
public static final fun is_19_04_or_greater ()Z public static final fun is_19_04_or_greater ()Z
public static final fun is_19_05_or_greater ()Z public static final fun is_19_05_or_greater ()Z
public static final fun is_19_09_or_greater ()Z public static final fun is_19_09_or_greater ()Z
public static final fun is_19_11_or_greater ()Z
public static final fun is_19_15_or_greater ()Z public static final fun is_19_15_or_greater ()Z
public static final fun is_19_16_or_greater ()Z public static final fun is_19_16_or_greater ()Z
public static final fun is_19_17_or_greater ()Z public static final fun is_19_17_or_greater ()Z
@ -1052,10 +1062,12 @@ 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_34_or_greater ()Z
public static final fun is_19_36_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_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_43_or_greater ()Z
public static final fun is_19_44_or_greater ()Z public static final fun is_19_44_or_greater ()Z
public static final fun is_19_46_or_greater ()Z public static final fun is_19_46_or_greater ()Z
public static final fun is_19_49_or_greater ()Z public static final fun is_19_49_or_greater ()Z
public static final fun is_19_50_or_greater ()Z
public static final fun is_20_02_or_greater ()Z public static final fun is_20_02_or_greater ()Z
public static final fun is_20_03_or_greater ()Z public static final fun is_20_03_or_greater ()Z
public static final fun is_20_05_or_greater ()Z public static final fun is_20_05_or_greater ()Z

View File

@ -152,7 +152,10 @@ private enum class MethodCall(
RegisterNetworkCallback1( RegisterNetworkCallback1(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"registerNetworkCallback", "registerNetworkCallback",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"), arrayOf(
"Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;"
),
"V", "V",
), ),
RegisterNetworkCallback2( RegisterNetworkCallback2(
@ -174,13 +177,20 @@ private enum class MethodCall(
RequestNetwork1( RequestNetwork1(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"requestNetwork", "requestNetwork",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"), arrayOf(
"Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;"
),
"V", "V",
), ),
RequestNetwork2( RequestNetwork2(
"Landroid/net/ConnectivityManager;", "Landroid/net/ConnectivityManager;",
"requestNetwork", "requestNetwork",
arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;", "I"), arrayOf(
"Landroid/net/NetworkRequest;",
"Landroid/net/ConnectivityManager\$NetworkCallback;",
"I"
),
"V", "V",
), ),
RequestNetwork3( RequestNetwork3(

View File

@ -19,7 +19,8 @@ val edgeToEdgeDisplayPatch = resourcePatch(
// Instead, it checks compileSdkVersion and prints a warning. // Instead, it checks compileSdkVersion and prints a warning.
try { try {
val manifestElement = document.getNode("manifest") as Element val manifestElement = document.getNode("manifest") as Element
val compileSdkVersion = Integer.parseInt(manifestElement.getAttribute("android:compileSdkVersion")) val compileSdkVersion =
Integer.parseInt(manifestElement.getAttribute("android:compileSdkVersion"))
if (compileSdkVersion < 35) { if (compileSdkVersion < 35) {
printWarn("This app may not be forcing edge to edge display (compileSdkVersion: $compileSdkVersion)") printWarn("This app may not be forcing edge to edge display (compileSdkVersion: $compileSdkVersion)")
} }

View File

@ -4,23 +4,27 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.extension.Constants.ACCOUNT_CLASS_DESCRIPTOR import app.revanced.patches.music.utils.extension.Constants.ACCOUNT_CLASS_DESCRIPTOR
import app.revanced.patches.music.utils.patch.PatchList.HIDE_ACCOUNT_COMPONENTS import app.revanced.patches.music.utils.patch.PatchList.HIDE_ACCOUNT_COMPONENTS
import app.revanced.patches.music.utils.resourceid.channelHandle
import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.music.utils.settings.CategoryType import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.util.fingerprint.matchOrThrow
import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.fingerprint.methodOrThrow
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
@Suppress("unused") @Suppress("unused")
@ -84,17 +88,50 @@ val accountComponentsPatch = bytecodePatch(
} }
// account switcher // account switcher
namesInactiveAccountThumbnailSizeFingerprint.matchOrThrow().let { val textViewField = with(
it.method.apply { channelHandleFingerprint
val targetIndex = it.patternMatch!!.startIndex .methodOrThrow(namesInactiveAccountThumbnailSizeFingerprint)
val targetRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA ) {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(channelHandle)
getInstruction(
indexOfFirstInstructionOrThrow(literalIndex) {
opcode == Opcode.IPUT_OBJECT &&
getReference<FieldReference>()?.type == "Landroid/widget/TextView;"
},
).getReference<FieldReference>()
}
addInstructions( namesInactiveAccountThumbnailSizeFingerprint.methodOrThrow().apply {
targetIndex, """ var hook = false
invoke-static {v$targetRegister}, $ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Z)Z
move-result v$targetRegister implementation!!.instructions
""" .withIndex()
) .filter { (_, instruction) ->
val reference =
(instruction as? ReferenceInstruction)?.reference
instruction.opcode == Opcode.IGET_OBJECT &&
reference is FieldReference &&
reference == textViewField
}
.map { (index, _) -> index }
.forEach { index ->
val insertIndex = index - 1
if (!hook && getInstruction(insertIndex).opcode == Opcode.IF_NEZ) {
val insertRegister =
getInstruction<OneRegisterInstruction>(insertIndex).registerA
addInstructions(
insertIndex, """
invoke-static {v$insertRegister}, $ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Z)Z
move-result v$insertRegister
"""
)
hook = true
}
}
if (!hook) {
throw PatchException("Could not find TextUtils.isEmpty() index")
} }
} }

View File

@ -1,11 +1,11 @@
package app.revanced.patches.music.account.components package app.revanced.patches.music.account.components
import app.revanced.patches.music.utils.resourceid.accountSwitcherAccessibility import app.revanced.patches.music.utils.resourceid.accountSwitcherAccessibility
import app.revanced.patches.music.utils.resourceid.channelHandle
import app.revanced.patches.music.utils.resourceid.menuEntry import app.revanced.patches.music.utils.resourceid.menuEntry
import app.revanced.patches.music.utils.resourceid.namesInactiveAccountThumbnailSize import app.revanced.patches.music.utils.resourceid.namesInactiveAccountThumbnailSize
import app.revanced.patches.music.utils.resourceid.tosFooter import app.revanced.patches.music.utils.resourceid.tosFooter
import app.revanced.util.fingerprint.legacyFingerprint import app.revanced.util.fingerprint.legacyFingerprint
import com.android.tools.smali.dexlib2.Opcode
internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint( internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
name = "accountSwitcherAccessibilityLabelFingerprint", name = "accountSwitcherAccessibilityLabelFingerprint",
@ -14,6 +14,12 @@ internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint(
literals = listOf(accountSwitcherAccessibility) literals = listOf(accountSwitcherAccessibility)
) )
internal val channelHandleFingerprint = legacyFingerprint(
name = "channelHandleFingerprint",
returnType = "V",
literals = listOf(channelHandle),
)
internal val menuEntryFingerprint = legacyFingerprint( internal val menuEntryFingerprint = legacyFingerprint(
name = "menuEntryFingerprint", name = "menuEntryFingerprint",
returnType = "V", returnType = "V",
@ -24,19 +30,6 @@ internal val namesInactiveAccountThumbnailSizeFingerprint = legacyFingerprint(
name = "namesInactiveAccountThumbnailSizeFingerprint", name = "namesInactiveAccountThumbnailSizeFingerprint",
returnType = "V", returnType = "V",
parameters = listOf("L", "Ljava/lang/Object;"), parameters = listOf("L", "Ljava/lang/Object;"),
opcodes = listOf(
Opcode.IF_NEZ,
Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.GOTO,
Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.IF_EQZ
),
literals = listOf(namesInactiveAccountThumbnailSize) literals = listOf(namesInactiveAccountThumbnailSize)
) )

View File

@ -115,7 +115,8 @@ val adsPatch = bytecodePatch(
.methodOrThrow(getPremiumDialogParentFingerprint) .methodOrThrow(getPremiumDialogParentFingerprint)
.apply { .apply {
val setContentViewIndex = indexOfSetContentViewInstruction(this) val setContentViewIndex = indexOfSetContentViewInstruction(this)
val dialogInstruction = getInstruction<FiveRegisterInstruction>(setContentViewIndex) val dialogInstruction =
getInstruction<FiveRegisterInstruction>(setContentViewIndex)
val dialogRegister = dialogInstruction.registerC val dialogRegister = dialogInstruction.registerC
val viewRegister = dialogInstruction.registerD val viewRegister = dialogInstruction.registerD

View File

@ -97,8 +97,6 @@ internal val showDialogCommandFingerprint = legacyFingerprint(
name = "showDialogCommandFingerprint", name = "showDialogCommandFingerprint",
returnType = "V", returnType = "V",
opcodes = listOf( opcodes = listOf(
Opcode.IF_EQ,
Opcode.IGET_OBJECT,
Opcode.INVOKE_VIRTUAL, Opcode.INVOKE_VIRTUAL,
Opcode.IGET, // get dialog code Opcode.IGET, // get dialog code
), ),

View File

@ -32,25 +32,32 @@ private val spoofAppVersionBytecodePatch = bytecodePatch(
) )
execute { execute {
if (!is_6_43_or_greater || is_7_25_or_greater) { if (!is_6_43_or_greater) {
return@execute return@execute
} }
if (is_7_17_or_greater) { var defaultVersionString = "6.42.55"
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
name == "SpoofAppVersionDefaultString" if (is_7_17_or_greater && !is_7_25_or_greater) {
}.replaceInstruction( defaultVersionString = "7.16.53"
0, defaultValue = "true"
"const-string v0, \"7.16.53\""
)
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) { findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
name == "SpoofAppVersionDefaultBoolean" name == "SpoofAppVersionDefaultBoolean"
}.replaceInstruction( }.replaceInstruction(
0, 0,
"const/4 v0, 0x1" "const/4 v0, 0x1"
) )
defaultValue = "true"
} }
if (is_7_25_or_greater) {
defaultVersionString = "7.17.52"
}
findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) {
name == "SpoofAppVersionDefaultString"
}.replaceInstruction(
0,
"const-string v0, \"$defaultVersionString\""
)
} }
} }
@ -63,6 +70,9 @@ val spoofAppVersionPatch = resourcePatch(
YOUTUBE_MUSIC_PACKAGE_NAME( YOUTUBE_MUSIC_PACKAGE_NAME(
"6.51.53", "6.51.53",
"7.16.53", "7.16.53",
"7.25.53",
"8.05.51",
"8.10.52",
), ),
) )
@ -73,13 +83,19 @@ val spoofAppVersionPatch = resourcePatch(
) )
execute { execute {
if (!is_6_43_or_greater || is_7_25_or_greater) { if (!is_6_43_or_greater) {
printWarn("\"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 6.43.53 ~ 7.24.51.") printWarn("\"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 6.51.53 or later.")
return@execute return@execute
} }
if (is_7_17_or_greater) { if (!is_7_17_or_greater) {
appendAppVersion("6.42.55")
}
if (is_7_17_or_greater && !is_7_25_or_greater) {
appendAppVersion("7.16.53") appendAppVersion("7.16.53")
} }
if (is_7_25_or_greater) {
appendAppVersion("7.17.52")
}
addSwitchPreference( addSwitchPreference(
CategoryType.GENERAL, CategoryType.GENERAL,

View File

@ -206,7 +206,8 @@ val changeHeaderPatch = resourcePatch(
printWarn(warnings) printWarn(warnings)
} }
val isLegacyLogoExists = get("res").resolve("drawable-xxhdpi").resolve("ytm_logo.png").exists() val isLegacyLogoExists =
get("res").resolve("drawable-xxhdpi").resolve("ytm_logo.png").exists()
if (is_7_27_or_greater && isLegacyLogoExists) { if (is_7_27_or_greater && isLegacyLogoExists) {
document("res/layout/signin_fragment.xml").use { document -> document("res/layout/signin_fragment.xml").use { document ->
document.doRecursively node@{ node -> document.doRecursively node@{ node ->

View File

@ -24,7 +24,7 @@ import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.indexOfFirstInstructionReversedOrThrow import app.revanced.util.indexOfFirstInstructionReversedOrThrow
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
private const val EXTENSION_METHOD_DESCRIPTOR = private const val EXTENSION_METHOD_DESCRIPTOR =
@ -41,7 +41,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
"7.16.53", "7.16.53",
"7.25.53", "7.25.53",
"8.05.51", "8.05.51",
"8.10.51", "8.12.53",
), ),
) )
@ -57,7 +57,7 @@ val cairoSplashAnimationPatch = bytecodePatch(
return@execute return@execute
} else if (!is_7_20_or_greater) { } else if (!is_7_20_or_greater) {
cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall( cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall(
45635386L, CAIRO_SPLASH_ANIMATION_FEATURE_FLAG,
EXTENSION_METHOD_DESCRIPTOR EXTENSION_METHOD_DESCRIPTOR
) )
} else { } else {
@ -69,18 +69,13 @@ val cairoSplashAnimationPatch = bytecodePatch(
opcode == Opcode.INVOKE_VIRTUAL && opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.name == "setContentView" getReference<MethodReference>()?.name == "setContentView"
} + 1 } + 1
val viewStubFindViewByIdIndex = indexOfFirstInstructionOrThrow(literalIndex) { val freeIndex = indexOfFirstInstructionOrThrow(insertIndex, Opcode.CONST)
val reference = getReference<MethodReference>()
opcode == Opcode.INVOKE_VIRTUAL &&
reference?.name == "findViewById" &&
reference.definingClass != "Landroid/view/View;"
}
val freeRegister = val freeRegister =
getInstruction<FiveRegisterInstruction>(viewStubFindViewByIdIndex).registerD getInstruction<OneRegisterInstruction>(freeIndex).registerA
val jumpIndex = indexOfFirstInstructionReversedOrThrow( val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex) {
viewStubFindViewByIdIndex, opcode == Opcode.INVOKE_VIRTUAL &&
Opcode.IGET_OBJECT getReference<MethodReference>()?.parameterTypes?.firstOrNull() == "Ljava/lang/Runnable;"
) } + 1
addInstructionsWithLabels( addInstructionsWithLabels(
insertIndex, """ insertIndex, """

View File

@ -5,6 +5,8 @@ import app.revanced.patches.music.utils.resourceid.mainActivityLaunchAnimation
import app.revanced.util.fingerprint.legacyFingerprint import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.indexOfFirstLiteralInstruction import app.revanced.util.indexOfFirstLiteralInstruction
internal const val CAIRO_SPLASH_ANIMATION_FEATURE_FLAG = 45635386L
/** /**
* This fingerprint is compatible with YouTube Music v7.06.53+ * This fingerprint is compatible with YouTube Music v7.06.53+
*/ */
@ -20,7 +22,7 @@ internal val cairoSplashAnimationConfigFingerprint = legacyFingerprint(
if (is_7_20_or_greater) { if (is_7_20_or_greater) {
method.indexOfFirstLiteralInstruction(mainActivityLaunchAnimation) >= 0 method.indexOfFirstLiteralInstruction(mainActivityLaunchAnimation) >= 0
} else { } else {
method.indexOfFirstLiteralInstruction(45635386) >= 0 method.indexOfFirstLiteralInstruction(CAIRO_SPLASH_ANIMATION_FEATURE_FLAG) >= 0
} }
} }
) )

View File

@ -1,13 +1,13 @@
package app.revanced.patches.music.misc.watchhistory package app.revanced.patches.music.misc.watchhistory
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.shared.trackingurlhook.hookWatchHistory
import app.revanced.patches.shared.trackingurlhook.trackingUrlHookPatch
import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.music.utils.patch.PatchList.WATCH_HISTORY import app.revanced.patches.music.utils.patch.PatchList.WATCH_HISTORY
import app.revanced.patches.music.utils.settings.CategoryType import app.revanced.patches.music.utils.settings.CategoryType
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.settingsPatch import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.patches.shared.trackingurlhook.hookWatchHistory
import app.revanced.patches.shared.trackingurlhook.trackingUrlHookPatch
@Suppress("unused") @Suppress("unused")
val watchHistoryPatch = bytecodePatch( val watchHistoryPatch = bytecodePatch(

View File

@ -124,14 +124,17 @@ val navigationBarComponentsPatch = bytecodePatch(
opcode == Opcode.IGET_OBJECT && opcode == Opcode.IGET_OBJECT &&
getReference<FieldReference>()?.type == "Ljava/lang/String;" getReference<FieldReference>()?.type == "Ljava/lang/String;"
} }
val browseIdReference = getInstruction<ReferenceInstruction>(browseIdIndex).reference as FieldReference val browseIdReference =
getInstruction<ReferenceInstruction>(browseIdIndex).reference as FieldReference
val fieldName = browseIdReference.name val fieldName = browseIdReference.name
val componentIndex = indexOfFirstInstructionOrThrow(stringIndex) { val componentIndex = indexOfFirstInstructionOrThrow(stringIndex) {
opcode == Opcode.IGET_OBJECT && opcode == Opcode.IGET_OBJECT &&
getReference<FieldReference>()?.toString() == browseIdReference.toString() getReference<FieldReference>()?.toString() == browseIdReference.toString()
} }
val browseIdRegister = getInstruction<TwoRegisterInstruction>(componentIndex).registerA val browseIdRegister =
val componentRegister = getInstruction<TwoRegisterInstruction>(componentIndex).registerB getInstruction<TwoRegisterInstruction>(componentIndex).registerA
val componentRegister =
getInstruction<TwoRegisterInstruction>(componentIndex).registerB
val enumIndex = it.patternMatch!!.startIndex + 3 val enumIndex = it.patternMatch!!.startIndex + 3
val enumRegister = getInstruction<OneRegisterInstruction>(enumIndex).registerA val enumRegister = getInstruction<OneRegisterInstruction>(enumIndex).registerA

View File

@ -54,7 +54,7 @@ internal val engagementPanelHeightFingerprint = legacyFingerprint(
parameters = emptyList(), parameters = emptyList(),
customFingerprint = { method, _ -> customFingerprint = { method, _ ->
AccessFlags.FINAL.isSet(method.accessFlags) && AccessFlags.FINAL.isSet(method.accessFlags) &&
method.containsLiteralInstruction(1) && method.containsLiteralInstruction(1) &&
method.indexOfFirstInstruction { method.indexOfFirstInstruction {
opcode == Opcode.INVOKE_VIRTUAL && opcode == Opcode.INVOKE_VIRTUAL &&
getReference<MethodReference>()?.name == "booleanValue" getReference<MethodReference>()?.name == "booleanValue"

View File

@ -746,15 +746,22 @@ val playerComponentsPatch = bytecodePatch(
val freeRegister = val freeRegister =
getInstruction<FiveRegisterInstruction>(bottomSheetBehaviorIndex).registerD getInstruction<FiveRegisterInstruction>(bottomSheetBehaviorIndex).registerD
val getFieldIndex = bottomSheetBehaviorIndex - 2
val getFieldReference =
getInstruction<ReferenceInstruction>(getFieldIndex).reference
val getFieldInstruction = getInstruction<TwoRegisterInstruction>(getFieldIndex)
addInstructionsWithLabels( addInstructionsWithLabels(
bottomSheetBehaviorIndex - 2, getFieldIndex + 1,
""" """
invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer()Z invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer()Z
move-result v$freeRegister move-result v$freeRegister
if-nez v$freeRegister, :dismiss if-nez v$freeRegister, :dismiss
iget-object v${getFieldInstruction.registerA}, v${getFieldInstruction.registerB}, $getFieldReference
""", """,
ExternalLabel("dismiss", getInstruction(bottomSheetBehaviorIndex + 1)) ExternalLabel("dismiss", getInstruction(bottomSheetBehaviorIndex + 1))
) )
removeInstruction(getFieldIndex)
} ?: throw PatchException("Could not find targetMethod") } ?: throw PatchException("Could not find targetMethod")
} }

View File

@ -16,7 +16,7 @@ internal object Constants {
"7.16.53", // This is the latest version that supports the 'Spoof app version' patch. "7.16.53", // This is the latest version that supports the 'Spoof app version' patch.
"7.25.53", // This is the last supported version for 2024. "7.25.53", // This is the last supported version for 2024.
"8.05.51", // This was the latest version supported by the previous RVX patch. "8.05.51", // This was the latest version supported by the previous RVX patch.
"8.10.51", // This is the latest version supported by the RVX patch. "8.12.53", // This is the latest version supported by the RVX patch.
) )
) )
} }

View File

@ -1,14 +1,8 @@
package app.revanced.patches.music.utils.extension package app.revanced.patches.music.utils.extension
import app.revanced.patches.music.utils.extension.hooks.applicationInitHook import app.revanced.patches.music.utils.extension.hooks.applicationInitHook
import app.revanced.patches.music.utils.extension.hooks.mainActivityBaseContextHook
import app.revanced.patches.shared.extension.hooks.cronetEngineContextHook
import app.revanced.patches.shared.extension.hooks.firebaseInitProviderContextHook
import app.revanced.patches.shared.extension.sharedExtensionPatch import app.revanced.patches.shared.extension.sharedExtensionPatch
val sharedExtensionPatch = sharedExtensionPatch( val sharedExtensionPatch = sharedExtensionPatch(
applicationInitHook, applicationInitHook,
cronetEngineContextHook,
firebaseInitProviderContextHook,
mainActivityBaseContextHook,
) )

View File

@ -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"
}
}

View File

@ -22,7 +22,6 @@ import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus
import app.revanced.patches.music.utils.settings.addPreferenceWithIntent import app.revanced.patches.music.utils.settings.addPreferenceWithIntent
import app.revanced.patches.music.utils.settings.addSwitchPreference import app.revanced.patches.music.utils.settings.addSwitchPreference
import app.revanced.patches.music.utils.settings.settingsPatch import app.revanced.patches.music.utils.settings.settingsPatch
import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint
import app.revanced.patches.shared.customspeed.customPlaybackSpeedPatch import app.revanced.patches.shared.customspeed.customPlaybackSpeedPatch
import app.revanced.patches.shared.extension.Constants.PATCHES_PATH import app.revanced.patches.shared.extension.Constants.PATCHES_PATH
@ -31,6 +30,7 @@ import app.revanced.patches.shared.indexOfBrandInstruction
import app.revanced.patches.shared.indexOfManufacturerInstruction import app.revanced.patches.shared.indexOfManufacturerInstruction
import app.revanced.patches.shared.indexOfModelInstruction import app.revanced.patches.shared.indexOfModelInstruction
import app.revanced.patches.shared.indexOfReleaseInstruction import app.revanced.patches.shared.indexOfReleaseInstruction
import app.revanced.patches.shared.spoof.blockrequest.blockRequestPatch
import app.revanced.util.findMethodOrThrow import app.revanced.util.findMethodOrThrow
import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall
import app.revanced.util.fingerprint.matchOrThrow import app.revanced.util.fingerprint.matchOrThrow

View File

@ -9,9 +9,8 @@ import app.revanced.patches.shared.mapping.ResourceType.ID
import app.revanced.patches.shared.mapping.ResourceType.LAYOUT import app.revanced.patches.shared.mapping.ResourceType.LAYOUT
import app.revanced.patches.shared.mapping.ResourceType.STRING import app.revanced.patches.shared.mapping.ResourceType.STRING
import app.revanced.patches.shared.mapping.ResourceType.STYLE import app.revanced.patches.shared.mapping.ResourceType.STYLE
import app.revanced.patches.shared.mapping.get import app.revanced.patches.shared.mapping.getResourceId
import app.revanced.patches.shared.mapping.resourceMappingPatch import app.revanced.patches.shared.mapping.resourceMappingPatch
import app.revanced.patches.shared.mapping.resourceMappings
var accountSwitcherAccessibility = -1L var accountSwitcherAccessibility = -1L
private set private set
@ -25,6 +24,8 @@ var buttonContainer = -1L
private set private set
var buttonIconPaddingMedium = -1L var buttonIconPaddingMedium = -1L
private set private set
var channelHandle = -1L
private set
var chipCloud = -1L var chipCloud = -1L
private set private set
var colorGrey = -1L var colorGrey = -1L
@ -126,217 +127,61 @@ internal val sharedResourceIdPatch = resourcePatch(
dependsOn(resourceMappingPatch) dependsOn(resourceMappingPatch)
execute { execute {
accountSwitcherAccessibility = resourceMappings[ accountSwitcherAccessibility = getResourceId(STRING, "account_switcher_accessibility_label")
STRING, actionBarLogo = getResourceId(DRAWABLE, "action_bar_logo")
"account_switcher_accessibility_label", actionBarLogoRingo2 = getResourceId(DRAWABLE, "action_bar_logo_ringo2")
] bottomSheetRecyclerView = getResourceId(LAYOUT, "bottom_sheet_recycler_view")
actionBarLogo = resourceMappings[ buttonContainer = getResourceId(ID, "button_container")
DRAWABLE, buttonIconPaddingMedium = getResourceId(DIMEN, "button_icon_padding_medium")
"action_bar_logo", channelHandle = getResourceId(ID, "channel_handle")
] chipCloud = getResourceId(LAYOUT, "chip_cloud")
actionBarLogoRingo2 = resourceMappings[ colorGrey = getResourceId(COLOR, "ytm_color_grey_12")
DRAWABLE, darkBackground = getResourceId(ID, "dark_background")
"action_bar_logo_ringo2", designBottomSheetDialog = getResourceId(LAYOUT, "design_bottom_sheet_dialog")
] elementsContainer = getResourceId(ID, "elements_container")
bottomSheetRecyclerView = resourceMappings[ endButtonsContainer = getResourceId(ID, "end_buttons_container")
LAYOUT, floatingLayout = getResourceId(ID, "floating_layout")
"bottom_sheet_recycler_view" historyMenuItem = getResourceId(ID, "history_menu_item")
] inlineTimeBarAdBreakMarkerColor =
buttonContainer = resourceMappings[ getResourceId(COLOR, "inline_time_bar_ad_break_marker_color")
ID, inlineTimeBarProgressColor = getResourceId(COLOR, "inline_time_bar_progress_color")
"button_container" interstitialsContainer = getResourceId(ID, "interstitials_container")
] isTablet = getResourceId(BOOL, "is_tablet")
buttonIconPaddingMedium = resourceMappings[ likeDislikeContainer = getResourceId(ID, "like_dislike_container")
DIMEN, mainActivityLaunchAnimation = getResourceId(LAYOUT, "main_activity_launch_animation")
"button_icon_padding_medium" menuEntry = getResourceId(LAYOUT, "menu_entry")
] miniPlayerDefaultText = getResourceId(STRING, "mini_player_default_text")
chipCloud = resourceMappings[ miniPlayerMdxPlaying = getResourceId(STRING, "mini_player_mdx_playing")
LAYOUT, miniPlayerPlayPauseReplayButton = getResourceId(ID, "mini_player_play_pause_replay_button")
"chip_cloud" miniPlayerViewPager = getResourceId(ID, "mini_player_view_pager")
] modernDialogBackground = getResourceId(DRAWABLE, "modern_dialog_background")
colorGrey = resourceMappings[ musicNotifierShelf = getResourceId(LAYOUT, "music_notifier_shelf")
COLOR, musicTasteBuilderShelf = getResourceId(LAYOUT, "music_tastebuilder_shelf")
"ytm_color_grey_12" namesInactiveAccountThumbnailSize =
] getResourceId(DIMEN, "names_inactive_account_thumbnail_size")
darkBackground = resourceMappings[ offlineSettingsMenuItem = getResourceId(ID, "offline_settings_menu_item")
ID, playerOverlayChip = getResourceId(ID, "player_overlay_chip")
"dark_background" playerViewPager = getResourceId(ID, "player_view_pager")
] privacyTosFooter = getResourceId(ID, "privacy_tos_footer")
designBottomSheetDialog = resourceMappings[ qualityAuto = getResourceId(STRING, "quality_auto")
LAYOUT, remixGenericButtonSize = getResourceId(DIMEN, "remix_generic_button_size")
"design_bottom_sheet_dialog" searchButton = getResourceId(LAYOUT, "search_button")
] slidingDialogAnimation = getResourceId(STYLE, "SlidingDialogAnimation")
elementsContainer = resourceMappings[ tapBloomView = getResourceId(ID, "tap_bloom_view")
ID, text1 = getResourceId(ID, "text1")
"elements_container" toolTipContentView = getResourceId(LAYOUT, "tooltip_content_view")
] topEnd = getResourceId(ID, "TOP_END")
endButtonsContainer = resourceMappings[ topStart = getResourceId(ID, "TOP_START")
ID, topBarMenuItemImageView = getResourceId(ID, "top_bar_menu_item_image_view")
"end_buttons_container" tosFooter = getResourceId(ID, "tos_footer")
] touchOutside = getResourceId(ID, "touch_outside")
floatingLayout = resourceMappings[ trimSilenceSwitch = getResourceId(ID, "trim_silence_switch")
ID, varispeedUnavailableTitle = getResourceId(STRING, "varispeed_unavailable_title")
"floating_layout" ytFillSamples = getResourceId(DRAWABLE, "yt_fill_samples_vd_theme_24")
] ytFillYouTubeMusic = getResourceId(DRAWABLE, "yt_fill_youtube_music_vd_theme_24")
historyMenuItem = resourceMappings[ ytOutlineSamples = getResourceId(DRAWABLE, "yt_outline_samples_vd_theme_24")
ID, ytOutlineYouTubeMusic = getResourceId(DRAWABLE, "yt_outline_youtube_music_vd_theme_24")
"history_menu_item" ytmLogo = getResourceId(DRAWABLE, "ytm_logo")
] ytmLogoRingo2 = getResourceId(DRAWABLE, "ytm_logo_ringo2")
inlineTimeBarAdBreakMarkerColor = resourceMappings[
COLOR,
"inline_time_bar_ad_break_marker_color"
]
inlineTimeBarProgressColor = resourceMappings[
COLOR,
"inline_time_bar_progress_color"
]
interstitialsContainer = resourceMappings[
ID,
"interstitials_container"
]
isTablet = resourceMappings[
BOOL,
"is_tablet"
]
likeDislikeContainer = resourceMappings[
ID,
"like_dislike_container"
]
mainActivityLaunchAnimation = resourceMappings[
LAYOUT,
"main_activity_launch_animation"
]
menuEntry = resourceMappings[
LAYOUT,
"menu_entry"
]
miniPlayerDefaultText = resourceMappings[
STRING,
"mini_player_default_text"
]
miniPlayerMdxPlaying = resourceMappings[
STRING,
"mini_player_mdx_playing"
]
miniPlayerPlayPauseReplayButton = resourceMappings[
ID,
"mini_player_play_pause_replay_button"
]
miniPlayerViewPager = resourceMappings[
ID,
"mini_player_view_pager"
]
modernDialogBackground = resourceMappings[
DRAWABLE,
"modern_dialog_background"
]
musicNotifierShelf = resourceMappings[
LAYOUT,
"music_notifier_shelf"
]
musicTasteBuilderShelf = resourceMappings[
LAYOUT,
"music_tastebuilder_shelf"
]
namesInactiveAccountThumbnailSize = resourceMappings[
DIMEN,
"names_inactive_account_thumbnail_size"
]
offlineSettingsMenuItem = resourceMappings[
ID,
"offline_settings_menu_item"
]
playerOverlayChip = resourceMappings[
ID,
"player_overlay_chip"
]
playerViewPager = resourceMappings[
ID,
"player_view_pager"
]
privacyTosFooter = resourceMappings[
ID,
"privacy_tos_footer"
]
qualityAuto = resourceMappings[
STRING,
"quality_auto"
]
remixGenericButtonSize = resourceMappings[
DIMEN,
"remix_generic_button_size"
]
searchButton = resourceMappings[
LAYOUT,
"search_button"
]
slidingDialogAnimation = resourceMappings[
STYLE,
"SlidingDialogAnimation"
]
tapBloomView = resourceMappings[
ID,
"tap_bloom_view"
]
text1 = resourceMappings[
ID,
"text1"
]
toolTipContentView = resourceMappings[
LAYOUT,
"tooltip_content_view"
]
topEnd = resourceMappings[
ID,
"TOP_END"
]
topStart = resourceMappings[
ID,
"TOP_START"
]
topBarMenuItemImageView = resourceMappings[
ID,
"top_bar_menu_item_image_view"
]
tosFooter = resourceMappings[
ID,
"tos_footer"
]
touchOutside = resourceMappings[
ID,
"touch_outside"
]
trimSilenceSwitch = resourceMappings[
ID,
"trim_silence_switch"
]
varispeedUnavailableTitle = resourceMappings[
STRING,
"varispeed_unavailable_title"
]
ytFillSamples = resourceMappings[
DRAWABLE,
"yt_fill_samples_vd_theme_24",
]
ytFillYouTubeMusic = resourceMappings[
DRAWABLE,
"yt_fill_youtube_music_vd_theme_24",
]
ytOutlineSamples = resourceMappings[
DRAWABLE,
"yt_outline_samples_vd_theme_24",
]
ytOutlineYouTubeMusic = resourceMappings[
DRAWABLE,
"yt_outline_youtube_music_vd_theme_24",
]
ytmLogo = resourceMappings[
DRAWABLE,
"ytm_logo",
]
ytmLogoRingo2 = resourceMappings[
DRAWABLE,
"ytm_logo_ringo2",
]
} }
} }

View File

@ -25,7 +25,8 @@ val videoTypeHookPatch = bytecodePatch(
videoTypeFingerprint.methodOrThrow(videoTypeParentFingerprint).apply { videoTypeFingerprint.methodOrThrow(videoTypeParentFingerprint).apply {
val getEnumIndex = indexOfGetEnumInstruction(this) val getEnumIndex = indexOfGetEnumInstruction(this)
val enumClass = (getInstruction<ReferenceInstruction>(getEnumIndex).reference as MethodReference).definingClass val enumClass =
(getInstruction<ReferenceInstruction>(getEnumIndex).reference as MethodReference).definingClass
val referenceIndex = indexOfFirstInstructionOrThrow(getEnumIndex) { val referenceIndex = indexOfFirstInstructionOrThrow(getEnumIndex) {
opcode == Opcode.SGET_OBJECT && opcode == Opcode.SGET_OBJECT &&
getReference<FieldReference>()?.type == enumClass getReference<FieldReference>()?.type == enumClass

View File

@ -71,7 +71,8 @@ val playerResponseMethodHookPatch = bytecodePatch(
val beforeVideoIdHooks = val beforeVideoIdHooks =
hooks.filterIsInstance<Hook.PlayerParameterBeforeVideoId>().asReversed() hooks.filterIsInstance<Hook.PlayerParameterBeforeVideoId>().asReversed()
val videoIdHooks = hooks.filterIsInstance<Hook.VideoId>().asReversed() val videoIdHooks = hooks.filterIsInstance<Hook.VideoId>().asReversed()
val videoIdAndPlaylistIdHooks = hooks.filterIsInstance<Hook.VideoIdAndPlaylistId>().asReversed() val videoIdAndPlaylistIdHooks =
hooks.filterIsInstance<Hook.VideoIdAndPlaylistId>().asReversed()
val afterVideoIdHooks = hooks.filterIsInstance<Hook.PlayerParameter>().asReversed() val afterVideoIdHooks = hooks.filterIsInstance<Hook.PlayerParameter>().asReversed()
// Add the hooks in this specific order as they insert instructions at the beginning of the method. // Add the hooks in this specific order as they insert instructions at the beginning of the method.

View File

@ -27,14 +27,6 @@ import com.android.tools.smali.dexlib2.iface.reference.FieldReference
private const val EXTENSION_CLASS_DESCRIPTOR = private const val EXTENSION_CLASS_DESCRIPTOR =
"$PATCHES_PATH/GeneralAdsPatch;" "$PATCHES_PATH/GeneralAdsPatch;"
private val isCommentAdsMethod: Method.() -> Boolean = {
parameterTypes.size == 1 &&
parameterTypes.first().startsWith("Lcom/reddit/ads/conversation/") &&
accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL &&
returnType == "V" &&
indexOfFirstStringInstruction("ad") >= 0
}
@Suppress("unused") @Suppress("unused")
val adsPatch = bytecodePatch( val adsPatch = bytecodePatch(
HIDE_ADS.title, HIDE_ADS.title,
@ -94,11 +86,20 @@ val adsPatch = bytecodePatch(
if (is_2025_06_or_greater) { if (is_2025_06_or_greater) {
listOf( listOf(
commentAdCommentScreenAdViewFingerprint, commentAdCommentScreenAdViewFingerprint,
commentAdDetailListHeaderViewFingerprint commentAdDetailListHeaderViewFingerprint,
commentsViewModelFingerprint
).forEach { fingerprint -> ).forEach { fingerprint ->
fingerprint.methodOrThrow().hook() fingerprint.methodOrThrow().hook()
} }
} else { } else {
val isCommentAdsMethod: Method.() -> Boolean = {
parameterTypes.size == 1 &&
parameterTypes.first().startsWith("Lcom/reddit/ads/conversation/") &&
accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL &&
returnType == "V" &&
indexOfFirstStringInstruction("ad") >= 0
}
classes.forEach { classDef -> classes.forEach { classDef ->
classDef.methods.forEach { method -> classDef.methods.forEach { method ->
if (method.isCommentAdsMethod()) { if (method.isCommentAdsMethod()) {

View File

@ -8,6 +8,7 @@ import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.reference.MethodReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
internal val adPostFingerprint = legacyFingerprint( internal val adPostFingerprint = legacyFingerprint(
name = "adPostFingerprint", name = "adPostFingerprint",
@ -49,6 +50,20 @@ internal val commentAdDetailListHeaderViewFingerprint = legacyFingerprint(
}, },
) )
internal val commentsViewModelFingerprint = legacyFingerprint(
name = "commentsViewModelFingerprint",
returnType = "V",
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
parameters = listOf("L", "Z", "L", "I"),
customFingerprint = { method, classDef ->
classDef.superclass == "Lcom/reddit/screen/presentation/CompositionViewModel;" &&
method.indexOfFirstInstruction {
opcode == Opcode.NEW_INSTANCE &&
getReference<TypeReference>()?.type?.startsWith("Lcom/reddit/postdetail/comment/refactor/CommentsViewModel\$LoadAdsSeparately\$") == true
} >= 0
},
)
internal val newAdPostFingerprint = legacyFingerprint( internal val newAdPostFingerprint = legacyFingerprint(
name = "newAdPostFingerprint", name = "newAdPostFingerprint",
returnType = "L", returnType = "L",

View File

@ -56,7 +56,8 @@ val navigationButtonsPatch = bytecodePatch(
if (bottomNavScreenFingerprint.resolvable()) { if (bottomNavScreenFingerprint.resolvable()) {
val bottomNavScreenMutableClass = with(bottomNavScreenFingerprint.methodOrThrow()) { val bottomNavScreenMutableClass = with(bottomNavScreenFingerprint.methodOrThrow()) {
val startIndex = indexOfGetDimensionPixelSizeInstruction(this) val startIndex = indexOfGetDimensionPixelSizeInstruction(this)
val targetIndex = indexOfFirstInstructionOrThrow(startIndex, Opcode.NEW_INSTANCE) val targetIndex =
indexOfFirstInstructionOrThrow(startIndex, Opcode.NEW_INSTANCE)
val targetReference = val targetReference =
getInstruction<ReferenceInstruction>(targetIndex).reference.toString() getInstruction<ReferenceInstruction>(targetIndex).reference.toString()
@ -65,7 +66,9 @@ val navigationButtonsPatch = bytecodePatch(
?: throw ClassNotFoundException("Failed to find class $targetReference") ?: throw ClassNotFoundException("Failed to find class $targetReference")
} }
bottomNavScreenOnGlobalLayoutFingerprint.second.matchOrNull(bottomNavScreenMutableClass) bottomNavScreenOnGlobalLayoutFingerprint.second.matchOrNull(
bottomNavScreenMutableClass
)
?.let { ?.let {
it.method.apply { it.method.apply {
val startIndex = it.patternMatch!!.startIndex val startIndex = it.patternMatch!!.startIndex
@ -82,7 +85,8 @@ val navigationButtonsPatch = bytecodePatch(
// Legacy method. // Legacy method.
bottomNavScreenHandlerFingerprint.methodOrThrow().apply { bottomNavScreenHandlerFingerprint.methodOrThrow().apply {
val targetIndex = indexOfGetItemsInstruction(this) + 1 val targetIndex = indexOfGetItemsInstruction(this) + 1
val targetRegister = getInstruction<OneRegisterInstruction>(targetIndex).registerA val targetRegister =
getInstruction<OneRegisterInstruction>(targetIndex).registerA
addInstructions( addInstructions(
targetIndex + 1, """ targetIndex + 1, """

View File

@ -1,36 +0,0 @@
package app.revanced.patches.reddit.layout.screenshotpopup
import app.revanced.patches.reddit.utils.resourceid.screenShotShareBanner
import app.revanced.util.containsLiteralInstruction
import app.revanced.util.fingerprint.legacyFingerprint
import com.android.tools.smali.dexlib2.Opcode
/**
* Reddit 2025.06.0 ~
*/
internal val screenshotTakenBannerFingerprint = legacyFingerprint(
name = "screenshotTakenBannerFingerprint",
returnType = "L",
opcodes = listOf(
Opcode.CONST_4,
Opcode.IF_NE,
),
customFingerprint = { method, classDef ->
method.containsLiteralInstruction(screenShotShareBanner) &&
classDef.type.startsWith("Lcom/reddit/sharing/screenshot/composables/") &&
method.name == "invoke"
}
)
/**
* ~ Reddit 2025.05.1
*/
internal val screenshotTakenBannerLegacyFingerprint = legacyFingerprint(
name = "screenshotTakenBannerLegacyFingerprint",
returnType = "V",
parameters = listOf("Landroidx/compose/runtime/", "I"),
customFingerprint = { method, classDef ->
classDef.type.endsWith("\$ScreenshotTakenBannerKt\$lambda-1\$1;") &&
method.name == "invoke"
}
)

View File

@ -1,26 +1,22 @@
package app.revanced.patches.reddit.layout.screenshotpopup package app.revanced.patches.reddit.layout.screenshotpopup
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.smali.ExternalLabel
import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE
import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH
import app.revanced.patches.reddit.utils.patch.PatchList.DISABLE_SCREENSHOT_POPUP import app.revanced.patches.reddit.utils.patch.PatchList.DISABLE_SCREENSHOT_POPUP
import app.revanced.patches.reddit.utils.resourceid.screenShotShareBanner
import app.revanced.patches.reddit.utils.resourceid.sharedResourceIdPatch
import app.revanced.patches.reddit.utils.settings.is_2025_06_or_greater
import app.revanced.patches.reddit.utils.settings.settingsPatch import app.revanced.patches.reddit.utils.settings.settingsPatch
import app.revanced.patches.reddit.utils.settings.updatePatchStatus import app.revanced.patches.reddit.utils.settings.updatePatchStatus
import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.findMutableMethodOf
import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstructionReversedOrThrow import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
private const val EXTENSION_METHOD_DESCRIPTOR = import com.android.tools.smali.dexlib2.iface.reference.MethodReference
"$PATCHES_PATH/ScreenshotPopupPatch;->disableScreenshotPopup()Z"
@Suppress("unused") @Suppress("unused")
val screenshotPopupPatch = bytecodePatch( val screenshotPopupPatch = bytecodePatch(
@ -29,39 +25,67 @@ val screenshotPopupPatch = bytecodePatch(
) { ) {
compatibleWith(COMPATIBLE_PACKAGE) compatibleWith(COMPATIBLE_PACKAGE)
dependsOn( dependsOn(settingsPatch)
settingsPatch,
sharedResourceIdPatch,
)
execute { execute {
if (is_2025_06_or_greater) { fun indexOfShowBannerInstruction(method: Method) =
screenshotTakenBannerFingerprint.methodOrThrow().apply { method.indexOfFirstInstruction {
val literalIndex = indexOfFirstLiteralInstructionOrThrow(screenShotShareBanner) val reference = getReference<FieldReference>()
val insertIndex = indexOfFirstInstructionReversedOrThrow(literalIndex, Opcode.CONST_4) opcode == Opcode.IGET_OBJECT &&
val insertRegister = getInstruction<OneRegisterInstruction>(insertIndex).registerA reference?.name?.contains("shouldShowBanner") == true &&
val jumpIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.SGET_OBJECT) reference.definingClass.startsWith("Lcom/reddit/sharing/screenshot/") == true
}
addInstructionsWithLabels( fun indexOfSetValueInstruction(method: Method) =
insertIndex, """ method.indexOfFirstInstruction {
invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR getReference<MethodReference>()?.name == "setValue"
move-result v$insertRegister
if-nez v$insertRegister, :hidden
""", ExternalLabel("hidden", getInstruction(jumpIndex))
)
} }
} else {
screenshotTakenBannerLegacyFingerprint.methodOrThrow().apply { fun indexOfBooleanInstruction(method: Method, startIndex: Int = 0) =
addInstructionsWithLabels( method.indexOfFirstInstruction(startIndex) {
0, """ val reference = getReference<FieldReference>()
invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR opcode == Opcode.SGET_OBJECT &&
move-result v0 reference?.definingClass == "Ljava/lang/Boolean;" &&
if-eqz v0, :dismiss reference.type == "Ljava/lang/Boolean;"
return-void
""", ExternalLabel("dismiss", getInstruction(0))
)
} }
val isScreenShotMethod: Method.() -> Boolean = {
definingClass.startsWith("Lcom/reddit/sharing/screenshot/") &&
name == "invokeSuspend" &&
indexOfShowBannerInstruction(this) >= 0 &&
indexOfBooleanInstruction(this) >= 0 &&
indexOfSetValueInstruction(this) >= 0
}
var hookCount = 0
classes.forEach { classDef ->
classDef.methods.forEach { method ->
if (method.isScreenShotMethod()) {
proxy(classDef)
.mutableClass
.findMutableMethodOf(method)
.apply {
val showBannerIndex = indexOfShowBannerInstruction(this)
val booleanIndex = indexOfBooleanInstruction(this, showBannerIndex)
val booleanRegister =
getInstruction<OneRegisterInstruction>(booleanIndex).registerA
addInstructions(
booleanIndex + 1, """
invoke-static {v$booleanRegister}, $PATCHES_PATH/ScreenshotPopupPatch;->disableScreenshotPopup(Ljava/lang/Boolean;)Ljava/lang/Boolean;
move-result-object v$booleanRegister
"""
)
hookCount++
}
}
}
}
if (hookCount == 0) {
throw PatchException("Failed to find hook method")
} }
updatePatchStatus( updatePatchStatus(

View File

@ -1,5 +1,6 @@
package app.revanced.patches.reddit.layout.subredditdialog package app.revanced.patches.reddit.layout.subredditdialog
import app.revanced.patches.reddit.utils.resourceid.nsfwDialogTitle
import app.revanced.util.fingerprint.legacyFingerprint import app.revanced.util.fingerprint.legacyFingerprint
import app.revanced.util.getReference import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction import app.revanced.util.indexOfFirstInstruction
@ -71,6 +72,14 @@ fun indexOfHasBeenVisitedInstruction(method: Method) =
reference.returnType == "Z" reference.returnType == "Z"
} }
internal val nsfwAlertBuilderFingerprint = legacyFingerprint(
name = "nsfwAlertBuilderFingerprint",
literals = listOf(nsfwDialogTitle),
customFingerprint = { method, _ ->
method.definingClass.startsWith("Lcom/reddit/screen/nsfw")
}
)
internal val redditAlertDialogsFingerprint = legacyFingerprint( internal val redditAlertDialogsFingerprint = legacyFingerprint(
name = "redditAlertDialogsFingerprint", name = "redditAlertDialogsFingerprint",
returnType = "V", returnType = "V",

Some files were not shown because too many files have changed in this diff Show More