mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-25 10:24:08 +01:00
Compare commits
99 Commits
v5.33.0-de
...
v5.35.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a8b618c4e | ||
|
|
c66c42e946 | ||
|
|
b340769cf3 | ||
|
|
0a8cd7a7db | ||
|
|
39f90e4b11 | ||
|
|
9256aa4548 | ||
|
|
7973c75552 | ||
|
|
2b2307416a | ||
|
|
1dbc2d4057 | ||
|
|
f6917dc361 | ||
|
|
d2f043e11a | ||
|
|
a392bc0dfd | ||
|
|
dfc127048a | ||
|
|
ed31d0cab6 | ||
|
|
0df6315f9c | ||
|
|
f14259f9ef | ||
|
|
1473db0bef | ||
|
|
829ca58a55 | ||
|
|
aace741e25 | ||
|
|
189529151a | ||
|
|
51237c177a | ||
|
|
23496c7c36 | ||
|
|
e6823d8924 | ||
|
|
43597dab21 | ||
|
|
c0824db142 | ||
|
|
1b7f84b7fa | ||
|
|
6d87c848d6 | ||
|
|
150bee2833 | ||
|
|
c3ee6eca44 | ||
|
|
01a04c338c | ||
|
|
3130225d9d | ||
|
|
16b27fb872 | ||
|
|
bedabd3fa3 | ||
|
|
84f3c6f02d | ||
|
|
25470baeee | ||
|
|
b86da73a87 | ||
|
|
4aaa7ca895 | ||
|
|
d3f63461e7 | ||
|
|
7a3ace2231 | ||
|
|
c89668a540 | ||
|
|
40ac8e1142 | ||
|
|
26c6420de5 | ||
|
|
bfd3989995 | ||
|
|
7e812ae1a8 | ||
|
|
c23a926b07 | ||
|
|
fe66baedb7 | ||
|
|
959f23d1e4 | ||
|
|
56fbd8cce0 | ||
|
|
1bb8c53ed3 | ||
|
|
5fc0631a15 | ||
|
|
bdbe96beba | ||
|
|
6bd9e49c7a | ||
|
|
f904ca6d7e | ||
|
|
e579c56921 | ||
|
|
83f239065a | ||
|
|
6499318f33 | ||
|
|
809e013c4e | ||
|
|
182829d51c | ||
|
|
61824ade23 | ||
|
|
ff4308e961 | ||
|
|
b5eb13c0a8 | ||
|
|
b702dceda0 | ||
|
|
d616652058 | ||
|
|
c3e571e765 | ||
|
|
30176a3318 | ||
|
|
9c0638d128 | ||
|
|
d7eb6e87a5 | ||
|
|
562e005772 | ||
|
|
f61218de52 | ||
|
|
a19b670e19 | ||
|
|
300d816350 | ||
|
|
63d64a5c87 | ||
|
|
0cfc31c8f7 | ||
|
|
a28891e5f3 | ||
|
|
36036b082d | ||
|
|
1bc63e50a7 | ||
|
|
4b2b5e3029 | ||
|
|
9afa7d2ac6 | ||
|
|
1a8146dbc8 | ||
|
|
178eed7fcd | ||
|
|
621292644c | ||
|
|
1dd01cf54a | ||
|
|
8c31374c53 | ||
|
|
2e177a8839 | ||
|
|
cfffd422f8 | ||
|
|
37aab8382e | ||
|
|
f4950ec2ea | ||
|
|
7bdc32867a | ||
|
|
6e60ac6963 | ||
|
|
1adbd563b2 | ||
|
|
9ccf13b680 | ||
|
|
7b8ca9c018 | ||
|
|
ae6dd23d08 | ||
|
|
b1d164b446 | ||
|
|
87c39dd485 | ||
|
|
1549ac12aa | ||
|
|
5d08fdddb8 | ||
|
|
98114e5bde | ||
|
|
a4817dfdd0 |
308
CHANGELOG.md
308
CHANGELOG.md
@@ -1,3 +1,311 @@
|
||||
# [5.35.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.35.0-dev.3...v5.35.0-dev.4) (2025-09-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Boost/Sync for Reddit:** Add `Fix Redgifs` patch ([#5725](https://github.com/ReVanced/revanced-patches/issues/5725)) ([c5e8079](https://github.com/ReVanced/revanced-patches/commit/c5e8079eab08075a72078cd0fa79f3beb1f75d98))
|
||||
|
||||
# [5.35.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.35.0-dev.2...v5.35.0-dev.3) (2025-09-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Instagram - Hide navigation buttons:** Fix Manager patching error ([0d10e94](https://github.com/ReVanced/revanced-patches/commit/0d10e94663283fac09f3efc57c9b9805c38c4e13))
|
||||
|
||||
# [5.35.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.35.0-dev.1...v5.35.0-dev.2) (2025-09-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Revert dependency updates to fix Manager pre-release patching ([4c7a1a8](https://github.com/ReVanced/revanced-patches/commit/4c7a1a8554c67797bf663e5230f566c5a9b229af))
|
||||
|
||||
# [5.35.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.34.1-dev.3...v5.35.0-dev.1) (2025-09-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Instagram:** Add `Hide navigation buttons` patch ([#5678](https://github.com/ReVanced/revanced-patches/issues/5678)) ([415cf0f](https://github.com/ReVanced/revanced-patches/commit/415cf0fb5b9b3dcaf4592943a69eea1c10447b07))
|
||||
|
||||
## [5.34.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.34.1-dev.2...v5.34.1-dev.3) (2025-08-24)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide layout components:** Hide Playable shelf header ([fbb5046](https://github.com/ReVanced/revanced-patches/commit/fbb50463f0e3f533a278c5251cfbce59f09ce641))
|
||||
|
||||
## [5.34.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.34.1-dev.1...v5.34.1-dev.2) (2025-08-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Proton mail:** Constrain patches to last working app target ([21c34b9](https://github.com/ReVanced/revanced-patches/commit/21c34b908e07a97de8c31c7c828b44a8cc4739b6))
|
||||
|
||||
## [5.34.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.34.0...v5.34.1-dev.1) (2025-08-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Unlock Premium:** Make compatible with latest versions again by fixing fingerprint ([#5684](https://github.com/ReVanced/revanced-patches/issues/5684)) ([30dcff1](https://github.com/ReVanced/revanced-patches/commit/30dcff13a56883efc499b71faadb403877cd1c67))
|
||||
|
||||
# [5.34.0](https://github.com/ReVanced/revanced-patches/compare/v5.33.0...v5.34.0) (2025-08-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Backdrops:** Remove broken patch that is no longer supported ([#5627](https://github.com/ReVanced/revanced-patches/issues/5627)) ([ebb8332](https://github.com/ReVanced/revanced-patches/commit/ebb83320838aa99dd4417d45a50333dd42c1218a))
|
||||
* **pixiv - Hide ads:** Constrain patch to last working app target ([d8ea56c](https://github.com/ReVanced/revanced-patches/commit/d8ea56ca4be47df1c43f96ec41b91c800f1d9daf))
|
||||
* **Twitch:** Constrain patches to last working app targets ([#5373](https://github.com/ReVanced/revanced-patches/issues/5373)) ([29a4748](https://github.com/ReVanced/revanced-patches/commit/29a47481c4efa209a3a53df60613b59a73adbe07))
|
||||
* **YouTube - Hide layout components:** Do not hide community posts on channel profiles ([#5634](https://github.com/ReVanced/revanced-patches/issues/5634)) ([9e3d5a2](https://github.com/ReVanced/revanced-patches/commit/9e3d5a2b36106479470f3f69920518b57e8c4dca))
|
||||
* **YouTube - Player Controls:** Fix chapter title overlapping the bottom buttons ([#5673](https://github.com/ReVanced/revanced-patches/issues/5673)) ([09ccee7](https://github.com/ReVanced/revanced-patches/commit/09ccee71384df338bbf8acc1097f619a372c4868))
|
||||
* **YouTube - SponsorBlock:** Do not hide voting or create button when the video ends ([6aba4e2](https://github.com/ReVanced/revanced-patches/commit/6aba4e284de9bb94b49eea8be2baf2870eecbbcf))
|
||||
* **YouTube - Video playback:** Disable HDR video does not disable Dolby Vision HDR ([#5661](https://github.com/ReVanced/revanced-patches/issues/5661)) ([6dab988](https://github.com/ReVanced/revanced-patches/commit/6dab98810645b96bd0387ba7d607e5d8ffb1b5bb))
|
||||
* **YouTube - Video quality:** Fix additional incorrect quality resolutions used by YouTube ([a2a1fbe](https://github.com/ReVanced/revanced-patches/commit/a2a1fbe2959be8334c54cfc3426c24a960c55c8f))
|
||||
* **YouTube - Video quality:** Show FHD+ icon for 1080p 60fps enhanced bitrate ([76bed37](https://github.com/ReVanced/revanced-patches/commit/76bed3734093713af24ef065d5ffc5b1cd83f29a))
|
||||
* **YouTube:** Use correct fade out animation when tapping to dismiss the video overlay ([#5670](https://github.com/ReVanced/revanced-patches/issues/5670)) ([cce6737](https://github.com/ReVanced/revanced-patches/commit/cce6737f627fc7621bbde50a5653b6af14c6f31a))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Instagram:** Support latest app version ([#5611](https://github.com/ReVanced/revanced-patches/issues/5611)) ([26fe690](https://github.com/ReVanced/revanced-patches/commit/26fe690dfbefe6c412c5f81f208a3b1d2fbd7a0a))
|
||||
* **NU.nl:** Support latest app version ([#5643](https://github.com/ReVanced/revanced-patches/issues/5643)) ([7338e4a](https://github.com/ReVanced/revanced-patches/commit/7338e4a5a99f913256120d0d58fede3aa4ee8922))
|
||||
* **YouTube - Hide player flyout menu items:** Add option to hide quality flyout menu ([eb55068](https://github.com/ReVanced/revanced-patches/commit/eb5506856a2eaf2a8585e598868ddba3e1429159))
|
||||
* **YouTube - Hide video action buttons:** Add "Hide Hype button" setting ([f13f377](https://github.com/ReVanced/revanced-patches/commit/f13f3770e7c4fd5bff8f3e224fb1b1ead50a3c18))
|
||||
* **YouTube - Hide video action buttons:** Add "Hide Promote button" setting ([1959396](https://github.com/ReVanced/revanced-patches/commit/1959396a53f4c07b94acddc5c0ee6cdf7ade7c7b))
|
||||
* **YouTube - Playback speed:** Show current playback speed on player speed dialog button ([#5607](https://github.com/ReVanced/revanced-patches/issues/5607)) ([279436a](https://github.com/ReVanced/revanced-patches/commit/279436a3657b50f98bb4cc64dc88dc14e422f204))
|
||||
* **YouTube:** Add `Disable sign in to TV popup` patch ([#5639](https://github.com/ReVanced/revanced-patches/issues/5639)) ([d0e5bd0](https://github.com/ReVanced/revanced-patches/commit/d0e5bd0479a8910b081c483ed2a6ab4d7134e3c3))
|
||||
|
||||
# [5.34.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.12...v5.34.0-dev.13) (2025-08-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Player Controls:** Fix chapter title overlapping the bottom buttons ([#5673](https://github.com/ReVanced/revanced-patches/issues/5673)) ([09ccee7](https://github.com/ReVanced/revanced-patches/commit/09ccee71384df338bbf8acc1097f619a372c4868))
|
||||
|
||||
# [5.34.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.12...v5.34.0-dev.13) (2025-08-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Player Controls:** Fix chapter title overlapping the bottom buttons ([#5673](https://github.com/ReVanced/revanced-patches/issues/5673)) ([09ccee7](https://github.com/ReVanced/revanced-patches/commit/09ccee71384df338bbf8acc1097f619a372c4868))
|
||||
|
||||
# [5.34.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.11...v5.34.0-dev.12) (2025-08-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube:** Use correct fade out animation when tapping to dismiss the video overlay ([#5670](https://github.com/ReVanced/revanced-patches/issues/5670)) ([cce6737](https://github.com/ReVanced/revanced-patches/commit/cce6737f627fc7621bbde50a5653b6af14c6f31a))
|
||||
|
||||
# [5.34.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.10...v5.34.0-dev.11) (2025-08-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - SponsorBlock:** Do not hide voting or create button when the video ends ([6aba4e2](https://github.com/ReVanced/revanced-patches/commit/6aba4e284de9bb94b49eea8be2baf2870eecbbcf))
|
||||
|
||||
# [5.34.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.9...v5.34.0-dev.10) (2025-08-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Video playback:** Disable HDR video does not disable Dolby Vision HDR ([#5661](https://github.com/ReVanced/revanced-patches/issues/5661)) ([6dab988](https://github.com/ReVanced/revanced-patches/commit/6dab98810645b96bd0387ba7d607e5d8ffb1b5bb))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide video action buttons:** Add "Hide Promote button" setting ([1959396](https://github.com/ReVanced/revanced-patches/commit/1959396a53f4c07b94acddc5c0ee6cdf7ade7c7b))
|
||||
|
||||
# [5.34.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.9...v5.34.0-dev.10) (2025-08-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide video action buttons:** Add "Hide Promote button" setting ([1959396](https://github.com/ReVanced/revanced-patches/commit/1959396a53f4c07b94acddc5c0ee6cdf7ade7c7b))
|
||||
|
||||
# [5.34.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.9...v5.34.0-dev.10) (2025-08-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide video action buttons:** Add "Hide Promote button" setting ([1959396](https://github.com/ReVanced/revanced-patches/commit/1959396a53f4c07b94acddc5c0ee6cdf7ade7c7b))
|
||||
|
||||
# [5.34.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.8...v5.34.0-dev.9) (2025-08-16)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide video action buttons:** Add "Hide Hype button" setting ([f13f377](https://github.com/ReVanced/revanced-patches/commit/f13f3770e7c4fd5bff8f3e224fb1b1ead50a3c18))
|
||||
|
||||
# [5.34.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.7...v5.34.0-dev.8) (2025-08-15)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **NU.nl:** Support latest app version ([#5643](https://github.com/ReVanced/revanced-patches/issues/5643)) ([7338e4a](https://github.com/ReVanced/revanced-patches/commit/7338e4a5a99f913256120d0d58fede3aa4ee8922))
|
||||
* **YouTube:** Add `Disable sign in to TV popup` patch ([#5639](https://github.com/ReVanced/revanced-patches/issues/5639)) ([d0e5bd0](https://github.com/ReVanced/revanced-patches/commit/d0e5bd0479a8910b081c483ed2a6ab4d7134e3c3))
|
||||
|
||||
# [5.34.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.6...v5.34.0-dev.7) (2025-08-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Video quality:** Fix additional incorrect quality resolutions used by YouTube ([a2a1fbe](https://github.com/ReVanced/revanced-patches/commit/a2a1fbe2959be8334c54cfc3426c24a960c55c8f))
|
||||
|
||||
# [5.34.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.5...v5.34.0-dev.6) (2025-08-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Video quality:** Show FHD+ icon for 1080p 60fps enhanced bitrate ([76bed37](https://github.com/ReVanced/revanced-patches/commit/76bed3734093713af24ef065d5ffc5b1cd83f29a))
|
||||
|
||||
# [5.34.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.4...v5.34.0-dev.5) (2025-08-10)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide player flyout menu items:** Add option to hide quality flyout menu ([eb55068](https://github.com/ReVanced/revanced-patches/commit/eb5506856a2eaf2a8585e598868ddba3e1429159))
|
||||
|
||||
# [5.34.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.3...v5.34.0-dev.4) (2025-08-10)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide layout components:** Do not hide community posts on channel profiles ([#5634](https://github.com/ReVanced/revanced-patches/issues/5634)) ([9e3d5a2](https://github.com/ReVanced/revanced-patches/commit/9e3d5a2b36106479470f3f69920518b57e8c4dca))
|
||||
|
||||
# [5.34.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.2...v5.34.0-dev.3) (2025-08-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **pixiv - Hide ads:** Constrain patch to last working app target ([d8ea56c](https://github.com/ReVanced/revanced-patches/commit/d8ea56ca4be47df1c43f96ec41b91c800f1d9daf))
|
||||
|
||||
# [5.34.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.1...v5.34.0-dev.2) (2025-08-09)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Backdrops:** Remove broken patch that is no longer supported ([#5627](https://github.com/ReVanced/revanced-patches/issues/5627)) ([ebb8332](https://github.com/ReVanced/revanced-patches/commit/ebb83320838aa99dd4417d45a50333dd42c1218a))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Playback speed:** Show current playback speed on player speed dialog button ([#5607](https://github.com/ReVanced/revanced-patches/issues/5607)) ([279436a](https://github.com/ReVanced/revanced-patches/commit/279436a3657b50f98bb4cc64dc88dc14e422f204))
|
||||
|
||||
# [5.34.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.33.0...v5.34.0-dev.1) (2025-08-08)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Twitch:** Constrain patches to last working app targets ([#5373](https://github.com/ReVanced/revanced-patches/issues/5373)) ([29a4748](https://github.com/ReVanced/revanced-patches/commit/29a47481c4efa209a3a53df60613b59a73adbe07))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Instagram:** Support latest app version ([#5611](https://github.com/ReVanced/revanced-patches/issues/5611)) ([26fe690](https://github.com/ReVanced/revanced-patches/commit/26fe690dfbefe6c412c5f81f208a3b1d2fbd7a0a))
|
||||
|
||||
# [5.33.0](https://github.com/ReVanced/revanced-patches/compare/v5.32.0...v5.33.0) (2025-08-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Messenger - Hide Facebook button:** Support the latest app version ([#5590](https://github.com/ReVanced/revanced-patches/issues/5590)) ([0cab98d](https://github.com/ReVanced/revanced-patches/commit/0cab98df1689dbf7a042f18f4a961d47da1430ad))
|
||||
* **NFC Tools:** Remove broken patch that is no longer supported ([#5584](https://github.com/ReVanced/revanced-patches/issues/5584)) ([cd3a6be](https://github.com/ReVanced/revanced-patches/commit/cd3a6be75c6bd3cc33c0b17a044bd6147f27b5ce))
|
||||
* **YouTube - Force original audio:** Disable a/b feature flag that forces localized audio ([#5582](https://github.com/ReVanced/revanced-patches/issues/5582)) ([9fe13ee](https://github.com/ReVanced/revanced-patches/commit/9fe13ee1af104c009efd19b826adef375e48e191))
|
||||
* **YouTube - Litho filter:** Correctly filter identifier of older YouTube targets ([bf29d69](https://github.com/ReVanced/revanced-patches/commit/bf29d6909e389819bad878ad3b94bbc90d823cc9))
|
||||
* **YouTube - Playback speed:** Use old speed menu for player button if enabled ([1e8f436](https://github.com/ReVanced/revanced-patches/commit/1e8f4368e117f4b278c24709231cb32546e46dc0))
|
||||
* **YouTube - Video quality:** Fix 144p default not always used ([2f7483a](https://github.com/ReVanced/revanced-patches/commit/2f7483a2d789c28a243b58bb7a252c0d590858ee))
|
||||
* **YouTube - Video quality:** Fix dialog quality list check mark not always shown ([295f0f2](https://github.com/ReVanced/revanced-patches/commit/295f0f216b5e8aa9d68457862e73e312b7342703))
|
||||
* **YouTube - Video quality:** Fix wrong qualities sometimes shown in player button dialog ([7378ae3](https://github.com/ReVanced/revanced-patches/commit/7378ae3c5fc88f91bf5cd6db47c6cd170a8c5a4f))
|
||||
* **YouTube - Video quality:** Use 1080p enhanced bitrate for Premium users ([#5565](https://github.com/ReVanced/revanced-patches/issues/5565)) ([bd3ace0](https://github.com/ReVanced/revanced-patches/commit/bd3ace0bd04ccd0369adb49d63aa0cf986402346))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **ORF ON:** Add `Remove root detection` patch ([#5551](https://github.com/ReVanced/revanced-patches/issues/5551)) ([6c6aa35](https://github.com/ReVanced/revanced-patches/commit/6c6aa35411a139dddc3a15dd757fbeded5d1a0a3))
|
||||
* **YouTube - Playback speed:** Add "Restore old playback speed menu" option ([#5552](https://github.com/ReVanced/revanced-patches/issues/5552)) ([b01f15b](https://github.com/ReVanced/revanced-patches/commit/b01f15b9acb0427aed99b0141ae271831b7936bf))
|
||||
* **YouTube:** Add player button to change video quality ([#5435](https://github.com/ReVanced/revanced-patches/issues/5435)) ([d5f51bf](https://github.com/ReVanced/revanced-patches/commit/d5f51bf400dd22626ff65d7563b6fde70d53fb25))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **YouTube:** Filter identifier callback only on root component creation ([#5558](https://github.com/ReVanced/revanced-patches/issues/5558)) ([ccac46e](https://github.com/ReVanced/revanced-patches/commit/ccac46eebc2e14b094454e37ef4461d48a62c53f))
|
||||
|
||||
# [5.33.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.12...v5.33.0-dev.13) (2025-08-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Messenger - Hide Facebook button:** Support the latest app version ([#5590](https://github.com/ReVanced/revanced-patches/issues/5590)) ([0cab98d](https://github.com/ReVanced/revanced-patches/commit/0cab98df1689dbf7a042f18f4a961d47da1430ad))
|
||||
|
||||
# [5.33.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.11...v5.33.0-dev.12) (2025-08-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Video quality:** Fix dialog quality list check mark not always shown ([295f0f2](https://github.com/ReVanced/revanced-patches/commit/295f0f216b5e8aa9d68457862e73e312b7342703))
|
||||
|
||||
# [5.33.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.10...v5.33.0-dev.11) (2025-08-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Video quality:** Fix 144p default not always used ([2f7483a](https://github.com/ReVanced/revanced-patches/commit/2f7483a2d789c28a243b58bb7a252c0d590858ee))
|
||||
|
||||
# [5.33.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.9...v5.33.0-dev.10) (2025-08-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Video quality:** Fix wrong qualities sometimes shown in player button dialog ([7378ae3](https://github.com/ReVanced/revanced-patches/commit/7378ae3c5fc88f91bf5cd6db47c6cd170a8c5a4f))
|
||||
|
||||
# [5.33.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.8...v5.33.0-dev.9) (2025-08-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Force original audio:** Disable a/b feature flag that forces localized audio ([#5582](https://github.com/ReVanced/revanced-patches/issues/5582)) ([9fe13ee](https://github.com/ReVanced/revanced-patches/commit/9fe13ee1af104c009efd19b826adef375e48e191))
|
||||
|
||||
# [5.33.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.7...v5.33.0-dev.8) (2025-08-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **NFC Tools:** Remove broken patch that is no longer supported ([#5584](https://github.com/ReVanced/revanced-patches/issues/5584)) ([cd3a6be](https://github.com/ReVanced/revanced-patches/commit/cd3a6be75c6bd3cc33c0b17a044bd6147f27b5ce))
|
||||
|
||||
# [5.33.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.6...v5.33.0-dev.7) (2025-08-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube:** Add player button to change video quality ([#5435](https://github.com/ReVanced/revanced-patches/issues/5435)) ([d5f51bf](https://github.com/ReVanced/revanced-patches/commit/d5f51bf400dd22626ff65d7563b6fde70d53fb25))
|
||||
|
||||
# [5.33.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.5...v5.33.0-dev.6) (2025-07-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Video quality:** Use 1080p enhanced bitrate for Premium users ([#5565](https://github.com/ReVanced/revanced-patches/issues/5565)) ([bd3ace0](https://github.com/ReVanced/revanced-patches/commit/bd3ace0bd04ccd0369adb49d63aa0cf986402346))
|
||||
|
||||
# [5.33.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.4...v5.33.0-dev.5) (2025-07-31)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Litho filter:** Correctly filter identifier of older YouTube targets ([bf29d69](https://github.com/ReVanced/revanced-patches/commit/bf29d6909e389819bad878ad3b94bbc90d823cc9))
|
||||
|
||||
# [5.33.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.3...v5.33.0-dev.4) (2025-07-30)
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **YouTube:** Filter identifier callback only on root component creation ([#5558](https://github.com/ReVanced/revanced-patches/issues/5558)) ([ccac46e](https://github.com/ReVanced/revanced-patches/commit/ccac46eebc2e14b094454e37ef4461d48a62c53f))
|
||||
|
||||
# [5.33.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.2...v5.33.0-dev.3) (2025-07-30)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Playback speed:** Use old speed menu for player button if enabled ([1e8f436](https://github.com/ReVanced/revanced-patches/commit/1e8f4368e117f4b278c24709231cb32546e46dc0))
|
||||
|
||||
# [5.33.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.1...v5.33.0-dev.2) (2025-07-29)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:boostforreddit:stub"))
|
||||
compileOnly(libs.annotation)
|
||||
compileOnly(libs.okhttp)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package app.revanced.extension.boostforreddit;
|
||||
|
||||
import app.revanced.extension.shared.fixes.redgifs.BaseFixRedgifsApiPatch;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
*/
|
||||
public class FixRedgifsApiPatch extends BaseFixRedgifsApiPatch {
|
||||
static {
|
||||
INSTANCE = new FixRedgifsApiPatch();
|
||||
}
|
||||
|
||||
public String getDefaultUserAgent() {
|
||||
// Boost uses a static user agent for Redgifs API calls
|
||||
return "Boost";
|
||||
}
|
||||
|
||||
public static OkHttpClient createClient() {
|
||||
return new OkHttpClient.Builder().addInterceptor(INSTANCE).build();
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
dependencies {
|
||||
implementation(project(":extensions:shared:library"))
|
||||
compileOnly(libs.okhttp)
|
||||
}
|
||||
|
||||
@@ -18,4 +18,5 @@ android {
|
||||
|
||||
dependencies {
|
||||
compileOnly(libs.annotation)
|
||||
compileOnly(libs.okhttp)
|
||||
}
|
||||
|
||||
@@ -329,7 +329,7 @@ public class Utils {
|
||||
return (R) child;
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("View with resource name '" + str + "' not found");
|
||||
throw new IllegalArgumentException("View with resource name not found: " + str);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1460,6 +1460,16 @@ public class Utils {
|
||||
return (int) (metrics.widthPixels * (percentage / 100.0f));
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
|
||||
*/
|
||||
@ColorInt
|
||||
public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) {
|
||||
return isDarkModeEnabled()
|
||||
? adjustColorBrightness(baseColor, darkThemeFactor)
|
||||
: adjustColorBrightness(baseColor, lightThemeFactor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the brightness of a color by lightening or darkening it based on the given factor.
|
||||
* <p>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package app.revanced.extension.shared.fixes.redgifs;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.Protocol;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
|
||||
|
||||
public abstract class BaseFixRedgifsApiPatch implements Interceptor {
|
||||
protected static BaseFixRedgifsApiPatch INSTANCE;
|
||||
public abstract String getDefaultUserAgent();
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Response intercept(@NonNull Chain chain) throws IOException {
|
||||
Request request = chain.request();
|
||||
if (!request.url().host().equals("api.redgifs.com")) {
|
||||
return chain.proceed(request);
|
||||
}
|
||||
|
||||
String userAgent = getDefaultUserAgent();
|
||||
|
||||
if (request.header("Authorization") != null) {
|
||||
Response response = chain.proceed(request.newBuilder().header("User-Agent", userAgent).build());
|
||||
if (response.isSuccessful()) {
|
||||
return response;
|
||||
}
|
||||
// It's possible that the user agent is being overwritten later down in the interceptor
|
||||
// chain, so make sure we grab the new user agent from the request headers.
|
||||
userAgent = response.request().header("User-Agent");
|
||||
response.close();
|
||||
}
|
||||
|
||||
try {
|
||||
RedgifsTokenManager.RedgifsToken token = RedgifsTokenManager.refreshToken(userAgent);
|
||||
|
||||
// Emulate response for old OAuth endpoint
|
||||
if (request.url().encodedPath().equals("/v2/oauth/client")) {
|
||||
String responseBody = RedgifsTokenManager.getEmulatedOAuthResponseBody(token);
|
||||
return new Response.Builder()
|
||||
.message("OK")
|
||||
.code(HttpURLConnection.HTTP_OK)
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.request(request)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(ResponseBody.create(
|
||||
responseBody, MediaType.get("application/json")))
|
||||
.build();
|
||||
}
|
||||
|
||||
Request modifiedRequest = request.newBuilder()
|
||||
.header("Authorization", "Bearer " + token.getAccessToken())
|
||||
.header("User-Agent", userAgent)
|
||||
.build();
|
||||
return chain.proceed(modifiedRequest);
|
||||
} catch (JSONException ex) {
|
||||
Logger.printException(() -> "Could not parse Redgifs response", ex);
|
||||
throw new IOException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package app.revanced.extension.shared.fixes.redgifs;
|
||||
|
||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
|
||||
import androidx.annotation.GuardedBy;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
|
||||
|
||||
/**
|
||||
* Manages Redgifs token lifecycle.
|
||||
*/
|
||||
public class RedgifsTokenManager {
|
||||
public static class RedgifsToken {
|
||||
// Expire after 23 hours to provide some breathing room
|
||||
private static final long EXPIRY_SECONDS = 23 * 60 * 60;
|
||||
|
||||
private final String accessToken;
|
||||
private final long refreshTimeInSeconds;
|
||||
|
||||
public RedgifsToken(String accessToken, long refreshTime) {
|
||||
this.accessToken = accessToken;
|
||||
this.refreshTimeInSeconds = refreshTime;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
public long getExpiryTimeInSeconds() {
|
||||
return refreshTimeInSeconds + EXPIRY_SECONDS;
|
||||
}
|
||||
|
||||
public boolean isValid() {
|
||||
if (accessToken == null) return false;
|
||||
return getExpiryTimeInSeconds() >= System.currentTimeMillis() / 1000;
|
||||
}
|
||||
}
|
||||
public static final String REDGIFS_API_HOST = "https://api.redgifs.com";
|
||||
private static final String GET_TEMPORARY_TOKEN = REDGIFS_API_HOST + "/v2/auth/temporary";
|
||||
@GuardedBy("itself")
|
||||
private static final Map<String, RedgifsToken> tokenMap = new HashMap<>();
|
||||
|
||||
private static String getToken(String userAgent) throws IOException, JSONException {
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(GET_TEMPORARY_TOKEN).openConnection();
|
||||
connection.setFixedLengthStreamingMode(0);
|
||||
connection.setRequestMethod(GET.name());
|
||||
connection.setRequestProperty("User-Agent", userAgent);
|
||||
connection.setRequestProperty("Content-Type", "application/json");
|
||||
connection.setRequestProperty("Accept", "application/json");
|
||||
connection.setUseCaches(false);
|
||||
|
||||
JSONObject responseObject = Requester.parseJSONObject(connection);
|
||||
return responseObject.getString("token");
|
||||
}
|
||||
|
||||
public static RedgifsToken refreshToken(String userAgent) throws IOException, JSONException {
|
||||
synchronized(tokenMap) {
|
||||
// Reference: https://github.com/JeffreyCA/Apollo-ImprovedCustomApi/pull/67
|
||||
RedgifsToken token = tokenMap.get(userAgent);
|
||||
if (token != null && token.isValid()) {
|
||||
return token;
|
||||
}
|
||||
|
||||
// Copy user agent from original request if present because Redgifs verifies
|
||||
// that the user agent in subsequent requests matches the one in the OAuth token.
|
||||
String accessToken = getToken(userAgent);
|
||||
long refreshTime = System.currentTimeMillis() / 1000;
|
||||
token = new RedgifsToken(accessToken, refreshTime);
|
||||
tokenMap.put(userAgent, token);
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getEmulatedOAuthResponseBody(RedgifsToken token) throws JSONException {
|
||||
// Reference: https://github.com/JeffreyCA/Apollo-ImprovedCustomApi/pull/67
|
||||
JSONObject responseObject = new JSONObject();
|
||||
responseObject.put("access_token", token.accessToken);
|
||||
responseObject.put("expiry_time", token.getExpiryTimeInSeconds() - (System.currentTimeMillis() / 1000));
|
||||
responseObject.put("scope", "read");
|
||||
responseObject.put("token_type", "Bearer");
|
||||
return responseObject.toString();
|
||||
}
|
||||
}
|
||||
@@ -2,4 +2,5 @@ dependencies {
|
||||
compileOnly(project(":extensions:shared:library"))
|
||||
compileOnly(project(":extensions:syncforreddit:stub"))
|
||||
compileOnly(libs.annotation)
|
||||
compileOnly(libs.okhttp)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package app.revanced.extension.syncforreddit;
|
||||
|
||||
import app.revanced.extension.shared.fixes.redgifs.BaseFixRedgifsApiPatch;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
/**
|
||||
* @noinspection unused
|
||||
*/
|
||||
public class FixRedgifsApiPatch extends BaseFixRedgifsApiPatch {
|
||||
static {
|
||||
INSTANCE = new FixRedgifsApiPatch();
|
||||
}
|
||||
|
||||
public String getDefaultUserAgent() {
|
||||
// To be filled in by patch
|
||||
return "";
|
||||
}
|
||||
|
||||
public static OkHttpClient install(OkHttpClient.Builder builder) {
|
||||
return builder.addInterceptor(INSTANCE).build();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
package app.revanced.extension.youtube
|
||||
|
||||
import app.revanced.extension.shared.Logger
|
||||
import java.util.Collections
|
||||
|
||||
/**
|
||||
* generic event provider class
|
||||
*/
|
||||
class Event<T> {
|
||||
private val eventListeners = mutableSetOf<(T) -> Unit>()
|
||||
private val eventListeners = Collections.synchronizedSet(mutableSetOf<(T) -> Unit>())
|
||||
|
||||
operator fun plusAssign(observer: (T) -> Unit) {
|
||||
addObserver(observer)
|
||||
}
|
||||
|
||||
fun addObserver(observer: (T) -> Unit) {
|
||||
Logger.printDebug { "Adding observer: $observer" }
|
||||
eventListeners.add(observer)
|
||||
}
|
||||
|
||||
@@ -23,7 +27,8 @@ class Event<T> {
|
||||
}
|
||||
|
||||
operator fun invoke(value: T) {
|
||||
for (observer in eventListeners)
|
||||
for (observer in eventListeners) {
|
||||
observer.invoke(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import android.view.Display;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -8,8 +10,10 @@ public class DisableHdrPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableHDRVideo() {
|
||||
return !Settings.DISABLE_HDR_VIDEO.get();
|
||||
public static int[] disableHdrVideo(Display.HdrCapabilities capabilities) {
|
||||
return Settings.DISABLE_HDR_VIDEO.get()
|
||||
? new int[0]
|
||||
: capabilities.getSupportedHdrTypes();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DisableSignInToTvPopupPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableSignInToTvPopup() {
|
||||
return Settings.DISABLE_SIGNIN_TO_TV_POPUP.get();
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,16 @@ public class ForceOriginalAudioPatch {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean ignoreDefaultAudioStream(boolean original) {
|
||||
if (Settings.FORCE_ORIGINAL_AUDIO.get()) {
|
||||
return false;
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@@ -50,7 +60,6 @@ public class ForceOriginalAudioPatch {
|
||||
return isOriginal;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "isDefaultAudioStream failure", ex);
|
||||
|
||||
return isDefault;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,11 +57,4 @@ public class PlayerControlsPatch {
|
||||
private static void fullscreenButtonVisibilityChanged(boolean isVisible) {
|
||||
// Code added during patching.
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static String getPlayerTopControlsLayoutResourceName(String original) {
|
||||
return "default";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.shared.PlayerControlsVisibility;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PlayerControlsVisibilityHookPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setPlayerControlsVisibility(@Nullable Enum<?> youTubePlayerControlsVisibility) {
|
||||
if (youTubePlayerControlsVisibility == null) return;
|
||||
|
||||
PlayerControlsVisibility.setFromString(youTubePlayerControlsVisibility.name());
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.Event;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
import app.revanced.extension.youtube.shared.VideoState;
|
||||
|
||||
/**
|
||||
@@ -16,11 +22,30 @@ import app.revanced.extension.youtube.shared.VideoState;
|
||||
public final class VideoInformation {
|
||||
|
||||
public interface PlaybackController {
|
||||
// Methods are added to YT classes during patching.
|
||||
boolean seekTo(long videoTime);
|
||||
void seekToRelative(long videoTimeOffset);
|
||||
// Methods are added during patching.
|
||||
boolean patch_seekTo(long videoTime);
|
||||
void patch_seekToRelative(long videoTimeOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface to use obfuscated methods.
|
||||
*/
|
||||
public interface VideoQualityMenuInterface {
|
||||
// Method is added during patching.
|
||||
void patch_setQuality(VideoQuality quality);
|
||||
}
|
||||
|
||||
/**
|
||||
* Video resolution of the automatic quality option..
|
||||
*/
|
||||
public static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
|
||||
|
||||
/**
|
||||
* Video quality names are the same text for all languages.
|
||||
* Premium can be "1080p Premium" or "1080p60 Premium"
|
||||
*/
|
||||
public static final String VIDEO_QUALITY_PREMIUM_NAME = "Premium";
|
||||
|
||||
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
|
||||
/**
|
||||
* Prefix present in all Short player parameters signature.
|
||||
@@ -30,12 +55,10 @@ public final class VideoInformation {
|
||||
private static WeakReference<PlaybackController> playerControllerRef = new WeakReference<>(null);
|
||||
private static WeakReference<PlaybackController> mdxPlayerDirectorRef = new WeakReference<>(null);
|
||||
|
||||
@NonNull
|
||||
private static String videoId = "";
|
||||
private static long videoLength = 0;
|
||||
private static long videoTime = -1;
|
||||
|
||||
@NonNull
|
||||
private static volatile String playerResponseVideoId = "";
|
||||
private static volatile boolean playerResponseVideoIdIsShort;
|
||||
private static volatile boolean videoIdIsShort;
|
||||
@@ -45,6 +68,44 @@ public final class VideoInformation {
|
||||
*/
|
||||
private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
|
||||
|
||||
private static int desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE;
|
||||
|
||||
private static boolean qualityNeedsUpdating;
|
||||
|
||||
/**
|
||||
* The available qualities of the current video.
|
||||
*/
|
||||
@Nullable
|
||||
private static VideoQuality[] currentQualities;
|
||||
|
||||
/**
|
||||
* The current quality of the video playing.
|
||||
* This is always the actual quality even if Automatic quality is active.
|
||||
*/
|
||||
@Nullable
|
||||
private static VideoQuality currentQuality;
|
||||
|
||||
/**
|
||||
* The current VideoQualityMenuInterface, set during setVideoQuality.
|
||||
*/
|
||||
@Nullable
|
||||
private static VideoQualityMenuInterface currentMenuInterface;
|
||||
|
||||
/**
|
||||
* Callback for when the current quality changes.
|
||||
*/
|
||||
public static final Event<VideoQuality> onQualityChange = new Event<>();
|
||||
|
||||
@Nullable
|
||||
public static VideoQuality[] getCurrentQualities() {
|
||||
return currentQualities;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static VideoQuality getCurrentQuality() {
|
||||
return currentQuality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
@@ -52,12 +113,18 @@ public final class VideoInformation {
|
||||
*/
|
||||
public static void initialize(@NonNull PlaybackController playerController) {
|
||||
try {
|
||||
Logger.printDebug(() -> "newVideoStarted");
|
||||
|
||||
playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController));
|
||||
videoTime = -1;
|
||||
videoLength = 0;
|
||||
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
|
||||
desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE;
|
||||
currentQualities = null;
|
||||
currentMenuInterface = null;
|
||||
setCurrentQuality(null);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to initialize", ex);
|
||||
Logger.printException(() -> "initialize failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,14 +264,14 @@ public final class VideoInformation {
|
||||
if (controller == null) {
|
||||
Logger.printDebug(() -> "Cannot seekTo because player controller is null");
|
||||
} else {
|
||||
if (controller.seekTo(adjustedSeekTime)) return true;
|
||||
if (controller.patch_seekTo(adjustedSeekTime)) return true;
|
||||
Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD.");
|
||||
// Else the video is loading or changing videos, or video is casting to a different device.
|
||||
}
|
||||
|
||||
// Try calling the seekTo method of the MDX player director (called when casting).
|
||||
// The difference has to be a different second mark in order to avoid infinite skip loops
|
||||
// as the Lounge API only supports seconds.
|
||||
// as the Lounge API only supports whole seconds.
|
||||
if (adjustedSeekTime / 1000 == videoTime / 1000) {
|
||||
Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small "
|
||||
+ "(" + (adjustedSeekTime - videoTime) + "ms)");
|
||||
@@ -217,9 +284,9 @@ public final class VideoInformation {
|
||||
return false;
|
||||
}
|
||||
|
||||
return controller.seekTo(adjustedSeekTime);
|
||||
return controller.patch_seekTo(adjustedSeekTime);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to seek", ex);
|
||||
Logger.printException(() -> "seekTo failure", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -239,7 +306,7 @@ public final class VideoInformation {
|
||||
if (controller == null) {
|
||||
Logger.printDebug(() -> "Cannot seek relative as player controller is null");
|
||||
} else {
|
||||
controller.seekToRelative(seekTime);
|
||||
controller.patch_seekToRelative(seekTime);
|
||||
}
|
||||
|
||||
// Adjust the fine adjustment function so it's at least 1 second before/after.
|
||||
@@ -255,10 +322,10 @@ public final class VideoInformation {
|
||||
if (controller == null) {
|
||||
Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null");
|
||||
} else {
|
||||
controller.seekToRelative(adjustedSeekTime);
|
||||
controller.patch_seekToRelative(adjustedSeekTime);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to seek relative", ex);
|
||||
Logger.printException(() -> "seekToRelative failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,14 +406,13 @@ public final class VideoInformation {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return If the playback is at the end of the video.
|
||||
* <p>
|
||||
* If video is playing in the background with no video visible,
|
||||
* this always returns false (even if the video is actually at the end).
|
||||
* <p>
|
||||
* This is equivalent to checking for {@link VideoState#ENDED},
|
||||
* but can give a more up-to-date result for code calling from some hooks.
|
||||
*
|
||||
* @return If the playback is at the end of the video.
|
||||
* @see VideoState
|
||||
*/
|
||||
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
|
||||
@@ -373,4 +439,134 @@ public final class VideoInformation {
|
||||
playbackSpeed = newlyLoadedPlaybackSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param resolution The desired video quality resolution to use.
|
||||
*/
|
||||
public static void setDesiredVideoResolution(int resolution) {
|
||||
Utils.verifyOnMainThread();
|
||||
Logger.printDebug(() -> "Setting desired video resolution: " + resolution);
|
||||
desiredVideoResolution = resolution;
|
||||
qualityNeedsUpdating = true;
|
||||
}
|
||||
|
||||
private static void setCurrentQuality(@Nullable VideoQuality quality) {
|
||||
Utils.verifyOnMainThread();
|
||||
if (currentQuality != quality) {
|
||||
Logger.printDebug(() -> "Current quality changed to: " + quality);
|
||||
currentQuality = quality;
|
||||
onQualityChange.invoke(quality);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forcefully changes the video quality of the currently playing video.
|
||||
*/
|
||||
public static void changeQuality(VideoQuality quality) {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
if (currentMenuInterface == null) {
|
||||
Logger.printException(() -> "Cannot change quality, menu interface is null");
|
||||
return;
|
||||
}
|
||||
currentMenuInterface.patch_setQuality(quality);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Fixes bad data used by YouTube.
|
||||
* Issue can be reproduced by selecting 480p quality on any Short,
|
||||
* and occasionally with random regular videos.
|
||||
*/
|
||||
public static int fixVideoQualityResolution(String name, int quality) {
|
||||
try {
|
||||
if (!name.startsWith(Integer.toString(quality))) {
|
||||
final int suffixIndex = name.indexOf('p');
|
||||
if (suffixIndex > 0) {
|
||||
final int fixedQuality = Integer.parseInt(name.substring(0, suffixIndex));
|
||||
Logger.printDebug(() -> "Fixing wrong quality resolution from: " +
|
||||
name + "(" + quality + ") to: " + name + ")" + fixedQuality + ")");
|
||||
return fixedQuality;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "fixVideoQualityResolution failed", ex);
|
||||
}
|
||||
|
||||
return quality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
|
||||
* @param originalQualityIndex quality index to use, as chosen by YouTube
|
||||
*/
|
||||
public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) {
|
||||
try {
|
||||
Utils.verifyOnMainThread();
|
||||
currentMenuInterface = menu;
|
||||
|
||||
final boolean availableQualitiesChanged = (currentQualities == null)
|
||||
|| !Arrays.equals(currentQualities, qualities);
|
||||
if (availableQualitiesChanged) {
|
||||
currentQualities = qualities;
|
||||
Logger.printDebug(() -> "VideoQualities: " + Arrays.toString(currentQualities));
|
||||
}
|
||||
|
||||
VideoQuality updatedCurrentQuality = qualities[originalQualityIndex];
|
||||
if (updatedCurrentQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE
|
||||
&& (currentQuality == null || currentQuality != updatedCurrentQuality)) {
|
||||
setCurrentQuality(updatedCurrentQuality);
|
||||
}
|
||||
|
||||
final int preferredQuality = desiredVideoResolution;
|
||||
if (preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
|
||||
return originalQualityIndex; // Nothing to do.
|
||||
}
|
||||
|
||||
// After changing videos the qualities can initially be for the prior video.
|
||||
// If the qualities have changed and the default is not auto then an update is needed.
|
||||
if (qualityNeedsUpdating) {
|
||||
qualityNeedsUpdating = false;
|
||||
} else if (!availableQualitiesChanged) {
|
||||
return originalQualityIndex;
|
||||
}
|
||||
|
||||
// Find the highest quality that is equal to or less than the preferred.
|
||||
int i = 0;
|
||||
final int lastQualityIndex = qualities.length - 1;
|
||||
for (VideoQuality quality : qualities) {
|
||||
final int qualityResolution = quality.patch_getResolution();
|
||||
if ((qualityResolution != AUTOMATIC_VIDEO_QUALITY_VALUE && qualityResolution <= preferredQuality)
|
||||
// Use the lowest video quality if the default is lower than all available.
|
||||
|| i == lastQualityIndex) {
|
||||
final boolean qualityNeedsChange = (i != originalQualityIndex);
|
||||
Logger.printDebug(() -> qualityNeedsChange
|
||||
? "Changing video quality from: " + updatedCurrentQuality + " to: " + quality
|
||||
: "Video is already the preferred quality: " + quality
|
||||
);
|
||||
|
||||
// On first load of a new regular video, if the video is already the
|
||||
// desired quality then the quality flyout will show 'Auto' (ie: Auto (720p)).
|
||||
//
|
||||
// To prevent user confusion, set the video index even if the
|
||||
// quality is already correct so the UI picker will not display "Auto".
|
||||
//
|
||||
// Only change Shorts quality if the quality actually needs to change,
|
||||
// because the "auto" option is not shown in the flyout
|
||||
// and setting the same quality again can cause the Short to restart.
|
||||
if (qualityNeedsChange || !ShortsPlayerState.isOpen()) {
|
||||
changeQuality(quality);
|
||||
return i;
|
||||
}
|
||||
|
||||
return originalQualityIndex;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setVideoQuality failure", ex);
|
||||
}
|
||||
return originalQualityIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import android.app.Instrumentation;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
@@ -155,10 +153,10 @@ public final class AdsFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == playerShoppingShelf) {
|
||||
return contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered();
|
||||
return contentIndex == 0 && playerShoppingShelfBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
if (exceptions.matches(path)) {
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQualityMenuPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@@ -21,7 +19,7 @@ public final class AdvancedVideoQualityMenuFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
isVideoQualityMenuVisible = true;
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -85,6 +83,14 @@ final class ButtonsFilter extends Filter {
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_CLIP_BUTTON,
|
||||
"yt_outline_scissors"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_HYPE_BUTTON,
|
||||
"yt_outline_star_shooting"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PROMOTE_BUTTON,
|
||||
"yt_outline_megaphone"
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -100,7 +106,7 @@ final class ButtonsFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == likeSubscribeGlow) {
|
||||
return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
|
||||
@@ -117,7 +123,7 @@ final class ButtonsFilter extends Filter {
|
||||
// Make sure the current path is the right one
|
||||
// to avoid false positives.
|
||||
return path.startsWith(VIDEO_ACTION_BAR_PATH)
|
||||
&& bufferButtonsGroupList.check(protobufBufferArray).isFiltered();
|
||||
&& bufferButtonsGroupList.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@@ -87,12 +85,12 @@ final class CommentsFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == chipBar) {
|
||||
// Playlist sort button uses same components and must only filter if the player is opened.
|
||||
return PlayerType.getCurrent().isMaximizedOrFullscreen()
|
||||
&& aiCommentsSummary.check(protobufBufferArray).isFiltered();
|
||||
&& aiCommentsSummary.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -3,7 +3,6 @@ package app.revanced.extension.youtube.patches.components;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -146,7 +145,7 @@ final class CustomFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
// All callbacks are custom filter groups.
|
||||
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
|
||||
@@ -158,6 +157,6 @@ final class CustomFilter extends Filter {
|
||||
return true; // No buffer filter, only path filtering.
|
||||
}
|
||||
|
||||
return custom.bufferSearch.matches(protobufBufferArray);
|
||||
return custom.bufferSearch.matches(buffer);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.StringTrieSearch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
@@ -105,7 +103,7 @@ final class DescriptionComponentsFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
|
||||
if (matchedGroup == aiGeneratedVideoSummarySection) {
|
||||
@@ -116,11 +114,11 @@ final class DescriptionComponentsFilter extends Filter {
|
||||
if (exceptions.matches(path)) return false;
|
||||
|
||||
if (matchedGroup == macroMarkersCarousel) {
|
||||
return contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered();
|
||||
return contentIndex == 0 && macroMarkersCarouselGroupList.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == horizontalShelf) {
|
||||
return cellVideoAttribute.check(protobufBufferArray).isFiltered();
|
||||
return cellVideoAttribute.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -59,7 +57,6 @@ abstract class Filter {
|
||||
* Called after an enabled filter has been matched.
|
||||
* Default implementation is to always filter the matched component and log the action.
|
||||
* Subclasses can perform additional or different checks if needed.
|
||||
*
|
||||
* <p>
|
||||
* Method is called off the main thread.
|
||||
*
|
||||
@@ -68,7 +65,7 @@ abstract class Filter {
|
||||
* @param contentIndex Matched index of the identifier or path.
|
||||
* @return True if the litho component should be filtered out.
|
||||
*/
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -554,7 +554,7 @@ final class KeywordContentFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
|
||||
return false;
|
||||
@@ -574,7 +574,7 @@ final class KeywordContentFilter extends Filter {
|
||||
}
|
||||
|
||||
MutableReference<String> matchRef = new MutableReference<>();
|
||||
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
|
||||
if (bufferSearch.matches(buffer, matchRef)) {
|
||||
updateStats(true, matchRef.value);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
);
|
||||
|
||||
private final StringTrieSearch exceptions = new StringTrieSearch();
|
||||
private final StringFilterGroup communityPosts;
|
||||
private final StringFilterGroup surveys;
|
||||
private final StringFilterGroup notifyMe;
|
||||
private final StringFilterGroup singleItemInformationPanel;
|
||||
@@ -44,6 +45,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
private final StringFilterGroup chipBar;
|
||||
private final StringFilterGroup channelProfile;
|
||||
private final ByteArrayFilterGroupList channelProfileBuffer;
|
||||
private final ByteArrayFilterGroup playablesBuffer;
|
||||
|
||||
public LayoutComponentsFilter() {
|
||||
exceptions.addPatterns(
|
||||
@@ -68,7 +70,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
|
||||
// Paths.
|
||||
|
||||
final var communityPosts = new StringFilterGroup(
|
||||
communityPosts = new StringFilterGroup(
|
||||
Settings.HIDE_COMMUNITY_POSTS,
|
||||
"post_base_wrapper", // may be obsolete and no longer needed.
|
||||
"text_post_root.eml",
|
||||
@@ -189,6 +191,12 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
"mini_game_card.eml"
|
||||
);
|
||||
|
||||
// Playable horizontal shelf header.
|
||||
playablesBuffer = new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYABLES,
|
||||
"mini_game"
|
||||
);
|
||||
|
||||
final var quickActions = new StringFilterGroup(
|
||||
Settings.HIDE_QUICK_ACTIONS,
|
||||
"quick_actions"
|
||||
@@ -304,7 +312,7 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
// This identifier is used not only in players but also in search results:
|
||||
// https://github.com/ReVanced/revanced-patches/issues/3245
|
||||
@@ -322,7 +330,13 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
}
|
||||
|
||||
if (matchedGroup == channelProfile) {
|
||||
return channelProfileBuffer.check(protobufBufferArray).isFiltered();
|
||||
return channelProfileBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == communityPosts && NavigationBar.isBackButtonVisible()) {
|
||||
// Allow community posts on channel profile page,
|
||||
// or if viewing an individual channel in the feed.
|
||||
return false;
|
||||
}
|
||||
|
||||
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
|
||||
@@ -331,11 +345,13 @@ public final class LayoutComponentsFilter extends Filter {
|
||||
return compactChannelBarInnerButton.check(path).isFiltered()
|
||||
// The filter may be broad, but in the context of a compactChannelBarInnerButton,
|
||||
// it's safe to assume that the button is the only thing that should be hidden.
|
||||
&& joinMembershipButton.check(protobufBufferArray).isFiltered();
|
||||
&& joinMembershipButton.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == horizontalShelves) {
|
||||
return contentIndex == 0 && (hideShelves() || ticketShelf.check(protobufBufferArray).isFiltered());
|
||||
return contentIndex == 0 && (hideShelves()
|
||||
|| ticketShelf.check(buffer).isFiltered()
|
||||
|| playablesBuffer.check(buffer).isFiltered());
|
||||
}
|
||||
|
||||
if (matchedGroup == chipBar) {
|
||||
|
||||
@@ -17,29 +17,28 @@ public final class LithoFilterPatch {
|
||||
* Simple wrapper to pass the litho parameters through the prefix search.
|
||||
*/
|
||||
private static final class LithoFilterParameters {
|
||||
@Nullable
|
||||
final String identifier;
|
||||
final String path;
|
||||
final byte[] protoBuffer;
|
||||
final byte[] buffer;
|
||||
|
||||
LithoFilterParameters(@Nullable String lithoIdentifier, String lithoPath, byte[] protoBuffer) {
|
||||
LithoFilterParameters(String lithoIdentifier, String lithoPath, byte[] buffer) {
|
||||
this.identifier = lithoIdentifier;
|
||||
this.path = lithoPath;
|
||||
this.protoBuffer = protoBuffer;
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
// Estimate the percentage of the buffer that are Strings.
|
||||
StringBuilder builder = new StringBuilder(Math.max(100, protoBuffer.length / 2));
|
||||
StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
|
||||
builder.append( "ID: ");
|
||||
builder.append(identifier);
|
||||
builder.append(" Path: ");
|
||||
builder.append(path);
|
||||
if (Settings.DEBUG_PROTOBUFFER.get()) {
|
||||
builder.append(" BufferStrings: ");
|
||||
findAsciiStrings(builder, protoBuffer);
|
||||
findAsciiStrings(builder, buffer);
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
@@ -128,21 +127,21 @@ public final class LithoFilterPatch {
|
||||
private static void filterUsingCallbacks(StringTrieSearch pathSearchTree,
|
||||
Filter filter, List<StringFilterGroup> groups,
|
||||
Filter.FilterContentType type) {
|
||||
String filterSimpleName = filter.getClass().getSimpleName();
|
||||
|
||||
for (StringFilterGroup group : groups) {
|
||||
if (!group.includeInSearch()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String pattern : group.filters) {
|
||||
String filterSimpleName = filter.getClass().getSimpleName();
|
||||
|
||||
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex,
|
||||
matchedLength, callbackParameter) -> {
|
||||
if (!group.isEnabled()) return false;
|
||||
|
||||
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
|
||||
final boolean isFiltered = filter.isFiltered(parameters.identifier,
|
||||
parameters.path, parameters.protoBuffer, group, type, matchedStartIndex);
|
||||
parameters.path, parameters.buffer, group, type, matchedStartIndex);
|
||||
|
||||
if (isFiltered && BaseSettings.DEBUG.get()) {
|
||||
if (type == Filter.FilterContentType.IDENTIFIER) {
|
||||
@@ -163,6 +162,7 @@ public final class LithoFilterPatch {
|
||||
|
||||
/**
|
||||
* Injection point. Called off the main thread.
|
||||
* Targets 20.22+
|
||||
*/
|
||||
public static void setProtoBuffer(byte[] buffer) {
|
||||
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
|
||||
@@ -193,9 +193,9 @@ public final class LithoFilterPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean shouldFilter(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
|
||||
public static boolean isFiltered(String lithoIdentifier, StringBuilder pathBuilder) {
|
||||
try {
|
||||
if (pathBuilder.length() == 0) {
|
||||
if (lithoIdentifier.isEmpty() && pathBuilder.length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -210,7 +210,7 @@ public final class LithoFilterPatch {
|
||||
lithoIdentifier, pathBuilder.toString(), buffer);
|
||||
Logger.printDebug(() -> "Searching " + parameter);
|
||||
|
||||
if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) {
|
||||
if (identifierSearchTree.matches(parameter.identifier, parameter)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ public final class LithoFilterPatch {
|
||||
return true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Litho filter failure", ex);
|
||||
Logger.printException(() -> "isFiltered failure", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@@ -34,12 +32,11 @@ public final class PlaybackSpeedMenuFilter extends Filter {
|
||||
Settings.CUSTOM_SPEED_MENU,
|
||||
"playback_speed_sheet_content.eml-js");
|
||||
|
||||
|
||||
addPathCallbacks(playbackRateSelectorGroup);
|
||||
addPathCallbacks(playbackRateSelectorGroup, oldPlaybackMenuGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == oldPlaybackMenuGroup) {
|
||||
isOldPlaybackSpeedMenuVisible = true;
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package app.revanced.extension.youtube.patches.components;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||
@@ -22,17 +20,9 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||
}
|
||||
|
||||
private final ByteArrayFilterGroupList flyoutFilterGroupList = new ByteArrayFilterGroupList();
|
||||
|
||||
private final ByteArrayFilterGroup exception;
|
||||
private final StringFilterGroup videoQualityMenuFooter;
|
||||
|
||||
public PlayerFlyoutMenuItemsFilter() {
|
||||
exception = new ByteArrayFilterGroup(
|
||||
// Whitelist Quality menu item when "Hide Additional settings menu" is enabled
|
||||
Settings.HIDE_PLAYER_FLYOUT_ADDITIONAL_SETTINGS,
|
||||
"quality_sheet"
|
||||
);
|
||||
|
||||
videoQualityMenuFooter = new StringFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER,
|
||||
"quality_sheet_footer"
|
||||
@@ -46,11 +36,11 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||
flyoutFilterGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_CAPTIONS,
|
||||
"closed_caption"
|
||||
"closed_caption_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_ADDITIONAL_SETTINGS,
|
||||
"yt_outline_gear"
|
||||
"yt_outline_gear_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_LOOP_VIDEO,
|
||||
@@ -58,31 +48,31 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_AMBIENT_MODE,
|
||||
"yt_outline_screen_light"
|
||||
"yt_outline_screen_light_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_STABLE_VOLUME,
|
||||
"volume_stable"
|
||||
"volume_stable_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_HELP,
|
||||
"yt_outline_question_circle"
|
||||
"yt_outline_question_circle_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_MORE_INFO,
|
||||
"yt_outline_info_circle"
|
||||
"yt_outline_info_circle_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_LOCK_SCREEN,
|
||||
"yt_outline_lock"
|
||||
"yt_outline_lock_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_SPEED,
|
||||
"yt_outline_play_arrow_half_circle"
|
||||
"yt_outline_play_arrow_half_circle_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_AUDIO_TRACK,
|
||||
"yt_outline_person_radar"
|
||||
"yt_outline_person_radar_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_SLEEP_TIMER,
|
||||
@@ -90,13 +80,17 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_WATCH_IN_VR,
|
||||
"yt_outline_vr"
|
||||
"yt_outline_vr_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_PLAYER_FLYOUT_VIDEO_QUALITY,
|
||||
"yt_outline_adjust_"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == videoQualityMenuFooter) {
|
||||
return true;
|
||||
@@ -107,10 +101,10 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
||||
}
|
||||
|
||||
// Shorts also use this player flyout panel
|
||||
if (PlayerType.getCurrent().isNoneOrHidden() || exception.check(protobufBufferArray).isFiltered()) {
|
||||
if (ShortsPlayerState.isOpen()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return flyoutFilterGroupList.check(protobufBufferArray).isFiltered();
|
||||
return flyoutFilterGroupList.check(buffer).isFiltered();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,15 +84,15 @@ public final class ReturnYouTubeDislikeFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
|
||||
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(buffer);
|
||||
if (result.isFiltered()) {
|
||||
String matchedVideoId = findVideoId(protobufBufferArray);
|
||||
String matchedVideoId = findVideoId(buffer);
|
||||
// Matched video will be null if in incognito mode.
|
||||
// Must pass a null id to correctly clear out the current video data.
|
||||
// Otherwise if a Short is opened in non-incognito, then incognito is enabled and another Short is opened,
|
||||
|
||||
@@ -4,8 +4,6 @@ import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButt
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
@@ -13,7 +11,6 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.NavigationBar;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
@@ -321,7 +318,7 @@ public final class ShortsFilter extends Filter {
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
boolean isFiltered(String identifier, String path, byte[] buffer,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (contentType == FilterContentType.PATH) {
|
||||
if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
|
||||
@@ -330,22 +327,22 @@ public final class ShortsFilter extends Filter {
|
||||
}
|
||||
|
||||
if (matchedGroup == useSoundButton) {
|
||||
return useSoundButtonBuffer.check(protobufBufferArray).isFiltered();
|
||||
return useSoundButtonBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == useTemplateButton) {
|
||||
return useTemplateButtonBuffer.check(protobufBufferArray).isFiltered();
|
||||
return useTemplateButtonBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == shortsCompactFeedVideo) {
|
||||
return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered();
|
||||
return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
// Video action buttons (comment, share, remix) have the same path.
|
||||
// Like and dislike are separate path filters and don't require buffer searching.
|
||||
if (matchedGroup == shortsActionBar) {
|
||||
return videoActionButton.check(path).isFiltered()
|
||||
&& videoActionButtonBuffer.check(protobufBufferArray).isFiltered();
|
||||
&& videoActionButtonBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == suggestedAction) {
|
||||
@@ -356,7 +353,7 @@ public final class ShortsFilter extends Filter {
|
||||
return true;
|
||||
}
|
||||
|
||||
return suggestedActionsBuffer.check(protobufBufferArray).isFiltered();
|
||||
return suggestedActionsBuffer.check(buffer).isFiltered();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -18,7 +18,7 @@ import app.revanced.extension.youtube.settings.Settings;
|
||||
public final class AdvancedVideoQualityMenuPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Injection point. Regular videos.
|
||||
*/
|
||||
public static void onFlyoutMenuCreate(RecyclerView recyclerView) {
|
||||
if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return;
|
||||
@@ -61,22 +61,12 @@ public final class AdvancedVideoQualityMenuPatch {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Used to force the creation of the advanced menu item for the Shorts quality flyout.
|
||||
*/
|
||||
public static boolean forceAdvancedVideoQualityMenuCreation(boolean original) {
|
||||
return Settings.ADVANCED_VIDEO_QUALITY_MENU.get() || original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Shorts video quality flyout.
|
||||
*/
|
||||
public static void showAdvancedVideoQualityMenu(ListView listView) {
|
||||
public static void addVideoQualityListMenuListener(ListView listView) {
|
||||
if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return;
|
||||
|
||||
listView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
|
||||
@@ -91,7 +81,6 @@ public final class AdvancedVideoQualityMenuPatch {
|
||||
listView.setSoundEffectsEnabled(false);
|
||||
final var qualityItemMenuPosition = 4;
|
||||
listView.performItemClick(null, qualityItemMenuPosition, 0);
|
||||
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showAdvancedVideoQualityMenu failure", ex);
|
||||
}
|
||||
@@ -102,4 +91,13 @@ public final class AdvancedVideoQualityMenuPatch {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Used to force the creation of the advanced menu item for the Shorts quality flyout.
|
||||
*/
|
||||
public static boolean forceAdvancedVideoQualityMenuCreation(boolean original) {
|
||||
return Settings.ADVANCED_VIDEO_QUALITY_MENU.get() || original;
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,7 @@ package app.revanced.extension.youtube.patches.playback.quality;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.NetworkType;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
@@ -20,166 +15,96 @@ import app.revanced.extension.youtube.shared.ShortsPlayerState;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class RememberVideoQualityPatch {
|
||||
private static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
|
||||
|
||||
private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI;
|
||||
private static final IntegerSetting videoQualityMobile = Settings.VIDEO_QUALITY_DEFAULT_MOBILE;
|
||||
private static final IntegerSetting shortsQualityWifi = Settings.SHORTS_QUALITY_DEFAULT_WIFI;
|
||||
private static final IntegerSetting shortsQualityMobile = Settings.SHORTS_QUALITY_DEFAULT_MOBILE;
|
||||
|
||||
private static boolean qualityNeedsUpdating;
|
||||
|
||||
/**
|
||||
* If the user selected a new quality from the flyout menu,
|
||||
* and {@link Settings#REMEMBER_VIDEO_QUALITY_LAST_SELECTED} is enabled.
|
||||
*/
|
||||
private static boolean userChangedDefaultQuality;
|
||||
|
||||
/**
|
||||
* Index of the video quality chosen by the user from the flyout menu.
|
||||
*/
|
||||
private static int userSelectedQualityIndex;
|
||||
|
||||
/**
|
||||
* The available qualities of the current video in human readable form: [1080, 720, 480]
|
||||
*/
|
||||
@Nullable
|
||||
private static List<Integer> videoQualities;
|
||||
|
||||
private static boolean shouldRememberVideoQuality() {
|
||||
BooleanSetting preference = ShortsPlayerState.isOpen() ?
|
||||
Settings.REMEMBER_SHORTS_QUALITY_LAST_SELECTED
|
||||
public static boolean shouldRememberVideoQuality() {
|
||||
BooleanSetting preference = ShortsPlayerState.isOpen()
|
||||
? Settings.REMEMBER_SHORTS_QUALITY_LAST_SELECTED
|
||||
: Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED;
|
||||
return preference.get();
|
||||
}
|
||||
|
||||
private static void changeDefaultQuality(int defaultQuality) {
|
||||
public static int getDefaultQualityResolution() {
|
||||
final boolean isShorts = ShortsPlayerState.isOpen();
|
||||
IntegerSetting preference = Utils.getNetworkType() == NetworkType.MOBILE
|
||||
? (isShorts ? shortsQualityMobile : videoQualityMobile)
|
||||
: (isShorts ? shortsQualityWifi : videoQualityWifi);
|
||||
return preference.get();
|
||||
}
|
||||
|
||||
public static void saveDefaultQuality(int qualityResolution) {
|
||||
final boolean shortPlayerOpen = ShortsPlayerState.isOpen();
|
||||
String networkTypeMessage;
|
||||
boolean useShortsPreference = ShortsPlayerState.isOpen();
|
||||
IntegerSetting qualitySetting;
|
||||
if (Utils.getNetworkType() == NetworkType.MOBILE) {
|
||||
if (useShortsPreference) shortsQualityMobile.save(defaultQuality);
|
||||
else videoQualityMobile.save(defaultQuality);
|
||||
networkTypeMessage = str("revanced_remember_video_quality_mobile");
|
||||
qualitySetting = shortPlayerOpen ? shortsQualityMobile : videoQualityMobile;
|
||||
} else {
|
||||
if (useShortsPreference) shortsQualityWifi.save(defaultQuality);
|
||||
else videoQualityWifi.save(defaultQuality);
|
||||
networkTypeMessage = str("revanced_remember_video_quality_wifi");
|
||||
qualitySetting = shortPlayerOpen ? shortsQualityWifi : videoQualityWifi;
|
||||
}
|
||||
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
|
||||
|
||||
if (qualitySetting.get() == qualityResolution) {
|
||||
// User clicked the same video quality as the current video,
|
||||
// or changed between 1080p Premium and non-Premium.
|
||||
return;
|
||||
}
|
||||
qualitySetting.save(qualityResolution);
|
||||
|
||||
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) {
|
||||
String qualityLabel = qualityResolution + "p";
|
||||
Utils.showToastShort(str(
|
||||
useShortsPreference ? "revanced_remember_video_quality_toast_shorts" : "revanced_remember_video_quality_toast",
|
||||
networkTypeMessage, (defaultQuality + "p")
|
||||
));
|
||||
shortPlayerOpen
|
||||
? "revanced_remember_video_quality_toast_shorts"
|
||||
: "revanced_remember_video_quality_toast",
|
||||
networkTypeMessage,
|
||||
qualityLabel)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
|
||||
* @param originalQualityIndex quality index to use, as chosen by YouTube
|
||||
* @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}.
|
||||
*/
|
||||
public static int setVideoQuality(Object[] qualities, final int originalQualityIndex, Object qInterface, String qIndexMethod) {
|
||||
public static void userChangedShortsQuality(int userSelectedQualityIndex) {
|
||||
try {
|
||||
boolean useShortsPreference = ShortsPlayerState.isOpen();
|
||||
final int preferredQuality = Utils.getNetworkType() == NetworkType.MOBILE
|
||||
? (useShortsPreference ? shortsQualityMobile : videoQualityMobile).get()
|
||||
: (useShortsPreference ? shortsQualityWifi : videoQualityWifi).get();
|
||||
|
||||
if (!userChangedDefaultQuality && preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
|
||||
return originalQualityIndex; // Nothing to do.
|
||||
}
|
||||
|
||||
if (videoQualities == null || videoQualities.size() != qualities.length) {
|
||||
videoQualities = new ArrayList<>(qualities.length);
|
||||
for (Object streamQuality : qualities) {
|
||||
for (Field field : streamQuality.getClass().getFields()) {
|
||||
if (field.getType().isAssignableFrom(Integer.TYPE)
|
||||
&& field.getName().length() <= 2) {
|
||||
videoQualities.add(field.getInt(streamQuality));
|
||||
}
|
||||
}
|
||||
if (shouldRememberVideoQuality()) {
|
||||
VideoQuality[] currentQualities = VideoInformation.getCurrentQualities();
|
||||
if (currentQualities == null) {
|
||||
Logger.printDebug(() -> "Cannot save default quality, qualities is null");
|
||||
return;
|
||||
}
|
||||
|
||||
// After changing videos the qualities can initially be for the prior video.
|
||||
// So if the qualities have changed an update is needed.
|
||||
qualityNeedsUpdating = true;
|
||||
Logger.printDebug(() -> "VideoQualities: " + videoQualities);
|
||||
VideoQuality quality = currentQualities[userSelectedQualityIndex];
|
||||
saveDefaultQuality(quality.patch_getResolution());
|
||||
}
|
||||
|
||||
if (userChangedDefaultQuality) {
|
||||
userChangedDefaultQuality = false;
|
||||
final int quality = videoQualities.get(userSelectedQualityIndex);
|
||||
Logger.printDebug(() -> "User changed default quality to: " + quality);
|
||||
changeDefaultQuality(quality);
|
||||
return userSelectedQualityIndex;
|
||||
}
|
||||
|
||||
if (!qualityNeedsUpdating) {
|
||||
return originalQualityIndex;
|
||||
}
|
||||
qualityNeedsUpdating = false;
|
||||
|
||||
// Find the highest quality that is equal to or less than the preferred.
|
||||
int qualityToUse = videoQualities.get(0); // first element is automatic mode
|
||||
int qualityIndexToUse = 0;
|
||||
int i = 0;
|
||||
for (Integer quality : videoQualities) {
|
||||
if (quality <= preferredQuality && qualityToUse < quality) {
|
||||
qualityToUse = quality;
|
||||
qualityIndexToUse = i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
// If the desired quality index is equal to the original index,
|
||||
// then the video is already set to the desired default quality.
|
||||
final int qualityToUseFinal = qualityToUse;
|
||||
if (qualityIndexToUse == originalQualityIndex) {
|
||||
// On first load of a new video, if the UI video quality flyout menu
|
||||
// is not updated then it will still show 'Auto' (ie: Auto (480p)),
|
||||
// even though it's already set to the desired resolution.
|
||||
//
|
||||
// To prevent confusion, set the video index anyways (even if it matches the existing index)
|
||||
// as that will force the UI picker to not display "Auto".
|
||||
Logger.printDebug(() -> "Video is already preferred quality: " + qualityToUseFinal);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Changing video quality from: "
|
||||
+ videoQualities.get(originalQualityIndex) + " to: " + qualityToUseFinal);
|
||||
}
|
||||
|
||||
Method m = qInterface.getClass().getMethod(qIndexMethod, Integer.TYPE);
|
||||
m.invoke(qInterface, qualityToUse);
|
||||
return qualityIndexToUse;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Failed to set quality", ex);
|
||||
return originalQualityIndex;
|
||||
Logger.printException(() -> "userChangedShortsQuality failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Old quality menu.
|
||||
* Injection point. Regular videos.
|
||||
* @param videoResolution Human readable resolution: 480, 720, 1080.
|
||||
*/
|
||||
public static void userChangedQuality(int selectedQualityIndex) {
|
||||
public static void userChangedQuality(int videoResolution) {
|
||||
Utils.verifyOnMainThread();
|
||||
Logger.printDebug(() -> "User changed quality to: " + videoResolution);
|
||||
|
||||
if (shouldRememberVideoQuality()) {
|
||||
userSelectedQualityIndex = selectedQualityIndex;
|
||||
userChangedDefaultQuality = true;
|
||||
saveDefaultQuality(videoResolution);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. New quality menu.
|
||||
*/
|
||||
public static void userChangedQualityInNewFlyout(int selectedQuality) {
|
||||
if (!shouldRememberVideoQuality()) return;
|
||||
|
||||
changeDefaultQuality(selectedQuality); // Quality is human readable resolution (ie: 1080).
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) {
|
||||
Logger.printDebug(() -> "newVideoStarted");
|
||||
qualityNeedsUpdating = true;
|
||||
videoQualities = null;
|
||||
VideoInformation.setDesiredVideoResolution(getDefaultQualityResolution());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ public class CustomPlaybackSpeedPatch {
|
||||
private static WeakReference<Dialog> currentDialog = new WeakReference<>(null);
|
||||
|
||||
static {
|
||||
// Cap at 2 decimals (rounds automatically).
|
||||
// Use same 2 digit format as built in speed picker,
|
||||
speedFormatter.setMinimumFractionDigits(2);
|
||||
speedFormatter.setMaximumFractionDigits(2);
|
||||
|
||||
final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get();
|
||||
@@ -321,7 +322,7 @@ public class CustomPlaybackSpeedPatch {
|
||||
TextView currentSpeedText = new TextView(context);
|
||||
float currentSpeed = VideoInformation.getPlaybackSpeed();
|
||||
// Initially show with only 0 minimum digits, so 1.0 shows as 1x
|
||||
currentSpeedText.setText(formatSpeedStringX(currentSpeed, 0));
|
||||
currentSpeedText.setText(formatSpeedStringX(currentSpeed));
|
||||
currentSpeedText.setTextColor(Utils.getAppForegroundColor());
|
||||
currentSpeedText.setTextSize(16);
|
||||
currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
@@ -398,10 +399,11 @@ public class CustomPlaybackSpeedPatch {
|
||||
return null;
|
||||
}
|
||||
|
||||
VideoInformation.overridePlaybackSpeed(roundedSpeed);
|
||||
RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
|
||||
currentSpeedText.setText(formatSpeedStringX(roundedSpeed, 2)); // Update display.
|
||||
currentSpeedText.setText(formatSpeedStringX(roundedSpeed)); // Update display.
|
||||
speedSlider.setProgress(speedToProgressValue(roundedSpeed)); // Update slider.
|
||||
|
||||
RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
|
||||
VideoInformation.overridePlaybackSpeed(roundedSpeed);
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -437,7 +439,7 @@ public class CustomPlaybackSpeedPatch {
|
||||
gridParams.setMargins(0, 0, 0, 0); // No margins around GridLayout.
|
||||
gridLayout.setLayoutParams(gridParams);
|
||||
|
||||
// For all buttons show at least 1 zero in decimal (2 -> "2.0").
|
||||
// For button use 1 digit minimum.
|
||||
speedFormatter.setMinimumFractionDigits(1);
|
||||
|
||||
// Add buttons for each preset playback speed.
|
||||
@@ -455,7 +457,7 @@ public class CustomPlaybackSpeedPatch {
|
||||
|
||||
// Create speed button.
|
||||
Button speedButton = new Button(context, null, 0);
|
||||
speedButton.setText(speedFormatter.format(speed)); // Do not use 'x' speed format.
|
||||
speedButton.setText(speedFormatter.format(speed));
|
||||
speedButton.setTextColor(Utils.getAppForegroundColor());
|
||||
speedButton.setTextSize(12);
|
||||
speedButton.setAllCaps(false);
|
||||
@@ -498,6 +500,9 @@ public class CustomPlaybackSpeedPatch {
|
||||
gridLayout.addView(buttonContainer);
|
||||
}
|
||||
|
||||
// Restore 2 digit minimum.
|
||||
speedFormatter.setMinimumFractionDigits(2);
|
||||
|
||||
// Add in-rows speed buttons layout to main layout.
|
||||
mainLayout.addView(gridLayout);
|
||||
|
||||
@@ -631,8 +636,7 @@ public class CustomPlaybackSpeedPatch {
|
||||
* @param speed The playback speed value to format.
|
||||
* @return A string representation of the speed with 'x' (e.g. "1.25x" or "1.00x").
|
||||
*/
|
||||
private static String formatSpeedStringX(float speed, int minimumFractionDigits) {
|
||||
speedFormatter.setMinimumFractionDigits(minimumFractionDigits);
|
||||
private static String formatSpeedStringX(float speed) {
|
||||
return speedFormatter.format(speed) + 'x';
|
||||
}
|
||||
|
||||
@@ -671,11 +675,9 @@ public class CustomPlaybackSpeedPatch {
|
||||
*/
|
||||
public static int getAdjustedBackgroundColor(boolean isHandleBar) {
|
||||
final int baseColor = Utils.getDialogBackgroundColor();
|
||||
float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
|
||||
float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
|
||||
return Utils.isDarkModeEnabled()
|
||||
? Utils.adjustColorBrightness(baseColor, darkThemeFactor) // Lighten for dark theme.
|
||||
: Utils.adjustColorBrightness(baseColor, lightThemeFactor); // Darken for light theme.
|
||||
final float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
|
||||
final float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
|
||||
return Utils.adjustColorBrightness(baseColor, lightThemeFactor, darkThemeFactor);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ public class SpoofAppVersionPatch {
|
||||
private static final String SPOOF_APP_VERSION_TARGET = Settings.SPOOF_APP_VERSION_TARGET.get();
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static String getYouTubeVersionOverride(String version) {
|
||||
if (SPOOF_APP_VERSION_ENABLED) return SPOOF_APP_VERSION_TARGET;
|
||||
|
||||
@@ -126,7 +126,7 @@ public final class SeekbarColorPatch {
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static boolean useLotteLaunchSplashScreen(boolean original) {
|
||||
// This method is only used for development purposes to force the old style launch screen.
|
||||
|
||||
@@ -172,6 +172,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_VIDEO_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE);
|
||||
public static final BooleanSetting OPEN_VIDEOS_FULLSCREEN_PORTRAIT = new BooleanSetting("revanced_open_videos_fullscreen_portrait", FALSE);
|
||||
public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE);
|
||||
public static final BooleanSetting VIDEO_QUALITY_DIALOG_BUTTON = new BooleanSetting("revanced_video_quality_dialog_button", FALSE);
|
||||
public static final IntegerSetting PLAYER_OVERLAY_OPACITY = new IntegerSetting("revanced_player_overlay_opacity", 100, true);
|
||||
public static final BooleanSetting PLAYER_POPUP_PANELS = new BooleanSetting("revanced_hide_player_popup_panels", FALSE);
|
||||
|
||||
@@ -222,7 +223,9 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_ASK_BUTTON = new BooleanSetting("revanced_hide_ask_button", FALSE);
|
||||
public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", TRUE);
|
||||
public static final BooleanSetting HIDE_DOWNLOAD_BUTTON = new BooleanSetting("revanced_hide_download_button", FALSE);
|
||||
public static final BooleanSetting HIDE_HYPE_BUTTON = new BooleanSetting("revanced_hide_hype_button", FALSE);
|
||||
public static final BooleanSetting HIDE_LIKE_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_like_dislike_button", FALSE);
|
||||
public static final BooleanSetting HIDE_PROMOTE_BUTTON = new BooleanSetting("revanced_hide_promote_button", FALSE);
|
||||
public static final BooleanSetting HIDE_REMIX_BUTTON = new BooleanSetting("revanced_hide_remix_button", TRUE);
|
||||
public static final BooleanSetting HIDE_REPORT_BUTTON = new BooleanSetting("revanced_hide_report_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SAVE_BUTTON = new BooleanSetting("revanced_hide_save_button", FALSE);
|
||||
@@ -243,6 +246,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_SPEED = new BooleanSetting("revanced_hide_player_flyout_speed", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_STABLE_VOLUME = new BooleanSetting("revanced_hide_player_flyout_stable_volume", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER = new BooleanSetting("revanced_hide_player_flyout_video_quality_footer", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_VIDEO_QUALITY = new BooleanSetting("revanced_hide_player_flyout_video_quality", FALSE);
|
||||
public static final BooleanSetting HIDE_PLAYER_FLYOUT_WATCH_IN_VR = new BooleanSetting("revanced_hide_player_flyout_watch_in_vr", TRUE);
|
||||
|
||||
// General layout
|
||||
@@ -254,6 +258,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
|
||||
public static final EnumSetting<SplashScreenAnimationStyle> SPLASH_SCREEN_ANIMATION_STYLE = new EnumSetting<>("revanced_splash_screen_animation_style", SplashScreenAnimationStyle.FPS_60_ONE_SECOND, true);
|
||||
public static final EnumSetting<HeaderLogo> HEADER_LOGO = new EnumSetting<>("revanced_header_logo", HeaderLogo.DEFAULT, true);
|
||||
public static final BooleanSetting DISABLE_SIGNIN_TO_TV_POPUP = new BooleanSetting("revanced_disable_signin_to_tv_popup", FALSE);
|
||||
|
||||
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE,
|
||||
"revanced_remove_viewer_discretion_dialog_user_dialog_message");
|
||||
|
||||
@@ -333,10 +333,8 @@ class AbstractPreferenceSearchData<T extends Preference> {
|
||||
return text;
|
||||
}
|
||||
|
||||
final int baseColor = Utils.getAppBackgroundColor();
|
||||
final int adjustedColor = Utils.isDarkModeEnabled()
|
||||
? Utils.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme.
|
||||
: Utils.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
|
||||
final int adjustedColor = Utils.adjustColorBrightness(Utils.getAppBackgroundColor(),
|
||||
0.95f, 1.20f);
|
||||
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
|
||||
|
||||
SpannableStringBuilder spannable = new SpannableStringBuilder(text);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package app.revanced.extension.youtube.shared
|
||||
|
||||
import app.revanced.extension.shared.Logger
|
||||
import app.revanced.extension.youtube.Event
|
||||
|
||||
/**
|
||||
* PlayerControls visibility state.
|
||||
*/
|
||||
enum class PlayerControlsVisibility {
|
||||
PLAYER_CONTROLS_VISIBILITY_UNKNOWN,
|
||||
PLAYER_CONTROLS_VISIBILITY_WILL_HIDE,
|
||||
PLAYER_CONTROLS_VISIBILITY_HIDDEN,
|
||||
PLAYER_CONTROLS_VISIBILITY_WILL_SHOW,
|
||||
PLAYER_CONTROLS_VISIBILITY_SHOWN;
|
||||
|
||||
companion object {
|
||||
|
||||
private val nameToPlayerControlsVisibility = PlayerControlsVisibility.entries.associateBy { it.name }
|
||||
|
||||
@JvmStatic
|
||||
fun setFromString(enumName: String) {
|
||||
val newType = nameToPlayerControlsVisibility[enumName]
|
||||
if (newType == null) {
|
||||
Logger.printException { "Unknown PlayerControlsVisibility encountered: $enumName" }
|
||||
} else {
|
||||
current = newType
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
var current
|
||||
get() = currentPlayerControlsVisibility
|
||||
private set(type) {
|
||||
if (currentPlayerControlsVisibility != type) {
|
||||
Logger.printDebug { "Changed to: $type" }
|
||||
|
||||
currentPlayerControlsVisibility = type
|
||||
onChange(type)
|
||||
}
|
||||
}
|
||||
|
||||
@Volatile // Read/write from different threads.
|
||||
private var currentPlayerControlsVisibility = PLAYER_CONTROLS_VISIBILITY_UNKNOWN
|
||||
|
||||
@JvmStatic
|
||||
val onChange = Event<PlayerControlsVisibility>()
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import android.graphics.Rect;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Range;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
@@ -877,7 +876,7 @@ public class SegmentPlaybackController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* injection point.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static void setSponsorBarRect(Object self) {
|
||||
@@ -909,7 +908,7 @@ public class SegmentPlaybackController {
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* injection point.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static void setSponsorBarThickness(int thickness) {
|
||||
|
||||
@@ -5,10 +5,10 @@ import android.view.View;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.patches.VideoInformation;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.videoplayer.PlayerControlButton;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class CreateSegmentButton {
|
||||
@Nullable
|
||||
private static PlayerControlButton instance;
|
||||
@@ -18,7 +18,7 @@ public class CreateSegmentButton {
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void initialize(View controlsView) {
|
||||
try {
|
||||
@@ -36,21 +36,27 @@ public class CreateSegmentButton {
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityImmediate(boolean visible) {
|
||||
if (instance != null) instance.setVisibilityImmediate(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibility(boolean visible, boolean animated) {
|
||||
if (instance != null) instance.setVisibility(visible, animated);
|
||||
}
|
||||
|
||||
private static boolean shouldBeShown() {
|
||||
return Settings.SB_ENABLED.get() && Settings.SB_CREATE_NEW_SEGMENT.get()
|
||||
&& !VideoInformation.isAtEndOfVideo();
|
||||
return Settings.SB_ENABLED.get() && Settings.SB_CREATE_NEW_SEGMENT.get();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ import java.util.Objects;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import app.revanced.extension.youtube.sponsorblock.objects.SponsorSegment;
|
||||
import kotlin.Unit;
|
||||
@@ -227,22 +226,4 @@ public class SponsorBlockViewController {
|
||||
params.bottomMargin = fullScreen ? ctaBottomMargin : defaultBottomMargin;
|
||||
view.setLayoutParams(params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void endOfVideoReached() {
|
||||
try {
|
||||
Logger.printDebug(() -> "endOfVideoReached");
|
||||
// the buttons automatically set themselves to visible when appropriate,
|
||||
// but if buttons are showing when the end of the video is reached then they need
|
||||
// to be forcefully hidden
|
||||
if (!Settings.AUTO_REPEAT.get()) {
|
||||
CreateSegmentButton.hideControls();
|
||||
VotingButton.hideControls();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "endOfVideoReached failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,12 @@ import android.view.View;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.patches.VideoInformation;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
|
||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
|
||||
import app.revanced.extension.youtube.videoplayer.PlayerControlButton;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class VotingButton {
|
||||
@Nullable
|
||||
private static PlayerControlButton instance;
|
||||
@@ -20,7 +20,7 @@ public class VotingButton {
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void initialize(View controlsView) {
|
||||
try {
|
||||
@@ -38,14 +38,21 @@ public class VotingButton {
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityImmediate(boolean visible) {
|
||||
if (instance != null) instance.setVisibilityImmediate(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibility(boolean visible, boolean animated) {
|
||||
if (instance != null) instance.setVisibility(visible, animated);
|
||||
@@ -53,6 +60,6 @@ public class VotingButton {
|
||||
|
||||
private static boolean shouldBeShown() {
|
||||
return Settings.SB_ENABLED.get() && Settings.SB_VOTING_BUTTON.get()
|
||||
&& SegmentPlaybackController.videoHasSegments() && !VideoInformation.isAtEndOfVideo();
|
||||
&& SegmentPlaybackController.videoHasSegments();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.patches.CopyVideoUrlPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class CopyVideoUrlButton {
|
||||
@@ -22,7 +21,7 @@ public class CopyVideoUrlButton {
|
||||
instance = new PlayerControlButton(
|
||||
controlsView,
|
||||
"revanced_copy_video_url_button",
|
||||
"revanced_copy_video_url_button_placeholder",
|
||||
null,
|
||||
Settings.COPY_VIDEO_URL::get,
|
||||
view -> CopyVideoUrlPatch.copyUrl(false),
|
||||
view -> {
|
||||
@@ -35,15 +34,22 @@ public class CopyVideoUrlButton {
|
||||
}
|
||||
}
|
||||
|
||||
/**`
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityImmediate(boolean visible) {
|
||||
if (instance != null) instance.setVisibilityImmediate(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibility(boolean visible, boolean animated) {
|
||||
if (instance != null) instance.setVisibility(visible, animated);
|
||||
|
||||
@@ -7,7 +7,6 @@ import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.youtube.patches.CopyVideoUrlPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class CopyVideoUrlTimestampButton {
|
||||
@@ -22,7 +21,7 @@ public class CopyVideoUrlTimestampButton {
|
||||
instance = new PlayerControlButton(
|
||||
controlsView,
|
||||
"revanced_copy_video_url_timestamp_button",
|
||||
"revanced_copy_video_url_timestamp_button_placeholder",
|
||||
null,
|
||||
Settings.COPY_VIDEO_URL_TIMESTAMP::get,
|
||||
view -> CopyVideoUrlPatch.copyUrl(true),
|
||||
view -> {
|
||||
@@ -36,14 +35,21 @@ public class CopyVideoUrlTimestampButton {
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityImmediate(boolean visible) {
|
||||
if (instance != null) instance.setVisibilityImmediate(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibility(boolean visible, boolean animated) {
|
||||
if (instance != null) instance.setVisibility(visible, animated);
|
||||
|
||||
@@ -22,7 +22,7 @@ public class ExternalDownloadButton {
|
||||
instance = new PlayerControlButton(
|
||||
controlsView,
|
||||
"revanced_external_download_button",
|
||||
"revanced_external_download_button_placeholder",
|
||||
null,
|
||||
Settings.EXTERNAL_DOWNLOADER::get,
|
||||
ExternalDownloadButton::onDownloadClick,
|
||||
null
|
||||
@@ -33,14 +33,21 @@ public class ExternalDownloadButton {
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityImmediate(boolean visible) {
|
||||
if (instance != null) instance.setVisibilityImmediate(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibility(boolean visible, boolean animated) {
|
||||
if (instance != null) instance.setVisibility(visible, animated);
|
||||
|
||||
@@ -4,19 +4,26 @@ import android.view.View;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.patches.VideoInformation;
|
||||
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.showToastShort;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class PlaybackSpeedDialogButton {
|
||||
|
||||
@Nullable
|
||||
private static PlayerControlButton instance;
|
||||
|
||||
private static final DecimalFormat speedDecimalFormatter = new DecimalFormat();
|
||||
static {
|
||||
speedDecimalFormatter.setMinimumFractionDigits(1);
|
||||
speedDecimalFormatter.setMaximumFractionDigits(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@@ -24,23 +31,28 @@ public class PlaybackSpeedDialogButton {
|
||||
try {
|
||||
instance = new PlayerControlButton(
|
||||
controlsView,
|
||||
"revanced_playback_speed_dialog_button_container",
|
||||
"revanced_playback_speed_dialog_button",
|
||||
"revanced_playback_speed_dialog_button_placeholder",
|
||||
"revanced_playback_speed_dialog_button_text",
|
||||
Settings.PLAYBACK_SPEED_DIALOG_BUTTON::get,
|
||||
view -> {
|
||||
try {
|
||||
CustomPlaybackSpeedPatch.showModernCustomPlaybackSpeedDialog(view.getContext());
|
||||
if (Settings.RESTORE_OLD_SPEED_MENU.get()) {
|
||||
CustomPlaybackSpeedPatch.showOldPlaybackSpeedMenu();
|
||||
} else {
|
||||
CustomPlaybackSpeedPatch.showModernCustomPlaybackSpeedDialog(view.getContext());
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "speed button onClick failure", ex);
|
||||
}
|
||||
},
|
||||
view -> {
|
||||
try {
|
||||
final float defaultSpeed = Settings.PLAYBACK_SPEED_DEFAULT.get();
|
||||
final float speed = (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get() ||
|
||||
VideoInformation.getPlaybackSpeed() == Settings.PLAYBACK_SPEED_DEFAULT.get())
|
||||
VideoInformation.getPlaybackSpeed() == defaultSpeed)
|
||||
? 1.0f
|
||||
: Settings.PLAYBACK_SPEED_DEFAULT.get();
|
||||
|
||||
: defaultSpeed;
|
||||
VideoInformation.overridePlaybackSpeed(speed);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "speed button reset failure", ex);
|
||||
@@ -48,22 +60,60 @@ public class PlaybackSpeedDialogButton {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
// Set the appropriate icon.
|
||||
updateButtonAppearance();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initializeButton failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityImmediate(boolean visible) {
|
||||
if (instance != null) instance.setVisibilityImmediate(visible);
|
||||
public static void setVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setVisibilityImmediate(boolean visible) {
|
||||
if (instance != null) {
|
||||
instance.setVisibilityImmediate(visible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setVisibility(boolean visible, boolean animated) {
|
||||
if (instance != null) instance.setVisibility(visible, animated);
|
||||
if (instance != null) {
|
||||
instance.setVisibility(visible, animated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void videoSpeedChanged(float currentVideoSpeed) {
|
||||
updateButtonAppearance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the button's appearance, including icon and text overlay.
|
||||
*/
|
||||
private static void updateButtonAppearance() {
|
||||
if (instance == null) return;
|
||||
|
||||
try {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
String speedText = speedDecimalFormatter.format(VideoInformation.getPlaybackSpeed());
|
||||
instance.setTextOverlay(speedText);
|
||||
Logger.printDebug(() -> "Updated playback speed button text to: " + speedText);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "updateButtonAppearance failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package app.revanced.extension.youtube.videoplayer;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.ViewPropertyAnimator;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -10,78 +11,70 @@ import java.lang.ref.WeakReference;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.shared.PlayerControlsVisibility;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import kotlin.Unit;
|
||||
|
||||
public class PlayerControlButton {
|
||||
public interface PlayerControlButtonVisibility {
|
||||
|
||||
public interface PlayerControlButtonStatus {
|
||||
/**
|
||||
* @return If the button should be shown when the player overlay is visible.
|
||||
*/
|
||||
boolean shouldBeShown();
|
||||
boolean buttonEnabled();
|
||||
}
|
||||
|
||||
private static final int fadeInDuration;
|
||||
private static final int fadeOutDuration;
|
||||
|
||||
private static final Animation fadeInAnimation;
|
||||
private static final Animation fadeOutAnimation;
|
||||
private static final Animation fadeOutImmediate;
|
||||
|
||||
static {
|
||||
fadeInDuration = Utils.getResourceInteger("fade_duration_fast");
|
||||
fadeOutDuration = Utils.getResourceInteger("fade_duration_scheduled");
|
||||
|
||||
fadeInAnimation = Utils.getResourceAnimation("fade_in");
|
||||
fadeInAnimation.setDuration(fadeInDuration);
|
||||
|
||||
fadeOutAnimation = Utils.getResourceAnimation("fade_out");
|
||||
fadeOutAnimation.setDuration(fadeOutDuration);
|
||||
|
||||
// Animation for the fast fade out after tapping the overlay.
|
||||
// Currently not used but should be.
|
||||
fadeOutImmediate = Utils.getResourceAnimation("abc_fade_out");
|
||||
fadeOutImmediate.setDuration(Utils.getResourceInteger("fade_duration_fast"));
|
||||
}
|
||||
private static final int fadeInDuration = Utils.getResourceInteger("fade_duration_fast");
|
||||
private static final int fadeOutDuration = Utils.getResourceInteger("fade_duration_scheduled");
|
||||
|
||||
private final WeakReference<View> containerRef;
|
||||
private final WeakReference<View> buttonRef;
|
||||
/**
|
||||
* Empty view with the same layout size as the button. Used to fill empty space while the
|
||||
* fade out animation runs. Without this the chapter titles overlapping the button when fading out.
|
||||
*/
|
||||
private final WeakReference<View> placeHolderRef;
|
||||
private final PlayerControlButtonVisibility visibilityCheck;
|
||||
private final WeakReference<TextView> textOverlayRef;
|
||||
private final PlayerControlButtonStatus enabledStatus;
|
||||
private boolean isVisible;
|
||||
private long lastTimeSetVisible;
|
||||
|
||||
public PlayerControlButton(View controlsViewGroup,
|
||||
String imageViewButtonId,
|
||||
@Nullable String placeholderId,
|
||||
PlayerControlButtonVisibility buttonVisibility,
|
||||
String buttonId,
|
||||
@Nullable String textOverlayId,
|
||||
PlayerControlButtonStatus enabledStatus,
|
||||
View.OnClickListener onClickListener,
|
||||
@Nullable View.OnLongClickListener longClickListener) {
|
||||
ImageView imageView = Utils.getChildViewByResourceName(controlsViewGroup, imageViewButtonId);
|
||||
imageView.setVisibility(View.GONE);
|
||||
this(controlsViewGroup, buttonId, buttonId, textOverlayId,
|
||||
enabledStatus, onClickListener, longClickListener);
|
||||
}
|
||||
|
||||
View tempPlaceholder = null;
|
||||
if (placeholderId != null) {
|
||||
tempPlaceholder = Utils.getChildViewByResourceName(controlsViewGroup, placeholderId);
|
||||
tempPlaceholder.setVisibility(View.GONE);
|
||||
}
|
||||
placeHolderRef = new WeakReference<>(tempPlaceholder);
|
||||
public PlayerControlButton(View controlsViewGroup,
|
||||
String viewToHide,
|
||||
String buttonId,
|
||||
@Nullable String textOverlayId,
|
||||
PlayerControlButtonStatus enabledStatus,
|
||||
View.OnClickListener onClickListener,
|
||||
@Nullable View.OnLongClickListener longClickListener) {
|
||||
View containerView = Utils.getChildViewByResourceName(controlsViewGroup, viewToHide);
|
||||
containerView.setVisibility(View.GONE);
|
||||
containerRef = new WeakReference<>(containerView);
|
||||
|
||||
imageView.setOnClickListener(onClickListener);
|
||||
View button = Utils.getChildViewByResourceName(controlsViewGroup, buttonId);
|
||||
button.setOnClickListener(onClickListener);
|
||||
if (longClickListener != null) {
|
||||
imageView.setOnLongClickListener(longClickListener);
|
||||
button.setOnLongClickListener(longClickListener);
|
||||
}
|
||||
buttonRef = new WeakReference<>(button);
|
||||
|
||||
visibilityCheck = buttonVisibility;
|
||||
buttonRef = new WeakReference<>(imageView);
|
||||
TextView tempTextOverlay = null;
|
||||
if (textOverlayId != null) {
|
||||
tempTextOverlay = Utils.getChildViewByResourceName(controlsViewGroup, textOverlayId);
|
||||
}
|
||||
textOverlayRef = new WeakReference<>(tempTextOverlay);
|
||||
|
||||
this.enabledStatus = enabledStatus;
|
||||
isVisible = false;
|
||||
|
||||
// Update the visibility after the player type changes.
|
||||
// This ensures that button animations are cleared and their states are updated correctly
|
||||
// when switching between states like minimized, maximized, or fullscreen, preventing
|
||||
// "stuck" animations or incorrect visibility. Without this fix the issue is most noticable
|
||||
// "stuck" animations or incorrect visibility. Without this fix the issue is most noticeable
|
||||
// when maximizing type 3 miniplayer.
|
||||
PlayerType.getOnChange().addObserver((PlayerType type) -> {
|
||||
playerTypeChanged(type);
|
||||
@@ -89,13 +82,55 @@ public class PlayerControlButton {
|
||||
});
|
||||
}
|
||||
|
||||
public void setVisibilityNegatedImmediate() {
|
||||
try {
|
||||
Utils.verifyOnMainThread();
|
||||
if (PlayerControlsVisibility.getCurrent() != PlayerControlsVisibility.PLAYER_CONTROLS_VISIBILITY_HIDDEN) {
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean buttonEnabled = enabledStatus.buttonEnabled();
|
||||
if (!buttonEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
View container = containerRef.get();
|
||||
if (container == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
isVisible = false;
|
||||
|
||||
ViewPropertyAnimator animate = container.animate();
|
||||
animate.cancel();
|
||||
|
||||
// If the overlay is tapped to display then immediately tapped to dismiss
|
||||
// before the fade in animation finishes, then the fade out animation is
|
||||
// the time between when the fade in started and now.
|
||||
final long animationDuration = Math.min(fadeInDuration,
|
||||
System.currentTimeMillis() - lastTimeSetVisible);
|
||||
if (animationDuration <= 0) {
|
||||
// Should never happen, but handle just in case.
|
||||
container.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
|
||||
animate.alpha(0)
|
||||
.setDuration(animationDuration)
|
||||
.withEndAction(() -> container.setVisibility(View.GONE))
|
||||
.start();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setVisibilityNegatedImmediate failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVisibilityImmediate(boolean visible) {
|
||||
if (visible) {
|
||||
// Fix button flickering, by pushing this call to the back of
|
||||
// the main thread and letting other layout code run first.
|
||||
Utils.runOnMainThread(() -> private_setVisibility(true, false));
|
||||
Utils.runOnMainThread(() -> privateSetVisibility(true, false));
|
||||
} else {
|
||||
private_setVisibility(false, false);
|
||||
privateSetVisibility(false, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,47 +138,53 @@ public class PlayerControlButton {
|
||||
// Ignore this call, otherwise with full screen thumbnails the buttons are visible while seeking.
|
||||
if (visible && !animated) return;
|
||||
|
||||
private_setVisibility(visible, animated);
|
||||
privateSetVisibility(visible, animated);
|
||||
}
|
||||
|
||||
private void private_setVisibility(boolean visible, boolean animated) {
|
||||
private void privateSetVisibility(boolean visible, boolean animated) {
|
||||
try {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
if (isVisible == visible) return;
|
||||
isVisible = visible;
|
||||
|
||||
View button = buttonRef.get();
|
||||
if (button == null) return;
|
||||
if (visible) {
|
||||
lastTimeSetVisible = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
View placeholder = placeHolderRef.get();
|
||||
final boolean shouldBeShown = visibilityCheck.shouldBeShown();
|
||||
View container = containerRef.get();
|
||||
if (container == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (visible && enabledStatus.buttonEnabled()) {
|
||||
ViewPropertyAnimator animate = container.animate();
|
||||
animate.cancel();
|
||||
container.setVisibility(View.VISIBLE);
|
||||
|
||||
if (visible && shouldBeShown) {
|
||||
button.clearAnimation();
|
||||
if (animated) {
|
||||
button.startAnimation(PlayerControlButton.fadeInAnimation);
|
||||
container.setAlpha(0);
|
||||
animate.alpha(1)
|
||||
.setDuration(fadeInDuration)
|
||||
.start();
|
||||
} else {
|
||||
container.setAlpha(1);
|
||||
}
|
||||
button.setVisibility(View.VISIBLE);
|
||||
} else if (container.getVisibility() == View.VISIBLE) {
|
||||
ViewPropertyAnimator animate = container.animate();
|
||||
animate.cancel();
|
||||
|
||||
if (placeholder != null) {
|
||||
placeholder.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
if (button.getVisibility() == View.VISIBLE) {
|
||||
button.clearAnimation();
|
||||
if (animated) {
|
||||
button.startAnimation(PlayerControlButton.fadeOutAnimation);
|
||||
}
|
||||
button.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (placeholder != null) {
|
||||
placeholder.setVisibility(shouldBeShown
|
||||
? View.VISIBLE
|
||||
: View.GONE);
|
||||
if (animated) {
|
||||
animate.alpha(0)
|
||||
.setDuration(fadeOutDuration)
|
||||
.withEndAction(() -> container.setVisibility(View.GONE))
|
||||
.start();
|
||||
} else {
|
||||
container.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "private_setVisibility failure", ex);
|
||||
Logger.printException(() -> "privateSetVisibility failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,40 +192,61 @@ public class PlayerControlButton {
|
||||
* Synchronizes the button state after the player state changes.
|
||||
*/
|
||||
private void playerTypeChanged(PlayerType newType) {
|
||||
Utils.verifyOnMainThread();
|
||||
if (newType != PlayerType.WATCH_WHILE_MINIMIZED && !newType.isMaximizedOrFullscreen()) {
|
||||
return;
|
||||
}
|
||||
|
||||
View button = buttonRef.get();
|
||||
if (button == null) return;
|
||||
View container = containerRef.get();
|
||||
if (container == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
button.clearAnimation();
|
||||
View placeholder = placeHolderRef.get();
|
||||
container.animate().cancel();
|
||||
|
||||
if (visibilityCheck.shouldBeShown()) {
|
||||
if (isVisible) {
|
||||
button.setVisibility(View.VISIBLE);
|
||||
if (placeholder != null) placeholder.setVisibility(View.GONE);
|
||||
} else {
|
||||
button.setVisibility(View.GONE);
|
||||
if (placeholder != null) placeholder.setVisibility(View.VISIBLE);
|
||||
}
|
||||
if (isVisible && enabledStatus.buttonEnabled()) {
|
||||
container.setVisibility(View.VISIBLE);
|
||||
container.setAlpha(1);
|
||||
} else {
|
||||
button.setVisibility(View.GONE);
|
||||
if (placeholder != null) placeholder.setVisibility(View.GONE);
|
||||
container.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void hide() {
|
||||
if (!isVisible) return;
|
||||
|
||||
Utils.verifyOnMainThread();
|
||||
View view = buttonRef.get();
|
||||
if (!isVisible) {
|
||||
return;
|
||||
}
|
||||
isVisible = false;
|
||||
|
||||
View view = containerRef.get();
|
||||
if (view == null) return;
|
||||
view.setVisibility(View.GONE);
|
||||
|
||||
view = placeHolderRef.get();
|
||||
if (view != null) view.setVisibility(View.GONE);
|
||||
isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the icon of the button.
|
||||
* @param resourceId Drawable identifier, or zero to hide the icon.
|
||||
*/
|
||||
public void setIcon(int resourceId) {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
View button = buttonRef.get();
|
||||
if (button instanceof ImageView imageButton) {
|
||||
imageButton.setImageResource(resourceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the text to be displayed on the text overlay.
|
||||
* @param text The text to set on the overlay, or null to clear the text.
|
||||
*/
|
||||
public void setTextOverlay(CharSequence text) {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
TextView textOverlay = textOverlayRef.get();
|
||||
if (textOverlay != null) {
|
||||
textOverlay.setText(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
package app.revanced.extension.youtube.videoplayer;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
import static app.revanced.extension.youtube.patches.VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE;
|
||||
import static app.revanced.extension.youtube.patches.VideoInformation.VIDEO_QUALITY_PREMIUM_NAME;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.UnderlineSpan;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.patches.VideoInformation;
|
||||
import app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import kotlin.Unit;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class VideoQualityDialogButton {
|
||||
|
||||
@Nullable
|
||||
private static PlayerControlButton instance;
|
||||
|
||||
@Nullable
|
||||
private static CharSequence currentOverlayText;
|
||||
|
||||
static {
|
||||
VideoInformation.onQualityChange.addObserver((@Nullable VideoQuality quality) -> {
|
||||
updateButtonText(quality);
|
||||
return Unit.INSTANCE;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void initializeButton(View controlsView) {
|
||||
try {
|
||||
instance = new PlayerControlButton(
|
||||
controlsView,
|
||||
"revanced_video_quality_dialog_button_container",
|
||||
"revanced_video_quality_dialog_button",
|
||||
"revanced_video_quality_dialog_button_text",
|
||||
Settings.VIDEO_QUALITY_DIALOG_BUTTON::get,
|
||||
view -> {
|
||||
try {
|
||||
showVideoQualityDialog(view.getContext());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Video quality button onClick failure", ex);
|
||||
}
|
||||
},
|
||||
view -> {
|
||||
try {
|
||||
VideoQuality[] qualities = VideoInformation.getCurrentQualities();
|
||||
if (qualities == null) {
|
||||
Logger.printDebug(() -> "Cannot reset quality, videoQualities is null");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Reset to default quality.
|
||||
final int defaultResolution = RememberVideoQualityPatch.getDefaultQualityResolution();
|
||||
for (VideoQuality quality : qualities) {
|
||||
final int resolution = quality.patch_getResolution();
|
||||
if (resolution != AUTOMATIC_VIDEO_QUALITY_VALUE && resolution <= defaultResolution) {
|
||||
Logger.printDebug(() -> "Resetting quality to: " + quality);
|
||||
VideoInformation.changeQuality(quality);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Existing hook cannot set default quality to auto.
|
||||
// Instead show the quality dialog.
|
||||
showVideoQualityDialog(view.getContext());
|
||||
return true;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Video quality button reset failure", ex);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
// Set initial text.
|
||||
updateButtonText(VideoInformation.getCurrentQuality());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initializeButton failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* injection point.
|
||||
*/
|
||||
public static void setVisibilityNegatedImmediate() {
|
||||
if (instance != null) instance.setVisibilityNegatedImmediate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setVisibilityImmediate(boolean visible) {
|
||||
if (instance != null) {
|
||||
instance.setVisibilityImmediate(visible);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void setVisibility(boolean visible, boolean animated) {
|
||||
if (instance != null) {
|
||||
instance.setVisibility(visible, animated);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the button text based on the current video quality.
|
||||
*/
|
||||
public static void updateButtonText(@Nullable VideoQuality quality) {
|
||||
try {
|
||||
Utils.verifyOnMainThread();
|
||||
if (instance == null) return;
|
||||
|
||||
final int resolution = quality == null
|
||||
? AUTOMATIC_VIDEO_QUALITY_VALUE // Video is still loading.
|
||||
: quality.patch_getResolution();
|
||||
|
||||
SpannableStringBuilder text = new SpannableStringBuilder();
|
||||
String qualityText = switch (resolution) {
|
||||
case AUTOMATIC_VIDEO_QUALITY_VALUE -> "";
|
||||
case 144, 240, 360 -> "LD";
|
||||
case 480 -> "SD";
|
||||
case 720 -> "HD";
|
||||
case 1080 -> "FHD";
|
||||
case 1440 -> "QHD";
|
||||
case 2160 -> "4K";
|
||||
default -> "?"; // Should never happen.
|
||||
};
|
||||
text.append(qualityText);
|
||||
|
||||
if (quality != null && quality.patch_getQualityName().contains(VIDEO_QUALITY_PREMIUM_NAME)) {
|
||||
// Underline the entire "FHD" text for 1080p Premium.
|
||||
text.setSpan(new UnderlineSpan(), 0, qualityText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
currentOverlayText = text;
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
if (currentOverlayText != text) {
|
||||
Logger.printDebug(() -> "Ignoring stale button text update of: " + text);
|
||||
return;
|
||||
}
|
||||
instance.setTextOverlay(text);
|
||||
}, 100);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "updateButtonText failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog with available video qualities, excluding Auto, with a title showing the current quality.
|
||||
*/
|
||||
private static void showVideoQualityDialog(Context context) {
|
||||
try {
|
||||
VideoQuality[] currentQualities = VideoInformation.getCurrentQualities();
|
||||
VideoQuality currentQuality = VideoInformation.getCurrentQuality();
|
||||
if (currentQualities == null || currentQuality == null) {
|
||||
Logger.printDebug(() -> "Cannot show qualities dialog, videoQualities is null");
|
||||
return;
|
||||
}
|
||||
if (currentQualities.length < 2) {
|
||||
// Should never happen.
|
||||
Logger.printException(() -> "Cannot show qualities dialog, no qualities available");
|
||||
return;
|
||||
}
|
||||
|
||||
// -1 adjustment for automatic quality at first index.
|
||||
int listViewSelectedIndex = -1;
|
||||
for (VideoQuality quality : currentQualities) {
|
||||
if (quality.patch_getQualityName().equals(currentQuality.patch_getQualityName())) {
|
||||
break;
|
||||
}
|
||||
listViewSelectedIndex++;
|
||||
}
|
||||
|
||||
List<String> qualityLabels = new ArrayList<>(currentQualities.length - 1);
|
||||
for (VideoQuality availableQuality : currentQualities) {
|
||||
if (availableQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE) {
|
||||
qualityLabels.add(availableQuality.patch_getQualityName());
|
||||
}
|
||||
}
|
||||
|
||||
Dialog dialog = new Dialog(context);
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
dialog.setCanceledOnTouchOutside(true);
|
||||
dialog.setCancelable(true);
|
||||
|
||||
final int dip4 = dipToPixels(4); // Height for handle bar.
|
||||
final int dip5 = dipToPixels(5); // Padding for mainLayout.
|
||||
final int dip6 = dipToPixels(6); // Bottom margin.
|
||||
final int dip8 = dipToPixels(8); // Side padding.
|
||||
final int dip16 = dipToPixels(16); // Left padding for ListView.
|
||||
final int dip20 = dipToPixels(20); // Margin below handle.
|
||||
final int dip40 = dipToPixels(40); // Width for handle bar.
|
||||
|
||||
LinearLayout mainLayout = new LinearLayout(context);
|
||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
mainLayout.setPadding(dip5, dip8, dip5, dip8);
|
||||
|
||||
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
|
||||
Utils.createCornerRadii(12), null, null));
|
||||
background.getPaint().setColor(Utils.getDialogBackgroundColor());
|
||||
mainLayout.setBackground(background);
|
||||
|
||||
View handleBar = new View(context);
|
||||
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
|
||||
Utils.createCornerRadii(4), null, null));
|
||||
final int baseColor = Utils.getDialogBackgroundColor();
|
||||
final int adjustedHandleBarBackgroundColor = Utils.adjustColorBrightness(
|
||||
baseColor, 0.9f, 1.25f);
|
||||
handleBackground.getPaint().setColor(adjustedHandleBarBackgroundColor);
|
||||
handleBar.setBackground(handleBackground);
|
||||
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(dip40, dip4);
|
||||
handleParams.gravity = Gravity.CENTER_HORIZONTAL;
|
||||
handleParams.setMargins(0, 0, 0, dip20);
|
||||
handleBar.setLayoutParams(handleParams);
|
||||
mainLayout.addView(handleBar);
|
||||
|
||||
// Create SpannableStringBuilder for formatted text.
|
||||
SpannableStringBuilder spannableTitle = new SpannableStringBuilder();
|
||||
String titlePart = str("video_quality_quick_menu_title");
|
||||
String separatorPart = str("video_quality_title_seperator");
|
||||
|
||||
// Append title part with default foreground color.
|
||||
spannableTitle.append(titlePart);
|
||||
spannableTitle.setSpan(
|
||||
new ForegroundColorSpan(Utils.getAppForegroundColor()),
|
||||
0,
|
||||
titlePart.length(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
spannableTitle.append(" "); // Space after title.
|
||||
|
||||
// Append separator part with adjusted title color.
|
||||
int separatorStart = spannableTitle.length();
|
||||
spannableTitle.append(separatorPart);
|
||||
final int adjustedTitleForegroundColor = Utils.adjustColorBrightness(
|
||||
Utils.getAppForegroundColor(), 1.6f, 0.6f);
|
||||
spannableTitle.setSpan(
|
||||
new ForegroundColorSpan(adjustedTitleForegroundColor),
|
||||
separatorStart,
|
||||
separatorStart + separatorPart.length(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
spannableTitle.append(" "); // Space after separator.
|
||||
|
||||
// Append quality label with adjusted title color.
|
||||
final int qualityStart = spannableTitle.length();
|
||||
spannableTitle.append(currentQuality.patch_getQualityName());
|
||||
spannableTitle.setSpan(
|
||||
new ForegroundColorSpan(adjustedTitleForegroundColor),
|
||||
qualityStart,
|
||||
qualityStart + currentQuality.patch_getQualityName().length(),
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
);
|
||||
|
||||
// Add title with current quality.
|
||||
TextView titleView = new TextView(context);
|
||||
titleView.setText(spannableTitle);
|
||||
titleView.setTextSize(16);
|
||||
// Remove setTextColor since color is handled by SpannableStringBuilder.
|
||||
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
titleParams.setMargins(dip8, 0, 0, dip20);
|
||||
titleView.setLayoutParams(titleParams);
|
||||
mainLayout.addView(titleView);
|
||||
|
||||
ListView listView = new ListView(context);
|
||||
CustomQualityAdapter adapter = new CustomQualityAdapter(context, qualityLabels);
|
||||
adapter.setSelectedPosition(listViewSelectedIndex);
|
||||
listView.setAdapter(adapter);
|
||||
listView.setDivider(null);
|
||||
listView.setPadding(dip16, 0, 0, 0);
|
||||
|
||||
listView.setOnItemClickListener((parent, view, which, id) -> {
|
||||
try {
|
||||
final int originalIndex = which + 1; // Adjust for automatic.
|
||||
VideoQuality selectedQuality = currentQualities[originalIndex];
|
||||
RememberVideoQualityPatch.userChangedQuality(selectedQuality.patch_getResolution());
|
||||
VideoInformation.changeQuality(selectedQuality);
|
||||
|
||||
dialog.dismiss();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Video quality selection failure", ex);
|
||||
}
|
||||
});
|
||||
|
||||
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
listViewParams.setMargins(0, 0, 0, dip5);
|
||||
listView.setLayoutParams(listViewParams);
|
||||
mainLayout.addView(listView);
|
||||
|
||||
LinearLayout wrapperLayout = new LinearLayout(context);
|
||||
wrapperLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
wrapperLayout.setPadding(dip8, 0, dip8, 0);
|
||||
wrapperLayout.addView(mainLayout);
|
||||
dialog.setContentView(wrapperLayout);
|
||||
|
||||
Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
WindowManager.LayoutParams params = window.getAttributes();
|
||||
params.gravity = Gravity.BOTTOM;
|
||||
params.y = dip6;
|
||||
int portraitWidth = context.getResources().getDisplayMetrics().widthPixels;
|
||||
if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
portraitWidth = Math.min(
|
||||
portraitWidth,
|
||||
context.getResources().getDisplayMetrics().heightPixels);
|
||||
// Limit height in landscape mode.
|
||||
params.height = Utils.percentageHeightToPixels(80);
|
||||
} else {
|
||||
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
}
|
||||
params.width = portraitWidth;
|
||||
window.setAttributes(params);
|
||||
window.setBackgroundDrawable(null);
|
||||
}
|
||||
|
||||
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
|
||||
Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
|
||||
slideInABottomAnimation.setDuration(fadeDurationFast);
|
||||
mainLayout.startAnimation(slideInABottomAnimation);
|
||||
|
||||
// noinspection ClickableViewAccessibility
|
||||
mainLayout.setOnTouchListener(new View.OnTouchListener() {
|
||||
final float dismissThreshold = dipToPixels(100);
|
||||
float touchY;
|
||||
float translationY;
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
touchY = event.getRawY();
|
||||
translationY = mainLayout.getTranslationY();
|
||||
return true;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
final float deltaY = event.getRawY() - touchY;
|
||||
if (deltaY >= 0) {
|
||||
mainLayout.setTranslationY(translationY + deltaY);
|
||||
}
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
if (mainLayout.getTranslationY() > dismissThreshold) {
|
||||
//noinspection ExtractMethodRecommender
|
||||
final float remainingDistance = context.getResources().getDisplayMetrics().heightPixels
|
||||
- mainLayout.getTop();
|
||||
TranslateAnimation slideOut = new TranslateAnimation(
|
||||
0, 0, mainLayout.getTranslationY(), remainingDistance);
|
||||
slideOut.setDuration(fadeDurationFast);
|
||||
slideOut.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {}
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {}
|
||||
});
|
||||
mainLayout.startAnimation(slideOut);
|
||||
} else {
|
||||
TranslateAnimation slideBack = new TranslateAnimation(
|
||||
0, 0, mainLayout.getTranslationY(), 0);
|
||||
slideBack.setDuration(fadeDurationFast);
|
||||
mainLayout.startAnimation(slideBack);
|
||||
mainLayout.setTranslationY(0);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showVideoQualityDialog failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static class CustomQualityAdapter extends ArrayAdapter<String> {
|
||||
private static final int CUSTOM_LIST_ITEM_CHECKED_ID = Utils.getResourceIdentifier(
|
||||
"revanced_custom_list_item_checked", "layout");
|
||||
private static final int CHECK_ICON_ID = Utils.getResourceIdentifier(
|
||||
"revanced_check_icon", "id");
|
||||
private static final int CHECK_ICON_PLACEHOLDER_ID = Utils.getResourceIdentifier(
|
||||
"revanced_check_icon_placeholder", "id");
|
||||
private static final int ITEM_TEXT_ID = Utils.getResourceIdentifier(
|
||||
"revanced_item_text", "id");
|
||||
|
||||
private int selectedPosition = -1;
|
||||
|
||||
public CustomQualityAdapter(@NonNull Context context, @NonNull List<String> objects) {
|
||||
super(context, 0, objects);
|
||||
}
|
||||
|
||||
private void setSelectedPosition(int position) {
|
||||
this.selectedPosition = position;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
||||
ViewHolder viewHolder;
|
||||
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(getContext()).inflate(
|
||||
CUSTOM_LIST_ITEM_CHECKED_ID,
|
||||
parent,
|
||||
false
|
||||
);
|
||||
viewHolder = new ViewHolder();
|
||||
viewHolder.checkIcon = convertView.findViewById(CHECK_ICON_ID);
|
||||
viewHolder.placeholder = convertView.findViewById(CHECK_ICON_PLACEHOLDER_ID);
|
||||
viewHolder.textView = convertView.findViewById(ITEM_TEXT_ID);
|
||||
convertView.setTag(viewHolder);
|
||||
} else {
|
||||
viewHolder = (ViewHolder) convertView.getTag();
|
||||
}
|
||||
|
||||
viewHolder.textView.setText(getItem(position));
|
||||
final boolean isSelected = position == selectedPosition;
|
||||
viewHolder.checkIcon.setVisibility(isSelected ? View.VISIBLE : View.GONE);
|
||||
viewHolder.placeholder.setVisibility(isSelected ? View.GONE : View.INVISIBLE);
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
private static class ViewHolder {
|
||||
ImageView checkIcon;
|
||||
View placeholder;
|
||||
TextView textView;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.google.android.libraries.youtube.innertube.model.media;
|
||||
|
||||
public abstract class VideoQuality implements Comparable<VideoQuality> {
|
||||
public abstract String patch_getQualityName();
|
||||
|
||||
public abstract int patch_getResolution();
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
|
||||
org.gradle.parallel = true
|
||||
android.useAndroidX = true
|
||||
kotlin.code.style = official
|
||||
version = 5.33.0-dev.2
|
||||
version = 5.35.0-dev.4
|
||||
|
||||
@@ -11,8 +11,8 @@ appcompat = "1.7.0"
|
||||
okhttp = "5.0.0-alpha.14"
|
||||
retrofit = "2.11.0"
|
||||
guava = "33.4.0-jre"
|
||||
protobuf-javalite = "4.31.1"
|
||||
protoc = "4.31.1"
|
||||
protobuf-javalite = "4.32.0"
|
||||
protoc = "4.32.0"
|
||||
protobuf = "0.9.5"
|
||||
antlr4 = "4.13.2"
|
||||
nanohttpd = "2.3.1"
|
||||
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -9,7 +9,7 @@
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"gradle-semantic-release-plugin": "^1.10.1",
|
||||
"semantic-release": "^24.2.6"
|
||||
"semantic-release": "^24.2.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -6889,10 +6889,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semantic-release": {
|
||||
"version": "24.2.6",
|
||||
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.6.tgz",
|
||||
"integrity": "sha512-D0cwjlO5RZzHHxAcsoF1HxiRLfC3ehw+ay+zntzFs6PNX6aV0JzKNG15mpxPipBYa/l4fHly88dHvgDyqwb1Ww==",
|
||||
"version": "24.2.7",
|
||||
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.7.tgz",
|
||||
"integrity": "sha512-g7RssbTAbir1k/S7uSwSVZFfFXwpomUB9Oas0+xi9KStSCmeDXcA7rNhiskjLqvUe/Evhx8fVCT16OSa34eM5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@semantic-release/commit-analyzer": "^13.0.0-beta.1",
|
||||
"@semantic-release/error": "^4.0.0",
|
||||
|
||||
@@ -4,6 +4,6 @@
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"gradle-semantic-release-plugin": "^1.10.1",
|
||||
"semantic-release": "^24.2.6"
|
||||
"semantic-release": "^24.2.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +264,10 @@ public final class app/revanced/patches/instagram/ads/HideAdsPatchKt {
|
||||
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/instagram/hide/navigation/HideNavigationButtonsKt {
|
||||
public static final fun getHideNavigationButtonsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/instagram/misc/signature/SignatureCheckPatchKt {
|
||||
public static final fun getSignatureCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -508,6 +512,13 @@ public final class app/revanced/patches/reddit/ad/general/HideAdsPatchKt {
|
||||
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/reddit/customclients/FixRedgifsApiPatchKt {
|
||||
public static final field CREATE_NEW_CLIENT_METHOD Ljava/lang/String;
|
||||
public static final field INSTALL_NEW_CLIENT_METHOD Ljava/lang/String;
|
||||
public static final fun fixRedgifsApiPatch (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
public static synthetic fun fixRedgifsApiPatch$default (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/reddit/customclients/FixSLinksPatchKt {
|
||||
public static final field RESOLVE_S_LINK_METHOD Ljava/lang/String;
|
||||
public static final field SET_ACCESS_TOKEN_METHOD Ljava/lang/String;
|
||||
@@ -536,6 +547,10 @@ public final class app/revanced/patches/reddit/customclients/boostforreddit/fix/
|
||||
public static final fun getFixAudioMissingInDownloadsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/reddit/customclients/boostforreddit/fix/redgifs/FixRedgifsApiPatchKt {
|
||||
public static final fun getFixRedgifsApi ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/reddit/customclients/boostforreddit/fix/slink/FixSLinksPatchKt {
|
||||
public static final field EXTENSION_CLASS_DESCRIPTOR Ljava/lang/String;
|
||||
public static final fun getFixSlinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
@@ -606,6 +621,10 @@ public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/
|
||||
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/redgifs/FixRedgifsApiPatchKt {
|
||||
public static final fun getFixRedgifsApi ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/slink/FixSLinksPatchKt {
|
||||
public static final field EXTENSION_CLASS_DESCRIPTOR Ljava/lang/String;
|
||||
public static final fun getFixSLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
@@ -1357,6 +1376,10 @@ public final class app/revanced/patches/youtube/layout/hide/shorts/HideShortsCom
|
||||
public static final fun getHideShortsComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/layout/hide/signintotvpopup/DisableSignInToTvPatchPopupKt {
|
||||
public static final fun getDisableSignInToTvPopupPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/DisableSuggestedVideoEndScreenPatchKt {
|
||||
public static final fun getDisableSuggestedVideoEndScreenPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -1532,6 +1555,10 @@ public final class app/revanced/patches/youtube/misc/navigation/NavigationBarHoo
|
||||
public static final fun setHookNavigationButtonCreated (Lkotlin/jvm/functions/Function1;)V
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/misc/playercontrols/PlayerControlsOverlayVisibilityPatchKt {
|
||||
public static final fun getPlayerControlsOverlayVisibilityPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatchKt {
|
||||
public static final fun getAddBottomControl ()Lkotlin/jvm/functions/Function1;
|
||||
public static final fun getPlayerControlsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
@@ -1660,6 +1687,10 @@ public final class app/revanced/patches/youtube/video/quality/RememberVideoQuali
|
||||
public static final fun getRememberVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/video/quality/VideoQualityDialogButtonPatchKt {
|
||||
public static final fun getVideoQualityDialogButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/video/quality/VideoQualityPatchKt {
|
||||
public static final fun getVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package app.revanced.patches.backdrops.misc.pro
|
||||
import app.revanced.patcher.fingerprint
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
|
||||
@Deprecated("Fingerprint no longer resolves and will soon be deleted.")
|
||||
internal val proUnlockFingerprint = fingerprint {
|
||||
opcodes(
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
|
||||
@@ -6,9 +6,8 @@ import app.revanced.patcher.patch.bytecodePatch
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
|
||||
@Suppress("unused")
|
||||
val proUnlockPatch = bytecodePatch(
|
||||
name = "Pro unlock",
|
||||
) {
|
||||
@Deprecated("This patch no longer works and will soon be deleted.")
|
||||
val proUnlockPatch = bytecodePatch{
|
||||
compatibleWith("com.backdrops.wallpapers")
|
||||
|
||||
execute {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
|
||||
package app.revanced.patches.instagram.hide.navigation
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
|
||||
internal val tabCreateButtonsLoopStartFingerprint = fingerprint {
|
||||
returns("V")
|
||||
strings("InstagramMainActivity.createTabButtons")
|
||||
opcodes(
|
||||
//Loop Start
|
||||
Opcode.IF_GE, // Check if index is finished (index, size)
|
||||
//Injection
|
||||
Opcode.INVOKE_INTERFACE,
|
||||
Opcode.MOVE_RESULT_OBJECT
|
||||
)
|
||||
}
|
||||
|
||||
internal val tabCreateButtonsLoopEndFingerprint = fingerprint {
|
||||
returns("V")
|
||||
strings("InstagramMainActivity.createTabButtons")
|
||||
opcodes(
|
||||
Opcode.IPUT_OBJECT,
|
||||
// Injection Jump
|
||||
Opcode.ADD_INT_LIT8, //Increase Index
|
||||
Opcode.GOTO_16 // Jump to loopStart
|
||||
// LoopEnd
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package app.revanced.patches.instagram.hide.navigation
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.booleanOption
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.smali.ExternalLabel
|
||||
import app.revanced.util.findFreeRegister
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||
import java.util.logging.Logger
|
||||
|
||||
@Suppress("unused")
|
||||
val hideNavigationButtonsPatch = bytecodePatch(
|
||||
name = "Hide navigation buttons",
|
||||
description = "Hides navigation bar buttons, such as the Reels and Create button.",
|
||||
use = false
|
||||
) {
|
||||
compatibleWith("com.instagram.android")
|
||||
|
||||
val hideReels by booleanOption(
|
||||
key = "hideReels",
|
||||
default = true,
|
||||
title = "Hide Reels",
|
||||
description = "Permanently hides the Reels button."
|
||||
)
|
||||
|
||||
val hideCreate by booleanOption(
|
||||
key = "hideCreate",
|
||||
default = true,
|
||||
title = "Hide Create",
|
||||
description = "Permanently hides the Create button."
|
||||
)
|
||||
|
||||
execute {
|
||||
if (!hideReels!! && !hideCreate!!) {
|
||||
return@execute Logger.getLogger(this::class.java.name).warning(
|
||||
"No hide navigation buttons options are enabled. No changes made."
|
||||
)
|
||||
}
|
||||
|
||||
tabCreateButtonsLoopStartFingerprint.method.apply {
|
||||
// Check the current loop index, and skip over adding the
|
||||
// navigation button view if the index matches a given button.
|
||||
|
||||
val startIndex = tabCreateButtonsLoopStartFingerprint.patternMatch!!.startIndex
|
||||
val endIndex = tabCreateButtonsLoopEndFingerprint.patternMatch!!.endIndex
|
||||
val insertIndex = startIndex + 1
|
||||
val loopIndexRegister = getInstruction<TwoRegisterInstruction>(startIndex).registerA
|
||||
val freeRegister = findFreeRegister(insertIndex, loopIndexRegister)
|
||||
val instruction = getInstruction(endIndex - 1)
|
||||
|
||||
var instructions = buildString {
|
||||
if (hideCreate!!) {
|
||||
appendLine(
|
||||
"""
|
||||
const v$freeRegister, 0x2
|
||||
if-eq v$freeRegister, v$loopIndexRegister, :skipAddView
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
if (hideReels!!) {
|
||||
appendLine(
|
||||
"""
|
||||
const v$freeRegister, 0x3
|
||||
if-eq v$freeRegister, v$loopIndexRegister, :skipAddView
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
addInstructionsWithLabels(
|
||||
insertIndex,
|
||||
instructions,
|
||||
ExternalLabel("skipAddView", instruction)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ val signatureCheckPatch = bytecodePatch(
|
||||
name = "Disable signature check",
|
||||
description = "Disables the signature check that causes the app to crash on startup."
|
||||
) {
|
||||
compatibleWith("com.instagram.android"("378.0.0.52.68"))
|
||||
compatibleWith("com.instagram.android")
|
||||
|
||||
execute {
|
||||
isValidSignatureMethodFingerprint
|
||||
|
||||
@@ -5,6 +5,5 @@ import app.revanced.patcher.fingerprint
|
||||
internal val isFacebookButtonEnabledFingerprint = fingerprint {
|
||||
parameters()
|
||||
returns("Z")
|
||||
strings("com.facebook.messaging.inbox.tab.plugins.core.tabtoolbarbutton." +
|
||||
"facebookbutton.facebooktoolbarbutton.FacebookButtonTabButtonImplementation")
|
||||
strings("FacebookButtonTabButtonImplementation")
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package app.revanced.patches.music.layout.compactheader
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.util.findFreeRegister
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
|
||||
@Suppress("unused")
|
||||
@@ -17,13 +18,14 @@ val hideCategoryBar = bytecodePatch(
|
||||
constructCategoryBarFingerprint.method.apply {
|
||||
val insertIndex = constructCategoryBarFingerprint.patternMatch!!.startIndex
|
||||
val register = getInstruction<OneRegisterInstruction>(insertIndex - 1).registerA
|
||||
val freeRegister = findFreeRegister(insertIndex, register)
|
||||
|
||||
addInstructions(
|
||||
insertIndex,
|
||||
"""
|
||||
const/16 v2, 0x8
|
||||
invoke-virtual {v$register, v2}, Landroid/view/View;->setVisibility(I)V
|
||||
""",
|
||||
const/16 v$freeRegister, 0x8
|
||||
invoke-virtual { v$register, v$freeRegister }, Landroid/view/View;->setVisibility(I)V
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package app.revanced.patches.nfctoolsse.misc.pro
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
@Deprecated("This patch no longer works and will soon be deleted.")
|
||||
internal val isLicenseRegisteredFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC)
|
||||
returns("Z")
|
||||
|
||||
@@ -4,9 +4,8 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
|
||||
@Suppress("unused")
|
||||
val unlockProPatch = bytecodePatch(
|
||||
name = "Unlock pro",
|
||||
) {
|
||||
@Deprecated("This patch no longer works and will soon be deleted.")
|
||||
val unlockProPatch = bytecodePatch{
|
||||
compatibleWith("com.wakdev.apps.nfctools.se")
|
||||
|
||||
execute {
|
||||
|
||||
@@ -14,7 +14,7 @@ val hideAdsPatch = bytecodePatch(
|
||||
name = "Hide ads",
|
||||
description = "Hide ads and sponsored articles in list pages and remove pre-roll ads on videos.",
|
||||
) {
|
||||
compatibleWith("nl.sanomamedia.android.nu"("11.3.0"))
|
||||
compatibleWith("nl.sanomamedia.android.nu")
|
||||
|
||||
dependsOn(sharedExtensionPatch("nunl", mainActivityOnCreateHook))
|
||||
|
||||
|
||||
@@ -4,6 +4,6 @@ import app.revanced.patches.shared.misc.extension.extensionHook
|
||||
|
||||
internal val mainActivityOnCreateHook = extensionHook {
|
||||
custom { method, classDef ->
|
||||
classDef.type == "Lnl/sanomamedia/android/nu/main/NUMainActivity;" && method.name == "onCreate"
|
||||
classDef.endsWith("/NUApplication;") && method.name == "onCreate"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
package app.revanced.patches.pixiv.ads
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Suppress("unused")
|
||||
val hideAdsPatch = bytecodePatch(
|
||||
name = "Hide ads",
|
||||
) {
|
||||
compatibleWith("jp.pxv.android")
|
||||
compatibleWith("jp.pxv.android"("6.141.1"))
|
||||
|
||||
execute {
|
||||
shouldShowAdsFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
const/4 v0, 0x0
|
||||
return v0
|
||||
""",
|
||||
)
|
||||
shouldShowAdsFingerprint.method.returnEarly(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ val removeFreeAccountsLimitPatch = resourcePatch(
|
||||
name = "Remove free accounts limit",
|
||||
description = "Removes the limit for maximum free accounts logged in.",
|
||||
) {
|
||||
compatibleWith("ch.protonmail.android")
|
||||
compatibleWith("ch.protonmail.android"("4.15.0"))
|
||||
|
||||
execute {
|
||||
document("res/values/integers.xml").use { document ->
|
||||
|
||||
@@ -10,7 +10,7 @@ val removeSentFromSignaturePatch = resourcePatch(
|
||||
name = "Remove 'Sent from' signature",
|
||||
description = "Removes the 'Sent from Proton Mail mobile' signature from emails.",
|
||||
) {
|
||||
compatibleWith("ch.protonmail.android")
|
||||
compatibleWith("ch.protonmail.android"("4.15.0"))
|
||||
|
||||
execute {
|
||||
val stringResourceFiles = mutableListOf<File>()
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package app.revanced.patches.reddit.customclients
|
||||
|
||||
import app.revanced.patcher.patch.BytecodePatchBuilder
|
||||
import app.revanced.patcher.patch.Patch
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
|
||||
const val INSTALL_NEW_CLIENT_METHOD = "install(Lokhttp3/OkHttpClient${'$'}Builder;)Lokhttp3/OkHttpClient;"
|
||||
const val CREATE_NEW_CLIENT_METHOD = "createClient()Lokhttp3/OkHttpClient;"
|
||||
|
||||
fun fixRedgifsApiPatch(
|
||||
extensionPatch: Patch<*>,
|
||||
block: BytecodePatchBuilder.() -> Unit = {},
|
||||
) = bytecodePatch(name = "Fix Redgifs API") {
|
||||
dependsOn(extensionPatch)
|
||||
|
||||
block()
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package app.revanced.patches.reddit.customclients.boostforreddit.fix.redgifs
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
|
||||
internal val createOkHttpClientFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PRIVATE)
|
||||
opcodes(
|
||||
Opcode.NEW_INSTANCE,
|
||||
Opcode.INVOKE_DIRECT,
|
||||
Opcode.NEW_INSTANCE,
|
||||
Opcode.INVOKE_DIRECT,
|
||||
Opcode.NEW_INSTANCE,
|
||||
Opcode.INVOKE_DIRECT,
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
Opcode.MOVE_RESULT_OBJECT
|
||||
)
|
||||
custom { _, classDef -> classDef.sourceFile == "RedGifsAPIv2.java" }
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package app.revanced.patches.reddit.customclients.boostforreddit.fix.redgifs
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patches.reddit.customclients.CREATE_NEW_CLIENT_METHOD
|
||||
import app.revanced.patches.reddit.customclients.boostforreddit.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.reddit.customclients.fixRedgifsApiPatch
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/boostforreddit/FixRedgifsApiPatch;"
|
||||
|
||||
@Suppress("unused")
|
||||
val fixRedgifsApi = fixRedgifsApiPatch(
|
||||
extensionPatch = sharedExtensionPatch
|
||||
) {
|
||||
compatibleWith("com.rubenmayayo.reddit")
|
||||
|
||||
execute {
|
||||
// region Patch Redgifs OkHttp3 client.
|
||||
|
||||
createOkHttpClientFingerprint.method.apply {
|
||||
val index = indexOfFirstInstructionOrThrow {
|
||||
val reference = getReference<MethodReference>()
|
||||
reference?.name == "build" && reference.definingClass == "Lokhttp3/OkHttpClient\$Builder;"
|
||||
}
|
||||
replaceInstruction(
|
||||
index,
|
||||
"""
|
||||
invoke-static { }, ${EXTENSION_CLASS_DESCRIPTOR}->$CREATE_NEW_CLIENT_METHOD
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package app.revanced.patches.reddit.customclients.sync.syncforreddit.fix.redgifs
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.util.indexOfFirstInstruction
|
||||
import app.revanced.util.writeRegister
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction11n
|
||||
|
||||
|
||||
internal val createOkHttpClientFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC)
|
||||
returns("V")
|
||||
parameters()
|
||||
custom { method, classDef ->
|
||||
// There are four functions (each creating a client) defined in this file with very similar fingerprints.
|
||||
// We're looking for the one that only creates one object (the builder) and sets client options true
|
||||
// (thus never reloading the register with a 0).
|
||||
classDef.sourceFile == "OkHttpHelper.java" &&
|
||||
method.instructions.count { it.opcode == Opcode.NEW_INSTANCE } == 1 &&
|
||||
method.indexOfFirstInstruction {
|
||||
opcode == Opcode.CONST_4 && writeRegister == 1 && (this as Instruction11n).narrowLiteral == 0
|
||||
} == -1
|
||||
}
|
||||
}
|
||||
|
||||
internal val getDefaultUserAgentFingerprint = fingerprint {
|
||||
custom { method, classDef ->
|
||||
method.name == "getDefaultUserAgent" && classDef.type == EXTENSION_CLASS_DESCRIPTOR
|
||||
}
|
||||
}
|
||||
|
||||
internal val getOriginalUserAgentFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||
returns("Ljava/lang/String;")
|
||||
parameters()
|
||||
custom { _, classDef -> classDef.sourceFile == "AccountSingleton.java" }
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package app.revanced.patches.reddit.customclients.sync.syncforreddit.fix.redgifs
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patches.reddit.customclients.INSTALL_NEW_CLIENT_METHOD
|
||||
import app.revanced.patches.reddit.customclients.fixRedgifsApiPatch
|
||||
import app.revanced.patches.reddit.customclients.sync.syncforreddit.extension.sharedExtensionPatch
|
||||
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
|
||||
|
||||
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/syncforreddit/FixRedgifsApiPatch;"
|
||||
|
||||
@Suppress("unused")
|
||||
val fixRedgifsApi = fixRedgifsApiPatch(
|
||||
extensionPatch = sharedExtensionPatch
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.laurencedawson.reddit_sync",
|
||||
"com.laurencedawson.reddit_sync.pro",
|
||||
"com.laurencedawson.reddit_sync.dev",
|
||||
)
|
||||
|
||||
execute {
|
||||
// region Patch Redgifs OkHttp3 client.
|
||||
|
||||
createOkHttpClientFingerprint.method.apply {
|
||||
val index = indexOfFirstInstructionOrThrow {
|
||||
val reference = getReference<MethodReference>()
|
||||
reference?.name == "build" && reference.definingClass == "Lokhttp3/OkHttpClient\$Builder;"
|
||||
}
|
||||
val register = getInstruction<FiveRegisterInstruction>(index).registerC
|
||||
replaceInstruction(
|
||||
index,
|
||||
"""
|
||||
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->$INSTALL_NEW_CLIENT_METHOD
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
getDefaultUserAgentFingerprint.method.apply {
|
||||
addInstructions(
|
||||
0,
|
||||
"""
|
||||
invoke-static { }, ${getOriginalUserAgentFingerprint.method}
|
||||
move-result-object v0
|
||||
return-object v0
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
}
|
||||
@@ -49,8 +49,8 @@ internal val contextMenuViewModelConstructorFingerprint = fingerprint {
|
||||
/**
|
||||
* Used to find the interface name of a context menu item.
|
||||
*/
|
||||
internal val removeAdsContextMenuItemClassFingerprint = fingerprint {
|
||||
strings("remove_ads_item", "ui_navigate")
|
||||
internal val removeItemFromPlaylistContextMenuItemClassFingerprint = fingerprint {
|
||||
strings("spotify:playlist:", "REMOVE_TRACK")
|
||||
}
|
||||
|
||||
internal const val CONTEXT_MENU_ITEM_CLASS_DESCRIPTOR_PLACEHOLDER = "Lapp/revanced/ContextMenuItemPlaceholder;"
|
||||
|
||||
@@ -21,7 +21,7 @@ val audioAdsPatch = bytecodePatch(
|
||||
addResourcesPatch,
|
||||
)
|
||||
|
||||
compatibleWith("tv.twitch.android.app")
|
||||
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
|
||||
|
||||
execute {
|
||||
addResources("twitch", "ad.audio.audioAdsPatch")
|
||||
|
||||
@@ -19,7 +19,7 @@ val embeddedAdsPatch = bytecodePatch(
|
||||
settingsPatch,
|
||||
)
|
||||
|
||||
compatibleWith("tv.twitch.android.app")
|
||||
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
|
||||
|
||||
execute {
|
||||
addResources("twitch", "ad.embedded.embeddedAdsPatch")
|
||||
|
||||
@@ -155,7 +155,5 @@ val videoAdsPatch = bytecodePatch(
|
||||
},
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
"tv.twitch.android.app",
|
||||
)
|
||||
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ val showDeletedMessagesPatch = bytecodePatch(
|
||||
addResourcesPatch,
|
||||
)
|
||||
|
||||
compatibleWith("tv.twitch.android.app")
|
||||
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
|
||||
|
||||
fun createSpoilerConditionInstructions(register: String = "v0") = """
|
||||
invoke-static {}, Lapp/revanced/extension/twitch/patches/ShowDeletedMessagesPatch;->shouldUseSpoiler()Z
|
||||
|
||||
@@ -20,7 +20,7 @@ val autoClaimChannelPointsPatch = bytecodePatch(
|
||||
addResourcesPatch,
|
||||
)
|
||||
|
||||
compatibleWith("tv.twitch.android.app")
|
||||
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
|
||||
|
||||
execute {
|
||||
addResources("twitch", "chat.autoclaim.autoClaimChannelPointsPatch")
|
||||
|
||||
@@ -20,7 +20,7 @@ val debugModePatch = bytecodePatch(
|
||||
addResourcesPatch,
|
||||
)
|
||||
|
||||
compatibleWith("tv.twitch.android.app")
|
||||
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
|
||||
|
||||
execute {
|
||||
addResources("twitch", "debug.debugModePatch")
|
||||
|
||||
@@ -48,7 +48,7 @@ val settingsPatch = bytecodePatch(
|
||||
settingsPatch(preferences = preferences),
|
||||
)
|
||||
|
||||
compatibleWith("tv.twitch.android.app")
|
||||
compatibleWith("tv.twitch.android.app"("16.9.1"))
|
||||
|
||||
execute {
|
||||
addResources("twitch", "misc.settings.settingsPatch")
|
||||
|
||||
@@ -42,7 +42,9 @@ val hideButtonsPatch = resourcePatch(
|
||||
SwitchPreference("revanced_hide_ask_button"),
|
||||
SwitchPreference("revanced_hide_clip_button"),
|
||||
SwitchPreference("revanced_hide_download_button"),
|
||||
SwitchPreference("revanced_hide_hype_button"),
|
||||
SwitchPreference("revanced_hide_like_dislike_button"),
|
||||
SwitchPreference("revanced_hide_promote_button"),
|
||||
SwitchPreference("revanced_hide_remix_button"),
|
||||
SwitchPreference("revanced_hide_report_button"),
|
||||
SwitchPreference("revanced_hide_save_button"),
|
||||
|
||||
@@ -57,6 +57,7 @@ val hidePlayerFlyoutMenuPatch = bytecodePatch(
|
||||
),
|
||||
SwitchPreference("revanced_hide_player_flyout_watch_in_vr"),
|
||||
SwitchPreference("revanced_hide_player_flyout_sleep_timer"),
|
||||
SwitchPreference("revanced_hide_player_flyout_video_quality"),
|
||||
SwitchPreference("revanced_hide_player_flyout_video_quality_footer"),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package app.revanced.patches.youtube.layout.hide.signintotvpopup
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.all.misc.resources.addResources
|
||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||
import app.revanced.patches.shared.misc.mapping.get
|
||||
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||
import app.revanced.patches.shared.misc.mapping.resourceMappings
|
||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||
|
||||
internal var mdx_seamless_tv_sign_in_drawer_fragment_title_id = -1L
|
||||
private set
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/youtube/patches/DisableSignInToTvPopupPatch;"
|
||||
|
||||
val disableSignInToTvPopupPatch = bytecodePatch(
|
||||
name = "Disable sign in to TV popup",
|
||||
description = "Adds an option to disable the popup asking to sign into a TV on the same local network.",
|
||||
) {
|
||||
dependsOn(
|
||||
settingsPatch,
|
||||
sharedExtensionPatch,
|
||||
addResourcesPatch,
|
||||
resourceMappingPatch
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
"com.google.android.youtube"(
|
||||
"19.34.42",
|
||||
"19.43.41",
|
||||
"19.47.53",
|
||||
"20.07.39",
|
||||
"20.12.46",
|
||||
"20.13.41",
|
||||
)
|
||||
)
|
||||
|
||||
execute {
|
||||
addResources("youtube", "layout.hide.signintotv.disableSignInToTvPopupPatch")
|
||||
|
||||
PreferenceScreen.MISC.addPreferences(
|
||||
SwitchPreference("revanced_disable_signin_to_tv_popup"),
|
||||
)
|
||||
|
||||
mdx_seamless_tv_sign_in_drawer_fragment_title_id = resourceMappings[
|
||||
"string",
|
||||
"mdx_seamless_tv_sign_in_drawer_fragment_title",
|
||||
]
|
||||
|
||||
signInToTvPopupFingerprint.method.addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->disableSignInToTvPopup()Z
|
||||
move-result v0
|
||||
if-eqz v0, :allow_sign_in_popup
|
||||
const/4 v0, 0x0
|
||||
return v0
|
||||
:allow_sign_in_popup
|
||||
nop
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package app.revanced.patches.youtube.layout.hide.signintotvpopup
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.util.literal
|
||||
|
||||
internal val signInToTvPopupFingerprint = fingerprint {
|
||||
returns("Z")
|
||||
parameters("Ljava/lang/String;", "Z", "L")
|
||||
literal {
|
||||
mdx_seamless_tv_sign_in_drawer_fragment_title_id
|
||||
}
|
||||
}
|
||||
@@ -202,7 +202,7 @@ val sponsorBlockPatch = bytecodePatch(
|
||||
"""
|
||||
invoke-static { v$register }, $EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->appendTimeWithoutSegments(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v$register
|
||||
""",
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@@ -252,15 +252,5 @@ val sponsorBlockPatch = bytecodePatch(
|
||||
}
|
||||
} ?: throw PatchException("Could not find the method which contains the replaceMeWith* strings")
|
||||
}
|
||||
|
||||
// The vote and create segment buttons automatically change their visibility when appropriate,
|
||||
// but if buttons are showing when the end of the video is reached then they will not automatically hide.
|
||||
// Add a hook to forcefully hide when the end of the video is reached.
|
||||
autoRepeatFingerprint.match(autoRepeatParentFingerprint.originalClassDef).method.addInstruction(
|
||||
0,
|
||||
"invoke-static {}, $EXTENSION_SPONSORBLOCK_VIEW_CONTROLLER_CLASS_DESCRIPTOR->endOfVideoReached()V",
|
||||
)
|
||||
|
||||
// TODO: Channel whitelisting.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ val lithoFilterPatch = bytecodePatch(
|
||||
move-object/from16 v$freeRegister, p2
|
||||
iget-object v$identifierRegister, v$freeRegister, $conversionContextIdentifierField
|
||||
iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField
|
||||
invoke-static { v$identifierRegister, v$pathRegister }, $EXTENSION_CLASS_DESCRIPTOR->shouldFilter(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
|
||||
invoke-static { v$identifierRegister, v$pathRegister }, $EXTENSION_CLASS_DESCRIPTOR->isFiltered(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
|
||||
move-result v$freeRegister
|
||||
if-eqz v$freeRegister, :unfiltered
|
||||
|
||||
|
||||
@@ -2,14 +2,63 @@ package app.revanced.patches.youtube.misc.playercontrols
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.util.containsLiteralInstruction
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstruction
|
||||
import app.revanced.util.indexOfFirstInstructionReversed
|
||||
import app.revanced.util.literal
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
|
||||
internal fun indexOfFocusableInTouchModeInstruction(method: Method) =
|
||||
method.indexOfFirstInstruction {
|
||||
getReference<MethodReference>()?.name == "setFocusableInTouchMode"
|
||||
}
|
||||
|
||||
internal fun indexOfTranslationInstruction(method: Method) =
|
||||
method.indexOfFirstInstructionReversed {
|
||||
getReference<MethodReference>()?.name == "setTranslationY"
|
||||
}
|
||||
|
||||
internal val playerControlsVisibilityEntityModelFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC)
|
||||
returns("L")
|
||||
parameters()
|
||||
opcodes(
|
||||
Opcode.IGET,
|
||||
Opcode.INVOKE_STATIC
|
||||
)
|
||||
custom { method, _ ->
|
||||
method.name == "getPlayerControlsVisibility"
|
||||
}
|
||||
}
|
||||
|
||||
internal val youtubeControlsOverlayFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL)
|
||||
returns("V")
|
||||
parameters()
|
||||
custom { method, _ ->
|
||||
indexOfFocusableInTouchModeInstruction(method) >= 0 &&
|
||||
method.containsLiteralInstruction(inset_overlay_view_layout_id) &&
|
||||
method.containsLiteralInstruction(scrim_overlay_id)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
internal val motionEventFingerprint = fingerprint {
|
||||
returns("V")
|
||||
parameters("Landroid/view/MotionEvent;")
|
||||
custom { method, _ ->
|
||||
indexOfTranslationInstruction(method) >= 0
|
||||
}
|
||||
}
|
||||
|
||||
internal val playerTopControlsInflateFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returns("V")
|
||||
parameters()
|
||||
literal { controlsLayoutStub }
|
||||
literal { controls_layout_stub_id }
|
||||
}
|
||||
|
||||
internal val playerControlsExtensionHookListenersExistFingerprint = fingerprint {
|
||||
@@ -35,7 +84,7 @@ internal val playerControlsExtensionHookFingerprint = fingerprint {
|
||||
internal val playerBottomControlsInflateFingerprint = fingerprint {
|
||||
returns("Ljava/lang/Object;")
|
||||
parameters()
|
||||
literal { bottomUiContainerResourceId }
|
||||
literal { bottom_ui_container_stub_id }
|
||||
}
|
||||
|
||||
internal val overlayViewInflateFingerprint = fingerprint {
|
||||
@@ -43,8 +92,8 @@ internal val overlayViewInflateFingerprint = fingerprint {
|
||||
returns("V")
|
||||
parameters("Landroid/view/View;")
|
||||
custom { methodDef, _ ->
|
||||
methodDef.containsLiteralInstruction(fullscreenButton) &&
|
||||
methodDef.containsLiteralInstruction(heatseekerViewstub)
|
||||
methodDef.containsLiteralInstruction(fullscreen_button_id) &&
|
||||
methodDef.containsLiteralInstruction(heatseeker_viewstub_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package app.revanced.patches.youtube.misc.playercontrols
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||
|
||||
private const val EXTENSION_PLAYER_CONTROLS_VISIBILITY_HOOK_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/youtube/patches/PlayerControlsVisibilityHookPatch;"
|
||||
|
||||
val PlayerControlsOverlayVisibilityPatch = bytecodePatch {
|
||||
dependsOn(sharedExtensionPatch)
|
||||
|
||||
execute {
|
||||
playerControlsVisibilityEntityModelFingerprint.let {
|
||||
it.method.apply {
|
||||
val startIndex = it.patternMatch!!.startIndex
|
||||
val iGetReference = getInstruction<ReferenceInstruction>(startIndex).reference
|
||||
val staticReference = getInstruction<ReferenceInstruction>(startIndex + 1).reference
|
||||
|
||||
it.classDef.methods.find { method -> method.name == "<init>" }?.apply {
|
||||
val targetIndex = indexOfFirstInstructionOrThrow(Opcode.IPUT_OBJECT)
|
||||
val targetRegister = getInstruction<TwoRegisterInstruction>(targetIndex).registerA
|
||||
|
||||
addInstructions(
|
||||
targetIndex + 1,
|
||||
"""
|
||||
iget v$targetRegister, v$targetRegister, $iGetReference
|
||||
invoke-static { v$targetRegister }, $staticReference
|
||||
move-result-object v$targetRegister
|
||||
invoke-static { v$targetRegister }, $EXTENSION_PLAYER_CONTROLS_VISIBILITY_HOOK_CLASS_DESCRIPTOR->setPlayerControlsVisibility(Ljava/lang/Enum;)V
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.revanced.patches.youtube.misc.playercontrols
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
@@ -40,13 +39,17 @@ internal lateinit var addTopControl: (String) -> Unit
|
||||
lateinit var addBottomControl: (String) -> Unit
|
||||
private set
|
||||
|
||||
internal var bottomUiContainerResourceId = -1L
|
||||
internal var bottom_ui_container_stub_id = -1L
|
||||
private set
|
||||
internal var controlsLayoutStub = -1L
|
||||
internal var controls_layout_stub_id = -1L
|
||||
private set
|
||||
internal var heatseekerViewstub = -1L
|
||||
internal var heatseeker_viewstub_id = -1L
|
||||
private set
|
||||
internal var fullscreenButton = -1L
|
||||
internal var fullscreen_button_id = -1L
|
||||
private set
|
||||
internal var inset_overlay_view_layout_id = -1L
|
||||
private set
|
||||
internal var scrim_overlay_id = -1L
|
||||
private set
|
||||
|
||||
val playerControlsResourcePatch = resourcePatch {
|
||||
@@ -65,10 +68,12 @@ val playerControlsResourcePatch = resourcePatch {
|
||||
execute {
|
||||
val targetResourceName = "youtube_controls_bottom_ui_container.xml"
|
||||
|
||||
bottomUiContainerResourceId = resourceMappings["id", "bottom_ui_container_stub"]
|
||||
controlsLayoutStub = resourceMappings["id", "controls_layout_stub"]
|
||||
heatseekerViewstub = resourceMappings["id", "heatseeker_viewstub"]
|
||||
fullscreenButton = resourceMappings["id", "fullscreen_button"]
|
||||
bottom_ui_container_stub_id = resourceMappings["id", "bottom_ui_container_stub"]
|
||||
controls_layout_stub_id = resourceMappings["id", "controls_layout_stub"]
|
||||
heatseeker_viewstub_id = resourceMappings["id", "heatseeker_viewstub"]
|
||||
fullscreen_button_id = resourceMappings["id", "fullscreen_button"]
|
||||
inset_overlay_view_layout_id = resourceMappings["id", "inset_overlay_view_layout"]
|
||||
scrim_overlay_id = resourceMappings["id", "scrim_overlay"]
|
||||
|
||||
bottomTargetDocument = document("res/layout/$targetResourceName")
|
||||
|
||||
@@ -198,6 +203,13 @@ fun injectVisibilityCheckCall(descriptor: String) {
|
||||
visibilityImmediateInsertIndex++,
|
||||
"invoke-static { p0 }, $descriptor->setVisibilityImmediate(Z)V",
|
||||
)
|
||||
|
||||
// Patch works without this hook, but it is needed to use the correct fade out animation
|
||||
// duration when tapping the overlay to dismiss.
|
||||
visibilityNegatedImmediateMethod.addInstruction(
|
||||
visibilityNegatedImmediateInsertIndex++,
|
||||
"invoke-static { }, $descriptor->setVisibilityNegatedImmediate()V",
|
||||
)
|
||||
}
|
||||
|
||||
internal const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
@@ -220,12 +232,16 @@ private lateinit var visibilityImmediateCallbacksExistMethod : MutableMethod
|
||||
private lateinit var visibilityImmediateMethod: MutableMethod
|
||||
private var visibilityImmediateInsertIndex: Int = 0
|
||||
|
||||
private lateinit var visibilityNegatedImmediateMethod: MutableMethod
|
||||
private var visibilityNegatedImmediateInsertIndex: Int = 0
|
||||
|
||||
val playerControlsPatch = bytecodePatch(
|
||||
description = "Manages the code for the player controls of the YouTube player.",
|
||||
) {
|
||||
dependsOn(
|
||||
playerControlsResourcePatch,
|
||||
sharedExtensionPatch,
|
||||
PlayerControlsOverlayVisibilityPatch
|
||||
)
|
||||
|
||||
execute {
|
||||
@@ -258,7 +274,7 @@ val playerControlsPatch = bytecodePatch(
|
||||
// Hook the fullscreen close button. Used to fix visibility
|
||||
// when seeking and other situations.
|
||||
overlayViewInflateFingerprint.method.apply {
|
||||
val resourceIndex = indexOfFirstLiteralInstructionReversedOrThrow(fullscreenButton)
|
||||
val resourceIndex = indexOfFirstLiteralInstructionReversedOrThrow(fullscreen_button_id)
|
||||
|
||||
val index = indexOfFirstInstructionOrThrow(resourceIndex) {
|
||||
opcode == Opcode.CHECK_CAST &&
|
||||
@@ -277,12 +293,17 @@ val playerControlsPatch = bytecodePatch(
|
||||
visibilityImmediateCallbacksExistMethod = playerControlsExtensionHookListenersExistFingerprint.method
|
||||
visibilityImmediateMethod = playerControlsExtensionHookFingerprint.method
|
||||
|
||||
motionEventFingerprint.match(youtubeControlsOverlayFingerprint.originalClassDef).method.apply {
|
||||
visibilityNegatedImmediateMethod = this
|
||||
visibilityNegatedImmediateInsertIndex = indexOfTranslationInstruction(this) + 1
|
||||
}
|
||||
|
||||
// A/B test for a slightly different bottom overlay controls,
|
||||
// that uses layout file youtube_video_exploder_controls_bottom_ui_container.xml
|
||||
// The change to support this is simple and only requires adding buttons to both layout files,
|
||||
// but for now force this different layout off since it's still an experimental test.
|
||||
if (is_19_35_or_greater) {
|
||||
playerBottomControlsExploderFeatureFlagFingerprint.method.returnEarly()
|
||||
playerBottomControlsExploderFeatureFlagFingerprint.method.returnLate(false)
|
||||
}
|
||||
|
||||
// A/B test of new top overlay controls. Two different layouts can be used:
|
||||
@@ -299,12 +320,9 @@ val playerControlsPatch = bytecodePatch(
|
||||
val index = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT_OBJECT)
|
||||
val register = getInstruction<OneRegisterInstruction>(index).registerA
|
||||
|
||||
addInstructions(
|
||||
addInstruction(
|
||||
index + 1,
|
||||
"""
|
||||
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getPlayerTopControlsLayoutResourceName(Ljava/lang/String;)Ljava/lang/String;
|
||||
move-result-object v$register
|
||||
""",
|
||||
"const-string v$register, \"default\""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ val recyclerViewTreeHookPatch = bytecodePatch {
|
||||
dependsOn(sharedExtensionPatch)
|
||||
|
||||
execute {
|
||||
|
||||
recyclerViewTreeObserverFingerprint.method.apply {
|
||||
val insertIndex = recyclerViewTreeObserverFingerprint.patternMatch!!.startIndex + 1
|
||||
val recyclerViewParameter = 2
|
||||
|
||||
@@ -121,7 +121,7 @@ internal val subtitleButtonControllerFingerprint = fingerprint {
|
||||
)
|
||||
}
|
||||
|
||||
internal val newVideoQualityChangedFingerprint = fingerprint {
|
||||
internal val videoQualityChangedFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returns("L")
|
||||
parameters("L")
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
package app.revanced.patches.youtube.video.audio
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.util.containsLiteralInstruction
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val streamingModelBuilderFingerprint = fingerprint {
|
||||
internal val formatStreamModelToStringFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returns("L")
|
||||
strings("vprng")
|
||||
returns("Ljava/lang/String;")
|
||||
custom { method, classDef ->
|
||||
method.name == "toString" && classDef.type ==
|
||||
"Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;"
|
||||
}
|
||||
}
|
||||
|
||||
internal val menuItemAudioTrackFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
parameters("L")
|
||||
returns("V")
|
||||
strings("menu_item_audio_track")
|
||||
internal const val AUDIO_STREAM_IGNORE_DEFAULT_FEATURE_FLAG = 45666189L
|
||||
|
||||
internal val selectAudioStreamFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||
returns("L")
|
||||
custom { method, _ ->
|
||||
method.parameters.size > 2 // Method has a large number of parameters and may change.
|
||||
&& method.parameters[1].type == "Lcom/google/android/libraries/youtube/innertube/model/media/PlayerConfigModel;"
|
||||
&& method.containsLiteralInstruction(AUDIO_STREAM_IGNORE_DEFAULT_FEATURE_FLAG)
|
||||
}
|
||||
}
|
||||
|
||||
internal val audioStreamingTypeSelector = fingerprint {
|
||||
accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL)
|
||||
returns("L")
|
||||
strings("raw") // String is not unique
|
||||
}
|
||||
@@ -5,22 +5,22 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||
import app.revanced.patches.all.misc.resources.addResources
|
||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.youtube.misc.playservice.is_20_07_or_greater
|
||||
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
|
||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.findMethodFromToString
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import app.revanced.util.insertLiteralOverride
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableField
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
|
||||
@@ -37,6 +37,7 @@ val forceOriginalAudioPatch = bytecodePatch(
|
||||
sharedExtensionPatch,
|
||||
settingsPatch,
|
||||
addResourcesPatch,
|
||||
versionCheckPatch
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
@@ -60,100 +61,93 @@ val forceOriginalAudioPatch = bytecodePatch(
|
||||
)
|
||||
)
|
||||
|
||||
fun Method.firstFormatStreamingModelCall(
|
||||
returnType: String = "Ljava/lang/String;"
|
||||
): MutableMethod {
|
||||
val audioTrackIdIndex = indexOfFirstInstructionOrThrow {
|
||||
val reference = getReference<MethodReference>()
|
||||
reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;"
|
||||
&& reference.returnType == returnType
|
||||
}
|
||||
|
||||
return navigate(this).to(audioTrackIdIndex).stop()
|
||||
// Disable feature flag that ignores the default track flag
|
||||
// and instead overrides to the user region language.
|
||||
if (is_20_07_or_greater) {
|
||||
selectAudioStreamFingerprint.method.insertLiteralOverride(
|
||||
AUDIO_STREAM_IGNORE_DEFAULT_FEATURE_FLAG,
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->ignoreDefaultAudioStream(Z)Z"
|
||||
)
|
||||
}
|
||||
|
||||
// Accessor methods of FormatStreamModel have no string constants and
|
||||
// opcodes are identical to other methods in the same class,
|
||||
// so must walk from another class that use the methods.
|
||||
val isDefaultMethod = streamingModelBuilderFingerprint.originalMethod.firstFormatStreamingModelCall("Z")
|
||||
val audioTrackIdMethod = menuItemAudioTrackFingerprint.originalMethod.firstFormatStreamingModelCall()
|
||||
val audioTrackDisplayNameMethod = audioStreamingTypeSelector.originalMethod.firstFormatStreamingModelCall()
|
||||
val formatStreamModelClass = proxy(classes.first {
|
||||
it.type == audioTrackIdMethod.definingClass
|
||||
}).mutableClass
|
||||
formatStreamModelToStringFingerprint.let {
|
||||
val isDefaultAudioTrackMethod = it.originalMethod.findMethodFromToString("isDefaultAudioTrack=")
|
||||
val audioTrackDisplayNameMethod = it.originalMethod.findMethodFromToString("audioTrackDisplayName=")
|
||||
val audioTrackIdMethod = it.originalMethod.findMethodFromToString("audioTrackId=")
|
||||
|
||||
formatStreamModelClass.apply {
|
||||
// Add a new field to store the override.
|
||||
val helperFieldName = "isDefaultAudioTrackOverride"
|
||||
fields.add(
|
||||
ImmutableField(
|
||||
type,
|
||||
helperFieldName,
|
||||
"Ljava/lang/Boolean;",
|
||||
// Boolean is a 100% immutable class (all fields are final)
|
||||
// and safe to write to a shared field without volatile/synchronization,
|
||||
// but without volatile the field can show stale data
|
||||
// and the same field is calculated more than once by different threads.
|
||||
AccessFlags.PRIVATE.value or AccessFlags.VOLATILE.value,
|
||||
it.classDef.apply {
|
||||
// Add a new field to store the override.
|
||||
val helperFieldName = "patch_isDefaultAudioTrackOverride"
|
||||
fields.add(
|
||||
ImmutableField(
|
||||
type,
|
||||
helperFieldName,
|
||||
"Ljava/lang/Boolean;",
|
||||
// Boolean is a 100% immutable class (all fields are final)
|
||||
// and safe to write to a shared field without volatile/synchronization,
|
||||
// but without volatile the field can show stale data
|
||||
// and the same field is calculated more than once by different threads.
|
||||
AccessFlags.PRIVATE.value or AccessFlags.VOLATILE.value,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).toMutable()
|
||||
)
|
||||
|
||||
// Add a helper method because the isDefaultAudioTrack() has only 2 registers and 3 are needed.
|
||||
val helperMethodClass = type
|
||||
val helperMethodName = "patch_isDefaultAudioTrack"
|
||||
val helperMethod = ImmutableMethod(
|
||||
helperMethodClass,
|
||||
helperMethodName,
|
||||
listOf(ImmutableMethodParameter("Z", null, null)),
|
||||
"Z",
|
||||
AccessFlags.PRIVATE.value,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
).toMutable()
|
||||
)
|
||||
MutableMethodImplementation(6),
|
||||
).toMutable().apply {
|
||||
addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
iget-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean;
|
||||
if-eqz v0, :call_extension
|
||||
invoke-virtual { v0 }, Ljava/lang/Boolean;->booleanValue()Z
|
||||
move-result v3
|
||||
return v3
|
||||
|
||||
:call_extension
|
||||
invoke-virtual { p0 }, $audioTrackIdMethod
|
||||
move-result-object v1
|
||||
|
||||
invoke-virtual { p0 }, $audioTrackDisplayNameMethod
|
||||
move-result-object v2
|
||||
|
||||
invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->isDefaultAudioStream(ZLjava/lang/String;Ljava/lang/String;)Z
|
||||
move-result v3
|
||||
|
||||
invoke-static { v3 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;
|
||||
move-result-object v0
|
||||
iput-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean;
|
||||
return v3
|
||||
"""
|
||||
)
|
||||
}
|
||||
methods.add(helperMethod)
|
||||
|
||||
// Add a helper method because the isDefaultAudioTrack() has only 2 registers and 3 are needed.
|
||||
val helperMethodClass = type
|
||||
val helperMethodName = "extension_isDefaultAudioTrack"
|
||||
val helperMethod = ImmutableMethod(
|
||||
helperMethodClass,
|
||||
helperMethodName,
|
||||
listOf(ImmutableMethodParameter("Z", null, null)),
|
||||
"Z",
|
||||
AccessFlags.PRIVATE.value,
|
||||
null,
|
||||
null,
|
||||
MutableMethodImplementation(6),
|
||||
).toMutable().apply {
|
||||
addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
iget-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean;
|
||||
if-eqz v0, :call_extension
|
||||
invoke-virtual { v0 }, Ljava/lang/Boolean;->booleanValue()Z
|
||||
move-result v3
|
||||
return v3
|
||||
|
||||
:call_extension
|
||||
invoke-virtual { p0 }, $audioTrackIdMethod
|
||||
move-result-object v1
|
||||
|
||||
invoke-virtual { p0 }, $audioTrackDisplayNameMethod
|
||||
move-result-object v2
|
||||
|
||||
invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->isDefaultAudioStream(ZLjava/lang/String;Ljava/lang/String;)Z
|
||||
move-result v3
|
||||
|
||||
invoke-static { v3 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;
|
||||
move-result-object v0
|
||||
iput-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean;
|
||||
return v3
|
||||
"""
|
||||
)
|
||||
}
|
||||
methods.add(helperMethod)
|
||||
// Modify isDefaultAudioTrack() to call extension helper method.
|
||||
isDefaultAudioTrackMethod.apply {
|
||||
val index = indexOfFirstInstructionOrThrow(Opcode.RETURN)
|
||||
val register = getInstruction<OneRegisterInstruction>(index).registerA
|
||||
|
||||
// Modify isDefaultAudioTrack() to call extension helper method.
|
||||
isDefaultMethod.apply {
|
||||
val index = indexOfFirstInstructionOrThrow(Opcode.RETURN)
|
||||
val register = getInstruction<OneRegisterInstruction>(index).registerA
|
||||
|
||||
addInstructions(
|
||||
index,
|
||||
"""
|
||||
invoke-direct { p0, v$register }, $helperMethodClass->$helperMethodName(Z)Z
|
||||
move-result v$register
|
||||
"""
|
||||
)
|
||||
addInstructions(
|
||||
index,
|
||||
"""
|
||||
invoke-direct { p0, v$register }, $helperMethodClass->$helperMethodName(Z)Z
|
||||
move-result v$register
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
package app.revanced.patches.youtube.video.hdr
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.all.misc.resources.addResources
|
||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||
import app.revanced.patches.all.misc.transformation.transformInstructionsPatch
|
||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||
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 const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
@@ -24,6 +25,31 @@ val disableHdrPatch = bytecodePatch(
|
||||
sharedExtensionPatch,
|
||||
settingsPatch,
|
||||
addResourcesPatch,
|
||||
// Override all calls of `getSupportedHdrTypes`.
|
||||
transformInstructionsPatch(
|
||||
filterMap = filterMap@{ classDef, _, instruction, instructionIndex ->
|
||||
if (classDef.type.startsWith("Lapp/revanced/")) {
|
||||
return@filterMap null
|
||||
}
|
||||
|
||||
val reference = instruction.getReference<MethodReference>()
|
||||
if (reference?.definingClass =="Landroid/view/Display\$HdrCapabilities;"
|
||||
&& reference.name == "getSupportedHdrTypes") {
|
||||
return@filterMap instruction to instructionIndex
|
||||
}
|
||||
return@filterMap null
|
||||
},
|
||||
transform = { method, entry ->
|
||||
val (instruction, index) = entry
|
||||
val register = (instruction as FiveRegisterInstruction).registerC
|
||||
|
||||
method.replaceInstruction(
|
||||
index,
|
||||
"invoke-static/range { v$register .. v$register }, $EXTENSION_CLASS_DESCRIPTOR->" +
|
||||
"disableHdrVideo(Landroid/view/Display\$HdrCapabilities;)[I",
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
@@ -43,29 +69,5 @@ val disableHdrPatch = bytecodePatch(
|
||||
PreferenceScreen.VIDEO.addPreferences(
|
||||
SwitchPreference("revanced_disable_hdr_video")
|
||||
)
|
||||
|
||||
hdrCapabilityFingerprint.let {
|
||||
it.originalMethod.apply {
|
||||
val stringIndex = it.stringMatches!!.first().index
|
||||
val navigateIndex = indexOfFirstInstructionOrThrow(stringIndex) {
|
||||
val reference = getReference<MethodReference>()
|
||||
reference?.parameterTypes == listOf("I", "Landroid/view/Display;") &&
|
||||
reference.returnType == "Z"
|
||||
}
|
||||
|
||||
// Modify the HDR lookup method (Method is in the same class as the fingerprint).
|
||||
navigate(this).to(navigateIndex).stop().addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->disableHDRVideo()Z
|
||||
move-result v0
|
||||
if-nez v0, :useHdr
|
||||
return v0
|
||||
:useHdr
|
||||
nop
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package app.revanced.patches.youtube.video.hdr
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val hdrCapabilityFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
strings(
|
||||
"av1_profile_main_10_hdr_10_plus_supported",
|
||||
"video/av01"
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.revanced.patches.youtube.video.information
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint
|
||||
import app.revanced.patches.youtube.shared.videoQualityChangedFingerprint
|
||||
import app.revanced.util.getReference
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
@@ -110,7 +110,7 @@ internal val seekRelativeFingerprint = fingerprint {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves with the class found in [newVideoQualityChangedFingerprint].
|
||||
* Resolves with the class found in [videoQualityChangedFingerprint].
|
||||
*/
|
||||
internal val playbackSpeedMenuSpeedChangedFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
@@ -133,3 +133,46 @@ internal val playbackSpeedClassFingerprint = fingerprint {
|
||||
)
|
||||
strings("PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT")
|
||||
}
|
||||
|
||||
|
||||
internal const val YOUTUBE_VIDEO_QUALITY_CLASS_TYPE = "Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;"
|
||||
|
||||
internal val videoQualityFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
|
||||
parameters(
|
||||
"I", // Resolution.
|
||||
"Ljava/lang/String;", // Human readable resolution: "480p", "1080p Premium", etc
|
||||
"Z",
|
||||
"L"
|
||||
)
|
||||
custom { _, classDef ->
|
||||
classDef.type == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
|
||||
}
|
||||
}
|
||||
|
||||
internal val videoQualitySetterFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returns("V")
|
||||
parameters("[L", "I", "Z")
|
||||
opcodes(
|
||||
Opcode.IF_EQZ,
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
Opcode.MOVE_RESULT_OBJECT,
|
||||
Opcode.INVOKE_VIRTUAL,
|
||||
Opcode.IPUT_BOOLEAN,
|
||||
)
|
||||
strings("menu_item_video_quality")
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches with the class found in [videoQualitySetterFingerprint].
|
||||
*/
|
||||
internal val setVideoQualityFingerprint = fingerprint {
|
||||
returns("V")
|
||||
parameters("L")
|
||||
opcodes(
|
||||
Opcode.IGET_OBJECT,
|
||||
Opcode.IPUT_OBJECT,
|
||||
Opcode.IGET_OBJECT,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
|
||||
import app.revanced.patcher.util.smali.toInstructions
|
||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint
|
||||
import app.revanced.patches.youtube.shared.videoQualityChangedFingerprint
|
||||
import app.revanced.patches.youtube.video.playerresponse.Hook
|
||||
import app.revanced.patches.youtube.video.playerresponse.addPlayerResponseMethodHook
|
||||
import app.revanced.patches.youtube.video.playerresponse.playerResponseMethodHookPatch
|
||||
@@ -38,7 +38,9 @@ import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/VideoInformation;"
|
||||
private const val EXTENSION_PLAYER_INTERFACE =
|
||||
"Lapp/revanced/extension/youtube/patches/VideoInformation${'$'}PlaybackController;"
|
||||
"Lapp/revanced/extension/youtube/patches/VideoInformation\$PlaybackController;"
|
||||
private const val EXTENSION_VIDEO_QUALITY_MENU_INTERFACE =
|
||||
"Lapp/revanced/extension/youtube/patches/VideoInformation\$VideoQualityMenuInterface;"
|
||||
|
||||
private lateinit var playerInitMethod: MutableMethod
|
||||
private var playerInitInsertIndex = -1
|
||||
@@ -83,7 +85,6 @@ val videoInformationPatch = bytecodePatch(
|
||||
)
|
||||
|
||||
execute {
|
||||
|
||||
playerInitMethod = playerInitFingerprint.classDef.methods.first { MethodUtil.isConstructor(it) }
|
||||
|
||||
// Find the location of the first invoke-direct call and extract the register storing the 'this' object reference.
|
||||
@@ -93,9 +94,6 @@ val videoInformationPatch = bytecodePatch(
|
||||
playerInitInsertRegister = playerInitMethod.getInstruction<FiveRegisterInstruction>(initThisIndex).registerC
|
||||
playerInitInsertIndex = initThisIndex + 1
|
||||
|
||||
// Hook the player controller for use through the extension.
|
||||
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "initialize")
|
||||
|
||||
val seekFingerprintResultMethod = seekFingerprint.match(playerInitFingerprint.originalClassDef).method
|
||||
val seekRelativeFingerprintResultMethod =
|
||||
seekRelativeFingerprint.match(playerInitFingerprint.originalClassDef).method
|
||||
@@ -263,7 +261,7 @@ val videoInformationPatch = bytecodePatch(
|
||||
|
||||
// Handle new playback speed menu.
|
||||
playbackSpeedMenuSpeedChangedFingerprint.match(
|
||||
newVideoQualityChangedFingerprint.originalClassDef,
|
||||
videoQualityChangedFingerprint.originalClassDef,
|
||||
).method.apply {
|
||||
val index = indexOfFirstInstructionOrThrow(Opcode.IGET)
|
||||
|
||||
@@ -272,6 +270,131 @@ val videoInformationPatch = bytecodePatch(
|
||||
speedSelectionValueRegister = getInstruction<TwoRegisterInstruction>(index).registerA
|
||||
}
|
||||
|
||||
videoQualityFingerprint.let {
|
||||
// Fix bad data used by YouTube.
|
||||
it.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
invoke-static { p2, p1 }, $EXTENSION_CLASS_DESCRIPTOR->fixVideoQualityResolution(Ljava/lang/String;I)I
|
||||
move-result p1
|
||||
"""
|
||||
)
|
||||
|
||||
// Add methods to access obfuscated quality fields.
|
||||
it.classDef.apply {
|
||||
methods.add(
|
||||
ImmutableMethod(
|
||||
type,
|
||||
"patch_getQualityName",
|
||||
listOf(),
|
||||
"Ljava/lang/String;",
|
||||
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
|
||||
null,
|
||||
null,
|
||||
MutableMethodImplementation(2),
|
||||
).toMutable().apply {
|
||||
// Only one string field.
|
||||
val qualityNameField = fields.single { field ->
|
||||
field.type == "Ljava/lang/String;"
|
||||
}
|
||||
|
||||
addInstructions(
|
||||
0,
|
||||
"""
|
||||
iget-object v0, p0, $qualityNameField
|
||||
return-object v0
|
||||
"""
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
methods.add(
|
||||
ImmutableMethod(
|
||||
type,
|
||||
"patch_getResolution",
|
||||
listOf(),
|
||||
"I",
|
||||
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
|
||||
null,
|
||||
null,
|
||||
MutableMethodImplementation(2),
|
||||
).toMutable().apply {
|
||||
val resolutionField = fields.single { field ->
|
||||
field.type == "I"
|
||||
}
|
||||
|
||||
addInstructions(
|
||||
0,
|
||||
"""
|
||||
iget v0, p0, $resolutionField
|
||||
return v0
|
||||
"""
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect video quality changes and override the current quality.
|
||||
setVideoQualityFingerprint.match(
|
||||
videoQualitySetterFingerprint.originalClassDef
|
||||
).let { match ->
|
||||
// This instruction refers to the field with the type that contains the setQuality method.
|
||||
val onItemClickListenerClassReference = match.method
|
||||
.getInstruction<ReferenceInstruction>(0).reference
|
||||
val setQualityFieldReference = match.method
|
||||
.getInstruction<ReferenceInstruction>(1).reference as FieldReference
|
||||
|
||||
proxy(
|
||||
classes.find { classDef ->
|
||||
classDef.type == setQualityFieldReference.type
|
||||
}!!
|
||||
).mutableClass.apply {
|
||||
// Add interface and helper methods to allow extension code to call obfuscated methods.
|
||||
interfaces.add(EXTENSION_VIDEO_QUALITY_MENU_INTERFACE)
|
||||
|
||||
methods.add(
|
||||
ImmutableMethod(
|
||||
type,
|
||||
"patch_setQuality",
|
||||
listOf(
|
||||
ImmutableMethodParameter(YOUTUBE_VIDEO_QUALITY_CLASS_TYPE, null, null)
|
||||
),
|
||||
"V",
|
||||
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
|
||||
null,
|
||||
null,
|
||||
MutableMethodImplementation(2),
|
||||
).toMutable().apply {
|
||||
val setQualityMenuIndexMethod = methods.single { method ->
|
||||
method.parameterTypes.firstOrNull() == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
|
||||
}
|
||||
|
||||
addInstructions(
|
||||
0,
|
||||
"""
|
||||
invoke-virtual { p0, p1 }, $setQualityMenuIndexMethod
|
||||
return-void
|
||||
"""
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
videoQualitySetterFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
# Get object instance to invoke setQuality method.
|
||||
iget-object v0, p0, $onItemClickListenerClassReference
|
||||
iget-object v0, v0, $setQualityFieldReference
|
||||
|
||||
invoke-static { p1, v0, p2 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([$YOUTUBE_VIDEO_QUALITY_CLASS_TYPE${EXTENSION_VIDEO_QUALITY_MENU_INTERFACE}I)I
|
||||
move-result p2
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "initialize")
|
||||
videoSpeedChangedHook(EXTENSION_CLASS_DESCRIPTOR, "videoSpeedChanged")
|
||||
userSelectedPlaybackSpeedHook(EXTENSION_CLASS_DESCRIPTOR, "userSelectedPlaybackSpeed")
|
||||
}
|
||||
@@ -282,8 +405,8 @@ private fun addSeekInterfaceMethods(targetClass: MutableClass, seekToMethod: Met
|
||||
targetClass.interfaces.add(EXTENSION_PLAYER_INTERFACE)
|
||||
|
||||
arrayOf(
|
||||
Triple(seekToMethod, "seekTo", true),
|
||||
Triple(seekToRelativeMethod, "seekToRelative", false),
|
||||
Triple(seekToMethod, "patch_seekTo", true),
|
||||
Triple(seekToRelativeMethod, "patch_seekToRelative", false),
|
||||
).forEach { (method, name, returnsBoolean) ->
|
||||
// Add interface method.
|
||||
// Get enum type for the seek helper method.
|
||||
|
||||
@@ -68,7 +68,6 @@ internal val advancedVideoQualityMenuPatch = bytecodePatch {
|
||||
// region Patch for the old type of the video quality menu.
|
||||
// Used for regular videos when spoofing to old app version,
|
||||
// and for the Shorts quality flyout on newer app versions.
|
||||
|
||||
videoQualityMenuViewInflateFingerprint.let {
|
||||
it.method.apply {
|
||||
val checkCastIndex = it.patternMatch!!.endIndex
|
||||
@@ -77,7 +76,7 @@ internal val advancedVideoQualityMenuPatch = bytecodePatch {
|
||||
addInstruction(
|
||||
checkCastIndex + 1,
|
||||
"invoke-static { v$listViewRegister }, $EXTENSION_CLASS_DESCRIPTOR->" +
|
||||
"showAdvancedVideoQualityMenu(Landroid/widget/ListView;)V",
|
||||
"addVideoQualityListMenuListener(Landroid/widget/ListView;)V",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user