mirror of
https://github.com/ReVanced/revanced-patches.git
synced 2026-01-16 07:43:56 +00:00
Compare commits
73 Commits
v5.25.0-de
...
v5.27.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c09255eaed | ||
|
|
e78d6240ea | ||
|
|
ed0d807d70 | ||
|
|
1c39004350 | ||
|
|
6127f48a9e | ||
|
|
ad416f4aa7 | ||
|
|
adfac8a1f2 | ||
|
|
498488d45b | ||
|
|
f8e31c820a | ||
|
|
826a391591 | ||
|
|
af827e2f1a | ||
|
|
97cd31509e | ||
|
|
c0448dece4 | ||
|
|
f00a95c0d8 | ||
|
|
7a432e5741 | ||
|
|
966a78bd81 | ||
|
|
6aff8e8ca4 | ||
|
|
11aa463fa6 | ||
|
|
bf1b639a2f | ||
|
|
6d5380d44d | ||
|
|
7e1547b5b9 | ||
|
|
c790b45cc5 | ||
|
|
65fc6b43f5 | ||
|
|
2257dd90aa | ||
|
|
4b8499ff2c | ||
|
|
bde3fda972 | ||
|
|
e2e07b5cb2 | ||
|
|
9d10ab6c00 | ||
|
|
d7644152fd | ||
|
|
9be21f4824 | ||
|
|
a2eae0bf04 | ||
|
|
679354b5b3 | ||
|
|
91dec21033 | ||
|
|
1d0c56819b | ||
|
|
4410816c22 | ||
|
|
7e4e48bc9f | ||
|
|
e435b33593 | ||
|
|
bf288b83ae | ||
|
|
7a53580380 | ||
|
|
6439efa2a9 | ||
|
|
bc45433dcb | ||
|
|
8871803e83 | ||
|
|
18954a0285 | ||
|
|
ce5385b28e | ||
|
|
3f4cdf6f83 | ||
|
|
094b4a1ea8 | ||
|
|
a320e35c32 | ||
|
|
5bf5a2d2db | ||
|
|
ff903ba9ac | ||
|
|
1079a54dbe | ||
|
|
2b0e3b4553 | ||
|
|
0265a7791b | ||
|
|
49ae0df224 | ||
|
|
e279491724 | ||
|
|
495260fe2b | ||
|
|
40f069fff7 | ||
|
|
de263c1061 | ||
|
|
bf1f26d8bb | ||
|
|
0ee2ed72d4 | ||
|
|
02373b0bd2 | ||
|
|
97c8e2489d | ||
|
|
08b2b2e104 | ||
|
|
6b386b67d2 | ||
|
|
f8343ae9f6 | ||
|
|
3ba791ac7d | ||
|
|
443b54bf09 | ||
|
|
53587f190d | ||
|
|
83c148addc | ||
|
|
5c8ed05727 | ||
|
|
33833d7a1e | ||
|
|
b712f38017 | ||
|
|
517368eda7 | ||
|
|
2093c0c175 |
223
CHANGELOG.md
223
CHANGELOG.md
@@ -1,3 +1,226 @@
|
||||
# [5.27.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.8...v5.27.0-dev.9) (2025-06-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Messenger:** Add `Hide Facebook button` patch ([#5057](https://github.com/ReVanced/revanced-patches/issues/5057)) ([9175b23](https://github.com/ReVanced/revanced-patches/commit/9175b23e8360d13c8c1c9c8602ca0b5931d13627))
|
||||
|
||||
# [5.27.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.7...v5.27.0-dev.8) (2025-06-09)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `Hide app icon` patch ([#4977](https://github.com/ReVanced/revanced-patches/issues/4977)) ([92311b8](https://github.com/ReVanced/revanced-patches/commit/92311b8e5675f3d4b80ed690d34b699fb847e3cd))
|
||||
|
||||
# [5.27.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.6...v5.27.0-dev.7) (2025-06-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide player overlay buttons:** Add in app setting for "Hide player control buttons background" ([#5147](https://github.com/ReVanced/revanced-patches/issues/5147)) ([dd8afa2](https://github.com/ReVanced/revanced-patches/commit/dd8afa2b07b50be24d764c0f6ddc9e1bbdb91bf1))
|
||||
|
||||
# [5.27.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.5...v5.27.0-dev.6) (2025-06-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide Shorts components:** Add hide 'New posts' button ([ac6b916](https://github.com/ReVanced/revanced-patches/commit/ac6b916c0c212167c4645e2110500dc811b3e54a))
|
||||
|
||||
# [5.27.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.4...v5.27.0-dev.5) (2025-06-08)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Google Photos:** Add `Enable DCIM folders backup control` patch ([#5138](https://github.com/ReVanced/revanced-patches/issues/5138)) ([328d232](https://github.com/ReVanced/revanced-patches/commit/328d232fe77406fa93a14768fc66e7b998506fba))
|
||||
|
||||
# [5.27.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.3...v5.27.0-dev.4) (2025-06-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Bandcamp - Remove play limits:** Support latest app version ([#5124](https://github.com/ReVanced/revanced-patches/issues/5124)) ([863e92b](https://github.com/ReVanced/revanced-patches/commit/863e92b20ad6682f10524e475ed18f879048ecae))
|
||||
|
||||
# [5.27.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.2...v5.27.0-dev.3) (2025-06-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify:** `Hide Create button` patch failing in edge cases ([#5131](https://github.com/ReVanced/revanced-patches/issues/5131)) ([0923600](https://github.com/ReVanced/revanced-patches/commit/0923600739a126329fc62100b500216860d7005e))
|
||||
|
||||
# [5.27.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.27.0-dev.1...v5.27.0-dev.2) (2025-06-06)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Video quality:** Remove non-functional Shorts 144p default quality ([3113cd6](https://github.com/ReVanced/revanced-patches/commit/3113cd6d092952c8657454452f34c0ae85358ec9))
|
||||
|
||||
# [5.27.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.3...v5.27.0-dev.1) (2025-06-05)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Theme:** Add option for black and white splash screen animation ([#5119](https://github.com/ReVanced/revanced-patches/issues/5119)) ([42db0c2](https://github.com/ReVanced/revanced-patches/commit/42db0c2e36fefccdbeaa072edcec48b1e05b6270))
|
||||
|
||||
## [5.26.1-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.2...v5.26.1-dev.3) (2025-06-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify:** Prevent hiding all navigation bar buttons ([#5122](https://github.com/ReVanced/revanced-patches/issues/5122)) ([8afbef0](https://github.com/ReVanced/revanced-patches/commit/8afbef01343c1e3e6e7e4a4cec6319aebfa4b11c))
|
||||
|
||||
## [5.26.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.26.1-dev.1...v5.26.1-dev.2) (2025-06-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide layout components:** Remove broken option 'Hide comments emoji picker' ([#5121](https://github.com/ReVanced/revanced-patches/issues/5121)) ([9a6a639](https://github.com/ReVanced/revanced-patches/commit/9a6a639c4905b00d6dffb0923c839c8e3ae54d0c))
|
||||
|
||||
## [5.26.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.26.0...v5.26.1-dev.1) (2025-06-05)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide Shorts components:** Disable A/B player flags that prevents hiding buttons ([bef0dac](https://github.com/ReVanced/revanced-patches/commit/bef0dacac54caf1ca9511d7bc19b19140ccb4eaf))
|
||||
|
||||
# [5.26.0](https://github.com/ReVanced/revanced-patches/compare/v5.25.0...v5.26.0) (2025-06-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Custom theme:** Apply accent color in more places ([#5039](https://github.com/ReVanced/revanced-patches/issues/5039)) ([9357887](https://github.com/ReVanced/revanced-patches/commit/9357887b6fca7aaf34dfb0163645b6a998e1db76))
|
||||
* **YouTube - Hide Shorts components:** Disable A/B player that prevents hiding buttons ([#5104](https://github.com/ReVanced/revanced-patches/issues/5104)) ([835b7bd](https://github.com/ReVanced/revanced-patches/commit/835b7bd7bd667abd632822c98898972e5124dbb6))
|
||||
* **YouTube:** Support A/B Shorts layout for RYD and component hiding ([#5081](https://github.com/ReVanced/revanced-patches/issues/5081)) ([8ecacaa](https://github.com/ReVanced/revanced-patches/commit/8ecacaad27162d9380014a9a13ac9220b12257b2))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Proton Mail:** Add `Remove free accounts limit` patch ([#4970](https://github.com/ReVanced/revanced-patches/issues/4970)) ([b0440ad](https://github.com/ReVanced/revanced-patches/commit/b0440ad6af0e190e516974ce896dcc54c8d2e122))
|
||||
* **Spotify:** Add `Hide Create button` patch ([#5062](https://github.com/ReVanced/revanced-patches/issues/5062)) ([3201681](https://github.com/ReVanced/revanced-patches/commit/32016819d2adbdfdd5e028941d56feda36d20b00))
|
||||
* **Sync for Reddit:** Add `Fix post thumbnails` patch ([e1ec30c](https://github.com/ReVanced/revanced-patches/commit/e1ec30c5b07560a39d7b8ab293b0c1f39fd59ef2))
|
||||
* **YouTube - Hide Shorts components:** Add option to hide comment panel ([#5102](https://github.com/ReVanced/revanced-patches/issues/5102)) ([22b9bee](https://github.com/ReVanced/revanced-patches/commit/22b9beedd3243a8d6a5635f591b91cdcf307be37))
|
||||
* **YouTube - Playback Speed:** Use modern custom speed dialog ([#5069](https://github.com/ReVanced/revanced-patches/issues/5069)) ([9a1e6ca](https://github.com/ReVanced/revanced-patches/commit/9a1e6ca178d9833ee2c681fb130b9290a4e89cd8))
|
||||
|
||||
# [5.26.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.7...v5.26.0-dev.8) (2025-06-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide Shorts components:** Disable A/B player that prevents hiding buttons ([#5104](https://github.com/ReVanced/revanced-patches/issues/5104)) ([835b7bd](https://github.com/ReVanced/revanced-patches/commit/835b7bd7bd667abd632822c98898972e5124dbb6))
|
||||
|
||||
# [5.26.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.6...v5.26.0-dev.7) (2025-06-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Hide Shorts components:** Add option to hide comment panel ([#5102](https://github.com/ReVanced/revanced-patches/issues/5102)) ([22b9bee](https://github.com/ReVanced/revanced-patches/commit/22b9beedd3243a8d6a5635f591b91cdcf307be37))
|
||||
|
||||
# [5.26.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.5...v5.26.0-dev.6) (2025-06-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Sync for Reddit:** Add `Fix post thumbnails` patch ([e1ec30c](https://github.com/ReVanced/revanced-patches/commit/e1ec30c5b07560a39d7b8ab293b0c1f39fd59ef2))
|
||||
|
||||
# [5.26.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.4...v5.26.0-dev.5) (2025-06-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify - Custom theme:** Apply accent color in more places ([#5039](https://github.com/ReVanced/revanced-patches/issues/5039)) ([9357887](https://github.com/ReVanced/revanced-patches/commit/9357887b6fca7aaf34dfb0163645b6a998e1db76))
|
||||
|
||||
# [5.26.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.3...v5.26.0-dev.4) (2025-06-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Spotify:** Add `Hide Create button` patch ([#5062](https://github.com/ReVanced/revanced-patches/issues/5062)) ([3201681](https://github.com/ReVanced/revanced-patches/commit/32016819d2adbdfdd5e028941d56feda36d20b00))
|
||||
|
||||
# [5.26.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.2...v5.26.0-dev.3) (2025-06-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Playback Speed:** Use modern custom speed dialog ([#5069](https://github.com/ReVanced/revanced-patches/issues/5069)) ([9a1e6ca](https://github.com/ReVanced/revanced-patches/commit/9a1e6ca178d9833ee2c681fb130b9290a4e89cd8))
|
||||
|
||||
# [5.26.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.1...v5.26.0-dev.2) (2025-06-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube:** Support A/B Shorts layout for RYD and component hiding ([#5081](https://github.com/ReVanced/revanced-patches/issues/5081)) ([8ecacaa](https://github.com/ReVanced/revanced-patches/commit/8ecacaad27162d9380014a9a13ac9220b12257b2))
|
||||
|
||||
# [5.26.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.25.0...v5.26.0-dev.1) (2025-05-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Proton Mail:** Add `Remove free accounts limit` patch ([#4970](https://github.com/ReVanced/revanced-patches/issues/4970)) ([b0440ad](https://github.com/ReVanced/revanced-patches/commit/b0440ad6af0e190e516974ce896dcc54c8d2e122))
|
||||
|
||||
# [5.25.0](https://github.com/ReVanced/revanced-patches/compare/v5.24.0...v5.25.0) (2025-05-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Disable Pairip license check:** Change patch to default off ([74b6a94](https://github.com/ReVanced/revanced-patches/commit/74b6a94577ac3f73b04bd0cce98fb7011a6607fd))
|
||||
* **Hide ADB status:** Resolve app crash on startup ([#5029](https://github.com/ReVanced/revanced-patches/issues/5029)) ([1abebd5](https://github.com/ReVanced/revanced-patches/commit/1abebd5f3b73250c6638d2d8a274b92ea8268924))
|
||||
* **Messenger:** Remove outdated `Disable switching emoji to sticker` patch ([#5044](https://github.com/ReVanced/revanced-patches/issues/5044)) ([7b182ca](https://github.com/ReVanced/revanced-patches/commit/7b182cab825ee3a4a3ca528c744c9d2a351c7cf8))
|
||||
* **Spotify Lite:** Remove obsolete `Enable on demand` patch ([#5046](https://github.com/ReVanced/revanced-patches/issues/5046)) ([4886d47](https://github.com/ReVanced/revanced-patches/commit/4886d47506c94b03c1f190ecc4947d3d91df6a47))
|
||||
* **YouTube - GmsCore support:** Restore patch functionality from prior merge ([7686bbe](https://github.com/ReVanced/revanced-patches/commit/7686bbe975644e1e582fa52f166879da5694ed93))
|
||||
* **YouTube - Hide ads:** Hide new type of general ad ([#5004](https://github.com/ReVanced/revanced-patches/issues/5004)) ([37e59d2](https://github.com/ReVanced/revanced-patches/commit/37e59d2771528c631dc13e73dac095fec95c6485))
|
||||
* **YouTube - Open Shorts in regular player:** Do not exit app when pressing back button in regular player ([#5020](https://github.com/ReVanced/revanced-patches/issues/5020)) ([3384f8d](https://github.com/ReVanced/revanced-patches/commit/3384f8dd0ff2a345f2e387f4ed1570079a83ccb6))
|
||||
* **YouTube:** Better handle incorrect duplicate translations ([20abac5](https://github.com/ReVanced/revanced-patches/commit/20abac52121fbecb65d87d0982f3380e1cf4e20e))
|
||||
* **Yuka - Unlock premium:** Remove broken patch that is no longer supported ([#5018](https://github.com/ReVanced/revanced-patches/issues/5018)) ([fac6e59](https://github.com/ReVanced/revanced-patches/commit/fac6e59d281e21e57abdcfc899cd1aeb18e5c2b8))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `Disable pairip license check` patch ([#4927](https://github.com/ReVanced/revanced-patches/issues/4927)) ([42d2c27](https://github.com/ReVanced/revanced-patches/commit/42d2c277982ef63e6ad42d85e46f13c3ab50243c))
|
||||
* **Messenger:** Add `Remove Meta AI` patch ([#4945](https://github.com/ReVanced/revanced-patches/issues/4945)) ([012dff7](https://github.com/ReVanced/revanced-patches/commit/012dff7b6511b9e519ccac96f6713cf1a1b327b4))
|
||||
* **Prime Video:** Add `Rename shared permissions` patch ([#5049](https://github.com/ReVanced/revanced-patches/issues/5049)) ([80f1fc6](https://github.com/ReVanced/revanced-patches/commit/80f1fc629e30e391bd5877f07dbdf4b6613bd1cf))
|
||||
* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5))
|
||||
* **Threads:** Hide Ads ([#5064](https://github.com/ReVanced/revanced-patches/issues/5064)) ([3c4cecb](https://github.com/ReVanced/revanced-patches/commit/3c4cecb966c2f99bfde99552686dda19ade5f67e))
|
||||
* **YouTube - Enable debugging:** Add settings menu to share debug logs ([#5021](https://github.com/ReVanced/revanced-patches/issues/5021)) ([1ec4a88](https://github.com/ReVanced/revanced-patches/commit/1ec4a88464a2a2810c02cf072950b618d183779a))
|
||||
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16))
|
||||
* **YouTube - Swipe controls:** Add separate color settings for the brightness and volume bars ([#5043](https://github.com/ReVanced/revanced-patches/issues/5043)) ([80f50e8](https://github.com/ReVanced/revanced-patches/commit/80f50e8c50ca6a8366b7fd7b01459fb16fa1074a))
|
||||
* **YouTube:** Add `Disable haptic feedback` patch ([#5033](https://github.com/ReVanced/revanced-patches/issues/5033)) ([bbe7974](https://github.com/ReVanced/revanced-patches/commit/bbe79744a513c96f9016476e8435f999e94c45d7))
|
||||
|
||||
# [5.25.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.13...v5.25.0-dev.14) (2025-05-29)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Threads:** Hide Ads ([#5064](https://github.com/ReVanced/revanced-patches/issues/5064)) ([3c4cecb](https://github.com/ReVanced/revanced-patches/commit/3c4cecb966c2f99bfde99552686dda19ade5f67e))
|
||||
|
||||
# [5.25.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.12...v5.25.0-dev.13) (2025-05-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Prime Video:** Add `Rename shared permissions` patch ([#5049](https://github.com/ReVanced/revanced-patches/issues/5049)) ([80f1fc6](https://github.com/ReVanced/revanced-patches/commit/80f1fc629e30e391bd5877f07dbdf4b6613bd1cf))
|
||||
|
||||
# [5.25.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.11...v5.25.0-dev.12) (2025-05-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Swipe controls:** Add separate color settings for the brightness and volume bars ([#5043](https://github.com/ReVanced/revanced-patches/issues/5043)) ([80f50e8](https://github.com/ReVanced/revanced-patches/commit/80f50e8c50ca6a8366b7fd7b01459fb16fa1074a))
|
||||
|
||||
# [5.25.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.10...v5.25.0-dev.11) (2025-05-27)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube - Enable debugging:** Add settings menu to share debug logs ([#5021](https://github.com/ReVanced/revanced-patches/issues/5021)) ([1ec4a88](https://github.com/ReVanced/revanced-patches/commit/1ec4a88464a2a2810c02cf072950b618d183779a))
|
||||
* **YouTube:** Add `Disable haptic feedback` patch ([#5033](https://github.com/ReVanced/revanced-patches/issues/5033)) ([bbe7974](https://github.com/ReVanced/revanced-patches/commit/bbe79744a513c96f9016476e8435f999e94c45d7))
|
||||
|
||||
# [5.25.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.9...v5.25.0-dev.10) (2025-05-27)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Messenger:** Remove outdated `Disable switching emoji to sticker` patch ([#5044](https://github.com/ReVanced/revanced-patches/issues/5044)) ([7b182ca](https://github.com/ReVanced/revanced-patches/commit/7b182cab825ee3a4a3ca528c744c9d2a351c7cf8))
|
||||
* **Spotify Lite:** Remove obsolete `Enable on demand` patch ([#5046](https://github.com/ReVanced/revanced-patches/issues/5046)) ([4886d47](https://github.com/ReVanced/revanced-patches/commit/4886d47506c94b03c1f190ecc4947d3d91df6a47))
|
||||
|
||||
# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26)
|
||||
|
||||
|
||||
|
||||
@@ -2,13 +2,15 @@ package app.revanced.extension.messenger.metaai;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class RemoveMetaAIPatch {
|
||||
public static boolean overrideConfigBool(long id, boolean value) {
|
||||
// It seems like all configs starting with 363219 are related to Meta AI.
|
||||
// A list of specific ones that need disabling would probably be better,
|
||||
// but these config numbers seem to change slightly with each update.
|
||||
// These first 6 digits don't though.
|
||||
if (Long.toString(id).startsWith("363219"))
|
||||
public static boolean overrideBooleanFlag(long id, boolean value) {
|
||||
// This catches all flag IDs related to Meta AI.
|
||||
// The IDs change slightly with every update,
|
||||
// so to work around this, IDs from different versions were compared
|
||||
// to find what they have in common, which turned out to be those first bits.
|
||||
// TODO: Find the specific flags that we care about and patch the code they control instead.
|
||||
if ((id & 0x7FFFFFC000000000L) == 0x810A8000000000L) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
package app.revanced.extension.shared;
|
||||
|
||||
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG;
|
||||
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_STACKTRACE;
|
||||
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
import static app.revanced.extension.shared.settings.BaseSettings.*;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
||||
|
||||
/**
|
||||
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
|
||||
* and additionally accessible thru {@link LogBufferManager}.
|
||||
*
|
||||
* All methods are thread safe.
|
||||
*/
|
||||
public class Logger {
|
||||
|
||||
/**
|
||||
@@ -17,99 +28,159 @@ public class Logger {
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface LogMessage {
|
||||
/**
|
||||
* @return Logger string message. This method is only called if logging is enabled.
|
||||
*/
|
||||
@NonNull
|
||||
String buildMessageString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return For outer classes, this returns {@link Class#getSimpleName()}.
|
||||
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
|
||||
* <br>
|
||||
* For example, each of these classes return 'SomethingView':
|
||||
* <code>
|
||||
* com.company.SomethingView
|
||||
* com.company.SomethingView$StaticClass
|
||||
* com.company.SomethingView$1
|
||||
* </code>
|
||||
*/
|
||||
private String findOuterClassSimpleName() {
|
||||
var selfClass = this.getClass();
|
||||
private enum LogLevel {
|
||||
DEBUG,
|
||||
INFO,
|
||||
ERROR
|
||||
}
|
||||
|
||||
String fullClassName = selfClass.getName();
|
||||
final int dollarSignIndex = fullClassName.indexOf('$');
|
||||
if (dollarSignIndex < 0) {
|
||||
return selfClass.getSimpleName(); // Already an outer class.
|
||||
/**
|
||||
* Log tag prefix. Only used for system logging.
|
||||
*/
|
||||
private static final String REVANCED_LOG_TAG_PREFIX = "revanced: ";
|
||||
|
||||
private static final String LOGGER_CLASS_NAME = Logger.class.getName();
|
||||
|
||||
/**
|
||||
* @return For outer classes, this returns {@link Class#getSimpleName()}.
|
||||
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
|
||||
* <br>
|
||||
* For example, each of these classes returns 'SomethingView':
|
||||
* <code>
|
||||
* com.company.SomethingView
|
||||
* com.company.SomethingView$StaticClass
|
||||
* com.company.SomethingView$1
|
||||
* </code>
|
||||
*/
|
||||
private static String getOuterClassSimpleName(Object obj) {
|
||||
Class<?> logClass = obj.getClass();
|
||||
String fullClassName = logClass.getName();
|
||||
final int dollarSignIndex = fullClassName.indexOf('$');
|
||||
if (dollarSignIndex < 0) {
|
||||
return logClass.getSimpleName(); // Already an outer class.
|
||||
}
|
||||
|
||||
// Class is inner, static, or anonymous.
|
||||
// Parse the simple name full name.
|
||||
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
|
||||
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
|
||||
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to handle logging to Android Log and {@link LogBufferManager}.
|
||||
* Appends the log message, stack trace (if enabled), and exception (if present) to logBuffer
|
||||
* with class name but without 'revanced:' prefix.
|
||||
*
|
||||
* @param logLevel The log level.
|
||||
* @param message Log message object.
|
||||
* @param ex Optional exception.
|
||||
* @param includeStackTrace If the current stack should be included.
|
||||
* @param showToast If a toast is to be shown.
|
||||
*/
|
||||
private static void logInternal(LogLevel logLevel, LogMessage message, @Nullable Throwable ex,
|
||||
boolean includeStackTrace, boolean showToast) {
|
||||
// It's very important that no Settings are used in this method,
|
||||
// as this code is used when a context is not set and thus referencing
|
||||
// a setting will crash the app.
|
||||
String messageString = message.buildMessageString();
|
||||
String className = getOuterClassSimpleName(message);
|
||||
|
||||
String logText = messageString;
|
||||
|
||||
// Append exception message if present.
|
||||
if (ex != null) {
|
||||
var exceptionMessage = ex.getMessage();
|
||||
if (exceptionMessage != null) {
|
||||
logText += "\nException: " + exceptionMessage;
|
||||
}
|
||||
}
|
||||
|
||||
// Class is inner, static, or anonymous.
|
||||
// Parse the simple name full name.
|
||||
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
|
||||
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
|
||||
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
|
||||
if (includeStackTrace) {
|
||||
var sw = new StringWriter();
|
||||
new Throwable().printStackTrace(new PrintWriter(sw));
|
||||
String stackTrace = sw.toString();
|
||||
// Remove the stacktrace elements of this class.
|
||||
final int loggerIndex = stackTrace.lastIndexOf(LOGGER_CLASS_NAME);
|
||||
final int loggerBegins = stackTrace.indexOf('\n', loggerIndex);
|
||||
logText += stackTrace.substring(loggerBegins);
|
||||
}
|
||||
|
||||
// Do not include "revanced:" prefix in clipboard logs.
|
||||
String managerToastString = className + ": " + logText;
|
||||
LogBufferManager.appendToLogBuffer(managerToastString);
|
||||
|
||||
String logTag = REVANCED_LOG_TAG_PREFIX + className;
|
||||
switch (logLevel) {
|
||||
case DEBUG:
|
||||
if (ex == null) Log.d(logTag, logText);
|
||||
else Log.d(logTag, logText, ex);
|
||||
break;
|
||||
case INFO:
|
||||
if (ex == null) Log.i(logTag, logText);
|
||||
else Log.i(logTag, logText, ex);
|
||||
break;
|
||||
case ERROR:
|
||||
if (ex == null) Log.e(logTag, logText);
|
||||
else Log.e(logTag, logText, ex);
|
||||
break;
|
||||
}
|
||||
|
||||
if (showToast) {
|
||||
Utils.showToastLong(managerToastString);
|
||||
}
|
||||
}
|
||||
|
||||
private static final String REVANCED_LOG_PREFIX = "revanced: ";
|
||||
|
||||
/**
|
||||
* Logs debug messages under the outer class name of the code calling this method.
|
||||
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
|
||||
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||
* <p>
|
||||
* Whenever possible, the log string should be constructed entirely inside
|
||||
* {@link LogMessage#buildMessageString()} so the performance cost of
|
||||
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||
*/
|
||||
public static void printDebug(@NonNull LogMessage message) {
|
||||
public static void printDebug(LogMessage message) {
|
||||
printDebug(message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs debug messages under the outer class name of the code calling this method.
|
||||
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
|
||||
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||
* <p>
|
||||
* Whenever possible, the log string should be constructed entirely inside
|
||||
* {@link LogMessage#buildMessageString()} so the performance cost of
|
||||
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||
*/
|
||||
public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
|
||||
public static void printDebug(LogMessage message, @Nullable Exception ex) {
|
||||
if (DEBUG.get()) {
|
||||
String logMessage = message.buildMessageString();
|
||||
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
|
||||
|
||||
if (DEBUG_STACKTRACE.get()) {
|
||||
var builder = new StringBuilder(logMessage);
|
||||
var sw = new StringWriter();
|
||||
new Throwable().printStackTrace(new PrintWriter(sw));
|
||||
|
||||
builder.append('\n').append(sw);
|
||||
logMessage = builder.toString();
|
||||
}
|
||||
|
||||
if (ex == null) {
|
||||
Log.d(logTag, logMessage);
|
||||
} else {
|
||||
Log.d(logTag, logMessage, ex);
|
||||
}
|
||||
logInternal(LogLevel.DEBUG, message, ex, DEBUG_STACKTRACE.get(), false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs information messages using the outer class name of the code calling this method.
|
||||
*/
|
||||
public static void printInfo(@NonNull LogMessage message) {
|
||||
public static void printInfo(LogMessage message) {
|
||||
printInfo(message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs information messages using the outer class name of the code calling this method.
|
||||
*/
|
||||
public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) {
|
||||
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
|
||||
String logMessage = message.buildMessageString();
|
||||
if (ex == null) {
|
||||
Log.i(logTag, logMessage);
|
||||
} else {
|
||||
Log.i(logTag, logMessage, ex);
|
||||
}
|
||||
public static void printInfo(LogMessage message, @Nullable Exception ex) {
|
||||
logInternal(LogLevel.INFO, message, ex, DEBUG_STACKTRACE.get(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs exceptions under the outer class name of the code calling this method.
|
||||
* Appends the log message, exception (if present), and toast message (if enabled) to logBuffer.
|
||||
*/
|
||||
public static void printException(@NonNull LogMessage message) {
|
||||
public static void printException(LogMessage message) {
|
||||
printException(message, null);
|
||||
}
|
||||
|
||||
@@ -122,35 +193,23 @@ public class Logger {
|
||||
* @param message log message
|
||||
* @param ex exception (optional)
|
||||
*/
|
||||
public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) {
|
||||
String messageString = message.buildMessageString();
|
||||
String outerClassSimpleName = message.findOuterClassSimpleName();
|
||||
String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName;
|
||||
if (ex == null) {
|
||||
Log.e(logMessage, messageString);
|
||||
} else {
|
||||
Log.e(logMessage, messageString, ex);
|
||||
}
|
||||
if (DEBUG_TOAST_ON_ERROR.get()) {
|
||||
Utils.showToastLong(outerClassSimpleName + ": " + messageString);
|
||||
}
|
||||
public static void printException(LogMessage message, @Nullable Throwable ex) {
|
||||
logInternal(LogLevel.ERROR, message, ex, DEBUG_STACKTRACE.get(), DEBUG_TOAST_ON_ERROR.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
||||
* Normally this method should not be used.
|
||||
*/
|
||||
public static void initializationInfo(@NonNull Class<?> callingClass, @NonNull String message) {
|
||||
Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message);
|
||||
public static void initializationInfo(LogMessage message) {
|
||||
logInternal(LogLevel.INFO, message, null, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
||||
* Normally this method should not be used.
|
||||
*/
|
||||
public static void initializationException(@NonNull Class<?> callingClass, @NonNull String message,
|
||||
@Nullable Exception ex) {
|
||||
Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex);
|
||||
public static void initializationException(LogMessage message, @Nullable Exception ex) {
|
||||
logInternal(LogLevel.ERROR, message, ex, false, false);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,15 +363,17 @@ public class Utils {
|
||||
|
||||
public static Context getContext() {
|
||||
if (context == null) {
|
||||
Logger.initializationException(Utils.class, "Context is not set by extension hook, returning null", null);
|
||||
Logger.initializationException(() -> "Context is not set by extension hook, returning null", null);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
public static void setContext(Context appContext) {
|
||||
// Intentionally use logger before context is set,
|
||||
// to expose any bugs in the 'no context available' logger method.
|
||||
Logger.initializationInfo(() -> "Set context: " + appContext);
|
||||
// Must initially set context to check the app language.
|
||||
context = appContext;
|
||||
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
|
||||
|
||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
||||
if (language != AppLanguage.DEFAULT) {
|
||||
@@ -383,8 +385,9 @@ public class Utils {
|
||||
}
|
||||
}
|
||||
|
||||
public static void setClipboard(@NonNull String text) {
|
||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
public static void setClipboard(CharSequence text) {
|
||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context
|
||||
.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
}
|
||||
@@ -548,14 +551,15 @@ public class Utils {
|
||||
private static void showToast(@NonNull String messageToToast, int toastDuration) {
|
||||
Objects.requireNonNull(messageToToast);
|
||||
runOnMainThreadNowOrLater(() -> {
|
||||
if (context == null) {
|
||||
Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
||||
Toast.makeText(context, messageToToast, toastDuration).show();
|
||||
}
|
||||
}
|
||||
);
|
||||
Context currentContext = context;
|
||||
|
||||
if (currentContext == null) {
|
||||
Logger.initializationException(() -> "Cannot show toast (context is null): " + messageToToast, null);
|
||||
} else {
|
||||
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
||||
Toast.makeText(currentContext, messageToToast, toastDuration).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isDarkModeEnabled() {
|
||||
@@ -579,7 +583,7 @@ public class Utils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically logs any exceptions the runnable throws
|
||||
* Automatically logs any exceptions the runnable throws.
|
||||
*/
|
||||
public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
|
||||
Runnable loggingRunnable = () -> {
|
||||
@@ -605,14 +609,14 @@ public class Utils {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return if the calling thread is on the main thread
|
||||
* @return if the calling thread is on the main thread.
|
||||
*/
|
||||
public static boolean isCurrentlyOnMainThread() {
|
||||
return Looper.getMainLooper().isCurrentThread();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalStateException if the calling thread is _off_ the main thread
|
||||
* @throws IllegalStateException if the calling thread is _off_ the main thread.
|
||||
*/
|
||||
public static void verifyOnMainThread() throws IllegalStateException {
|
||||
if (!isCurrentlyOnMainThread()) {
|
||||
@@ -621,7 +625,7 @@ public class Utils {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalStateException if the calling thread is _on_ the main thread
|
||||
* @throws IllegalStateException if the calling thread is _on_ the main thread.
|
||||
*/
|
||||
public static void verifyOffMainThread() throws IllegalStateException {
|
||||
if (isCurrentlyOnMainThread()) {
|
||||
@@ -635,6 +639,11 @@ public class Utils {
|
||||
OTHER,
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling extension code must ensure the un-patched app has the permission
|
||||
* <code>android.permission.ACCESS_NETWORK_STATE</code>, otherwise the app will crash
|
||||
* if this method is used.
|
||||
*/
|
||||
public static boolean isNetworkConnected() {
|
||||
NetworkType networkType = getNetworkType();
|
||||
return networkType == NetworkType.MOBILE
|
||||
@@ -642,10 +651,11 @@ public class Utils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling extension code must ensure the target app has the
|
||||
* <code>ACCESS_NETWORK_STATE</code> app manifest permission.
|
||||
* Calling extension code must ensure the un-patched app has the permission
|
||||
* <code>android.permission.ACCESS_NETWORK_STATE</code>, otherwise the app will crash
|
||||
* if this method is used.
|
||||
*/
|
||||
@SuppressWarnings({"deprecation", "MissingPermission"})
|
||||
@SuppressLint({"MissingPermission", "deprecation"})
|
||||
public static NetworkType getNetworkType() {
|
||||
Context networkContext = getContext();
|
||||
if (networkContext == null) {
|
||||
@@ -782,8 +792,9 @@ public class Utils {
|
||||
preferences.add(new Pair<>(sortValue, preference));
|
||||
}
|
||||
|
||||
//noinspection ComparatorCombinators
|
||||
Collections.sort(preferences, (pair1, pair2)
|
||||
-> pair1.first.compareToIgnoreCase(pair2.first));
|
||||
-> pair1.first.compareTo(pair2.first));
|
||||
|
||||
int index = 0;
|
||||
for (Pair<String, Preference> pair : preferences) {
|
||||
|
||||
@@ -70,7 +70,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
||||
|
||||
// Show the user the settings in JSON format.
|
||||
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
|
||||
Utils.setClipboard(getEditText().getText().toString());
|
||||
Utils.setClipboard(getEditText().getText());
|
||||
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
|
||||
importSettings(builder.getContext(), getEditText().getText().toString());
|
||||
});
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import java.util.Deque;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
|
||||
/**
|
||||
* Manages a buffer for storing debug logs from {@link Logger}.
|
||||
* Stores just under 1MB of the most recent log data.
|
||||
*
|
||||
* All methods are thread-safe.
|
||||
*/
|
||||
public final class LogBufferManager {
|
||||
/** Maximum byte size of all buffer entries. Must be less than Android's 1 MB Binder transaction limit. */
|
||||
private static final int BUFFER_MAX_BYTES = 900_000;
|
||||
/** Limit number of log lines. */
|
||||
private static final int BUFFER_MAX_SIZE = 10_000;
|
||||
|
||||
private static final Deque<String> logBuffer = new ConcurrentLinkedDeque<>();
|
||||
private static final AtomicInteger logBufferByteSize = new AtomicInteger();
|
||||
|
||||
/**
|
||||
* Appends a log message to the internal buffer if debugging is enabled.
|
||||
* The buffer is limited to approximately {@link #BUFFER_MAX_BYTES} or {@link #BUFFER_MAX_SIZE}
|
||||
* to prevent excessive memory usage.
|
||||
*
|
||||
* @param message The log message to append.
|
||||
*/
|
||||
public static void appendToLogBuffer(String message) {
|
||||
Objects.requireNonNull(message);
|
||||
|
||||
// It's very important that no Settings are used in this method,
|
||||
// as this code is used when a context is not set and thus referencing
|
||||
// a setting will crash the app.
|
||||
logBuffer.addLast(message);
|
||||
int newSize = logBufferByteSize.addAndGet(message.length());
|
||||
|
||||
// Remove oldest entries if over the log size limits.
|
||||
while (newSize > BUFFER_MAX_BYTES || logBuffer.size() > BUFFER_MAX_SIZE) {
|
||||
String removed = logBuffer.pollFirst();
|
||||
if (removed == null) {
|
||||
// Thread race of two different calls to this method, and the other thread won.
|
||||
return;
|
||||
}
|
||||
|
||||
newSize = logBufferByteSize.addAndGet(-removed.length());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports all logs from the internal buffer to the clipboard.
|
||||
* Displays a toast with the result.
|
||||
*/
|
||||
public static void exportToClipboard() {
|
||||
try {
|
||||
if (!BaseSettings.DEBUG.get()) {
|
||||
Utils.showToastShort(str("revanced_debug_logs_disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (logBuffer.isEmpty()) {
|
||||
Utils.showToastShort(str("revanced_debug_logs_none_found"));
|
||||
clearLogBufferData(); // Clear toast log entry that was just created.
|
||||
return;
|
||||
}
|
||||
|
||||
// Most (but not all) Android 13+ devices always show a "copied to clipboard" toast
|
||||
// and there is no way to programmatically detect if a toast will show or not.
|
||||
// Show a toast even if using Android 13+, but show ReVanced toast first (before copying to clipboard).
|
||||
Utils.showToastShort(str("revanced_debug_logs_copied_to_clipboard"));
|
||||
|
||||
Utils.setClipboard(String.join("\n", logBuffer));
|
||||
} catch (Exception ex) {
|
||||
// Handle security exception if clipboard access is denied.
|
||||
String errorMessage = String.format(str("revanced_debug_logs_failed_to_export"), ex.getMessage());
|
||||
Utils.showToastLong(errorMessage);
|
||||
Logger.printDebug(() -> errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearLogBufferData() {
|
||||
// Cannot simply clear the log buffer because there is no
|
||||
// write lock for both the deque and the atomic int.
|
||||
// Instead pop off log entries and decrement the size one by one.
|
||||
while (!logBuffer.isEmpty()) {
|
||||
String removed = logBuffer.pollFirst();
|
||||
if (removed != null) {
|
||||
logBufferByteSize.addAndGet(-removed.length());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the internal log buffer and displays a toast with the result.
|
||||
*/
|
||||
public static void clearLogBuffer() {
|
||||
if (!BaseSettings.DEBUG.get()) {
|
||||
Utils.showToastShort(str("revanced_debug_logs_disabled"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Show toast before clearing, otherwise toast log will still remain.
|
||||
Utils.showToastShort(str("revanced_debug_logs_clear_toast"));
|
||||
clearLogBufferData();
|
||||
}
|
||||
}
|
||||
@@ -60,8 +60,9 @@ public class SortedListPreference extends ListPreference {
|
||||
}
|
||||
}
|
||||
|
||||
//noinspection ComparatorCombinators
|
||||
Collections.sort(lastEntries, (pair1, pair2)
|
||||
-> pair1.first.compareToIgnoreCase(pair2.first));
|
||||
-> pair1.first.compareTo(pair2.first));
|
||||
|
||||
CharSequence[] sortedEntries = new CharSequence[entrySize];
|
||||
CharSequence[] sortedEntryValues = new CharSequence[entrySize];
|
||||
|
||||
@@ -71,9 +71,7 @@ final class PlayerRoutes {
|
||||
return innerTubeBody.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @noinspection SameParameterValue
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
|
||||
var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
|
||||
|
||||
|
||||
@@ -6,11 +6,11 @@ dependencies {
|
||||
|
||||
android {
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
package app.revanced.extension.spotify.layout.hide.createbutton;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.spotify.shared.ComponentFilters.*;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class HideCreateButtonPatch {
|
||||
|
||||
/**
|
||||
* A list of component filters that match whether a navigation bar item is the Create button.
|
||||
* The main approach used is matching the resource id for the Create button title.
|
||||
*/
|
||||
private static final List<ComponentFilter> CREATE_BUTTON_COMPONENT_FILTERS = List.of(
|
||||
new ResourceIdComponentFilter("navigationbar_musicappitems_create_title", "string"),
|
||||
// Temporary fallback and fix for APKs merged with AntiSplit-M not having resources properly encoded,
|
||||
// and thus getting the resource identifier for the Create button title always return 0.
|
||||
// FIXME: Remove this once the above issue is no longer relevant.
|
||||
new StringComponentFilter("spotify:create-menu")
|
||||
);
|
||||
|
||||
/**
|
||||
* A component filter for the old id of the resource which contained the Create button title.
|
||||
* Used in older versions of the app.
|
||||
*/
|
||||
private static final ResourceIdComponentFilter OLD_CREATE_BUTTON_COMPONENT_FILTER =
|
||||
new ResourceIdComponentFilter("bottom_navigation_bar_create_tab_title", "string");
|
||||
|
||||
/**
|
||||
* Injection point. This method is called on every navigation bar item to check whether it is the Create button.
|
||||
* If the navigation bar item is the Create button, it returns null to erase it.
|
||||
* The method fingerprint used to patch ensures we can safely return null here.
|
||||
*/
|
||||
public static Object returnNullIfIsCreateButton(Object navigationBarItem) {
|
||||
if (navigationBarItem == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String stringifiedNavigationBarItem = navigationBarItem.toString();
|
||||
|
||||
for (ComponentFilter componentFilter : CREATE_BUTTON_COMPONENT_FILTERS) {
|
||||
if (componentFilter.filterUnavailable()) {
|
||||
Logger.printInfo(() -> "returnNullIfIsCreateButton: Filter " +
|
||||
componentFilter.getFilterRepresentation() + " not available, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stringifiedNavigationBarItem.contains(componentFilter.getFilterValue())) {
|
||||
Logger.printInfo(() -> "Hiding Create button because the navigation bar item " + navigationBarItem +
|
||||
" matched the filter " + componentFilter.getFilterRepresentation());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return navigationBarItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Called in older versions of the app. Returns whether the old navigation bar item is the old
|
||||
* Create button.
|
||||
*/
|
||||
public static boolean isOldCreateButton(int oldNavigationBarItemTitleResId) {
|
||||
if (OLD_CREATE_BUTTON_COMPONENT_FILTER.filterUnavailable()) {
|
||||
Logger.printInfo(() -> "Skipping hiding old Create button because the resource id for " +
|
||||
OLD_CREATE_BUTTON_COMPONENT_FILTER.resourceName + " is not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (oldNavigationBarItemTitleResId == OLD_CREATE_BUTTON_COMPONENT_FILTER.getResourceId()) {
|
||||
Logger.printInfo(() -> "Hiding old Create button because the navigation bar item title resource id" +
|
||||
" matched " + OLD_CREATE_BUTTON_COMPONENT_FILTER.getFilterRepresentation());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,54 @@ import app.revanced.extension.shared.Utils;
|
||||
@SuppressWarnings("unused")
|
||||
public final class CustomThemePatch {
|
||||
|
||||
private static final int BACKGROUND_COLOR = getColorFromString("@color/gray_7");
|
||||
private static final int BACKGROUND_COLOR_SECONDARY = getColorFromString("@color/gray_15");
|
||||
private static final int ACCENT_COLOR = getColorFromString("@color/spotify_green_157");
|
||||
private static final int ACCENT_PRESSED_COLOR =
|
||||
getColorFromString("@color/dark_brightaccent_background_press");
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
* Returns an int representation of the color resource or hex code.
|
||||
*/
|
||||
public static long getThemeColor(String colorString) {
|
||||
private static int getColorFromString(String colorString) {
|
||||
try {
|
||||
return Utils.getColorFromString(colorString);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Invalid custom color: " + colorString, ex);
|
||||
Logger.printException(() -> "Invalid color string: " + colorString, ex);
|
||||
return Color.BLACK;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point. Returns an int representation of the replaced color from the original color.
|
||||
*/
|
||||
public static int replaceColor(int originalColor) {
|
||||
switch (originalColor) {
|
||||
// Playlist background color.
|
||||
case 0xFF121212:
|
||||
return BACKGROUND_COLOR;
|
||||
|
||||
// Share menu background color.
|
||||
case 0xFF1F1F1F:
|
||||
// Home category pills background color.
|
||||
case 0xFF333333:
|
||||
// Settings header background color.
|
||||
case 0xFF282828:
|
||||
// Spotify Connect device list background color.
|
||||
case 0xFF2A2A2A:
|
||||
return BACKGROUND_COLOR_SECONDARY;
|
||||
|
||||
// Some Lottie animations have a color that's slightly off due to rounding errors.
|
||||
case 0xFF1ED760: case 0xFF1ED75F:
|
||||
// Intermediate color used in some animations, same rounding issue.
|
||||
case 0xFF1DB954: case 0xFF1CB854:
|
||||
return ACCENT_COLOR;
|
||||
|
||||
case 0xFF1ABC54:
|
||||
return ACCENT_PRESSED_COLOR;
|
||||
|
||||
default:
|
||||
return originalColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,11 @@ public final class SanitizeSharingLinksPatch {
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build().toString();
|
||||
String sanitizedUrl = builder.build().toString();
|
||||
Logger.printInfo(() -> "Sanitized url " + url + " to " + sanitizedUrl);
|
||||
return sanitizedUrl;
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "sanitizeUrl failure", ex);
|
||||
|
||||
Logger.printException(() -> "sanitizeUrl failure with " + url, ex);
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package app.revanced.extension.spotify.shared;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
public final class ComponentFilters {
|
||||
|
||||
public interface ComponentFilter {
|
||||
String getFilterValue();
|
||||
String getFilterRepresentation();
|
||||
default boolean filterUnavailable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ResourceIdComponentFilter implements ComponentFilter {
|
||||
|
||||
public final String resourceName;
|
||||
public final String resourceType;
|
||||
// Android resources are always positive, so -1 is a valid sentinel value to indicate it has not been loaded.
|
||||
// 0 is returned when a resource has not been found.
|
||||
private int resourceId = -1;
|
||||
private String stringfiedResourceId = null;
|
||||
|
||||
public ResourceIdComponentFilter(String resourceName, String resourceType) {
|
||||
this.resourceName = resourceName;
|
||||
this.resourceType = resourceType;
|
||||
}
|
||||
|
||||
public int getResourceId() {
|
||||
if (resourceId == -1) {
|
||||
resourceId = Utils.getResourceIdentifier(resourceName, resourceType);
|
||||
}
|
||||
return resourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilterValue() {
|
||||
if (stringfiedResourceId == null) {
|
||||
stringfiedResourceId = Integer.toString(getResourceId());
|
||||
}
|
||||
return stringfiedResourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilterRepresentation() {
|
||||
boolean resourceFound = getResourceId() != 0;
|
||||
return (resourceFound ? getFilterValue() + " (" : "") + resourceName + (resourceFound ? ")" : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean filterUnavailable() {
|
||||
boolean resourceNotFound = getResourceId() == 0;
|
||||
if (resourceNotFound) {
|
||||
Logger.printInfo(() -> "Resource id for " + resourceName + " was not found");
|
||||
}
|
||||
return resourceNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class StringComponentFilter implements ComponentFilter {
|
||||
|
||||
public final String string;
|
||||
|
||||
public StringComponentFilter(String string) {
|
||||
this.string = string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilterValue() {
|
||||
return string;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFilterRepresentation() {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,11 @@ android {
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
minSdk = 21
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,9 +16,7 @@ public class SpoofSimPatch {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.initializationException(SpoofSimPatch.class,
|
||||
"Context is not yet set, cannot spoof: " + fieldSpoofed, null);
|
||||
|
||||
Logger.initializationException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,14 @@ public class ThemeHelper {
|
||||
return isDarkTheme() ? getLightThemeColor() : getDarkThemeColor();
|
||||
}
|
||||
|
||||
public static int getDialogBackgroundColor() {
|
||||
final String colorName = isDarkTheme()
|
||||
? "yt_black1"
|
||||
: "yt_white1";
|
||||
|
||||
return Utils.getColorFromString(colorName);
|
||||
}
|
||||
|
||||
public static int getToolbarBackgroundColor() {
|
||||
final String colorName = isDarkTheme()
|
||||
? "yt_black3"
|
||||
|
||||
@@ -686,7 +686,7 @@ public final class AlternativeThumbnailsPatch {
|
||||
? "" : fullUrl.substring(imageExtensionEndIndex);
|
||||
}
|
||||
|
||||
/** @noinspection SameParameterValue */
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) {
|
||||
// Images could be upgraded to webp if they are not already, but this fails quite often,
|
||||
// especially for new videos uploaded in the last hour.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class DisableHapticFeedbackPatch {
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableChapterVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableSeekUndoVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disablePreciseSeekingVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static boolean disableZoomVibrate() {
|
||||
return Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get();
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
@@ -58,6 +59,22 @@ public final class HidePlayerOverlayButtonsPatch {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static void hidePlayerControlButtonsBackground(View rootView) {
|
||||
try {
|
||||
if (!Settings.HIDE_PLAYER_CONTROL_BUTTONS_BACKGROUND.get()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Each button is an ImageView with a background set to another drawable.
|
||||
removeImageViewsBackgroundRecursive(rootView);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "removePlayerControlButtonsBackground failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void hideView(View parentView, int resourceId) {
|
||||
View nextPreviousButton = parentView.findViewById(resourceId);
|
||||
|
||||
@@ -69,4 +86,16 @@ public final class HidePlayerOverlayButtonsPatch {
|
||||
Logger.printDebug(() -> "Hiding previous/next button");
|
||||
Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton);
|
||||
}
|
||||
|
||||
private static void removeImageViewsBackgroundRecursive(View currentView) {
|
||||
if (currentView instanceof ImageView imageView) {
|
||||
imageView.setBackground(null);
|
||||
}
|
||||
|
||||
if (currentView instanceof ViewGroup viewGroup) {
|
||||
for (int i = 0; i < viewGroup.getChildCount(); i++) {
|
||||
removeImageViewsBackgroundRecursive(viewGroup.getChildAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,11 +152,13 @@ public class ReturnYouTubeDislikePatch {
|
||||
return original; // No need to check for Shorts in the context.
|
||||
}
|
||||
|
||||
if (conversionContextString.contains("|shorts_dislike_button.eml")) {
|
||||
if (Utils.containsAny(conversionContextString,
|
||||
"|shorts_dislike_button.eml", "|reel_dislike_button.eml")) {
|
||||
return getShortsSpan(original, true);
|
||||
}
|
||||
|
||||
if (conversionContextString.contains("|shorts_like_button.eml")) {
|
||||
if (Utils.containsAny(conversionContextString,
|
||||
"|shorts_like_button.eml", "|reel_like_button.eml")) {
|
||||
if (!Utils.containsNumber(original)) {
|
||||
Logger.printDebug(() -> "Replacing hidden likes count");
|
||||
return getShortsSpan(original, false);
|
||||
@@ -361,6 +363,11 @@ public class ReturnYouTubeDislikePatch {
|
||||
if (videoId.equals(lastPrefetchedVideoId)) {
|
||||
return;
|
||||
}
|
||||
if (!Utils.isNetworkConnected()) {
|
||||
Logger.printDebug(() -> "Cannot pre-fetch RYD, network is not connected");
|
||||
lastPrefetchedVideoId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort();
|
||||
// Shorts shelf in home and subscription feed causes player response hook to be called,
|
||||
@@ -415,6 +422,12 @@ public class ReturnYouTubeDislikePatch {
|
||||
}
|
||||
Logger.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType);
|
||||
|
||||
if (!Utils.isNetworkConnected()) {
|
||||
Logger.printDebug(() -> "Cannot fetch RYD, network is not connected");
|
||||
currentVideoData = null;
|
||||
return;
|
||||
}
|
||||
|
||||
ReturnYouTubeDislike data = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||
// Pre-emptively set the data to short status.
|
||||
// Required to prevent Shorts data from being used on a minimized video in incognito mode.
|
||||
|
||||
@@ -354,4 +354,23 @@ public final class VideoInformation {
|
||||
return videoTime >= videoLength && videoLength > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the current playback speed.
|
||||
* Rest of the implementation added by patch.
|
||||
*/
|
||||
public static void overridePlaybackSpeed(float speedOverride) {
|
||||
Logger.printDebug(() -> "Overriding playback speed to: " + speedOverride);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* @param newlyLoadedPlaybackSpeed The current playback speed.
|
||||
*/
|
||||
public static void setPlaybackSpeed(float newlyLoadedPlaybackSpeed) {
|
||||
if (playbackSpeed != newlyLoadedPlaybackSpeed) {
|
||||
Logger.printDebug(() -> "Video speed changed: " + newlyLoadedPlaybackSpeed);
|
||||
playbackSpeed = newlyLoadedPlaybackSpeed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package app.revanced.extension.youtube.patches;
|
||||
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ZoomHapticsPatch {
|
||||
public static boolean shouldVibrate() {
|
||||
return !Settings.DISABLE_ZOOM_HAPTICS.get();
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,6 @@ import app.revanced.extension.youtube.settings.Settings;
|
||||
@SuppressWarnings("unused")
|
||||
final class CommentsFilter extends Filter {
|
||||
|
||||
private static final String TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH
|
||||
= "|CellType|ContainerType|ContainerType|ContainerType|ContainerType|ContainerType|";
|
||||
|
||||
private final StringFilterGroup commentComposer;
|
||||
private final ByteArrayFilterGroup emojiPickerBufferGroup;
|
||||
private final StringFilterGroup filterChipBar;
|
||||
private final ByteArrayFilterGroup aiCommentsSummary;
|
||||
|
||||
@@ -50,14 +45,9 @@ final class CommentsFilter extends Filter {
|
||||
"super_thanks_button.eml"
|
||||
);
|
||||
|
||||
commentComposer = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_TIMESTAMP_AND_EMOJI_BUTTONS,
|
||||
"comment_composer.eml"
|
||||
);
|
||||
|
||||
emojiPickerBufferGroup = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"id.comment.quick_emoji.button"
|
||||
StringFilterGroup timestampButton = new StringFilterGroup(
|
||||
Settings.HIDE_COMMENTS_TIMESTAMP_BUTTON,
|
||||
"composer_timestamp_button.eml"
|
||||
);
|
||||
|
||||
filterChipBar = new StringFilterGroup(
|
||||
@@ -77,7 +67,7 @@ final class CommentsFilter extends Filter {
|
||||
createAShort,
|
||||
previewComment,
|
||||
thanksButton,
|
||||
commentComposer,
|
||||
timestampButton,
|
||||
filterChipBar
|
||||
);
|
||||
}
|
||||
@@ -85,14 +75,6 @@ final class CommentsFilter extends Filter {
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == commentComposer) {
|
||||
// To completely hide the emoji buttons (and leave no empty space), the timestamp button is
|
||||
// also hidden because the buffer is exactly the same and there's no way selectively hide.
|
||||
return contentIndex == 0
|
||||
&& path.endsWith(TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH)
|
||||
&& emojiPickerBufferGroup.check(protobufBufferArray).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == filterChipBar) {
|
||||
return aiCommentsSummary.check(protobufBufferArray).isFiltered();
|
||||
}
|
||||
|
||||
@@ -10,18 +10,11 @@ import app.revanced.extension.youtube.settings.Settings;
|
||||
*/
|
||||
public final class PlaybackSpeedMenuFilterPatch extends Filter {
|
||||
|
||||
/**
|
||||
* Old litho based speed selection menu.
|
||||
*/
|
||||
public static volatile boolean isOldPlaybackSpeedMenuVisible;
|
||||
|
||||
/**
|
||||
* 0.05x speed selection menu.
|
||||
*/
|
||||
public static volatile boolean isPlaybackRateSelectorMenuVisible;
|
||||
|
||||
private final StringFilterGroup oldPlaybackMenuGroup;
|
||||
|
||||
public PlaybackSpeedMenuFilterPatch() {
|
||||
// 0.05x litho speed menu.
|
||||
var playbackRateSelectorGroup = new StringFilterGroup(
|
||||
@@ -29,22 +22,13 @@ public final class PlaybackSpeedMenuFilterPatch extends Filter {
|
||||
"playback_rate_selector_menu_sheet.eml-js"
|
||||
);
|
||||
|
||||
// Old litho based speed menu.
|
||||
oldPlaybackMenuGroup = new StringFilterGroup(
|
||||
Settings.CUSTOM_SPEED_MENU,
|
||||
"playback_speed_sheet_content.eml-js");
|
||||
|
||||
addPathCallbacks(playbackRateSelectorGroup, oldPlaybackMenuGroup);
|
||||
addPathCallbacks(playbackRateSelectorGroup);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||
if (matchedGroup == oldPlaybackMenuGroup) {
|
||||
isOldPlaybackSpeedMenuVisible = true;
|
||||
} else {
|
||||
isPlaybackRateSelectorMenuVisible = true;
|
||||
}
|
||||
isPlaybackRateSelectorMenuVisible = true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -143,12 +143,14 @@ public final class ShortsFilter extends Filter {
|
||||
|
||||
StringFilterGroup likeButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_LIKE_BUTTON,
|
||||
"shorts_like_button.eml"
|
||||
"shorts_like_button.eml",
|
||||
"reel_like_button.eml"
|
||||
);
|
||||
|
||||
StringFilterGroup dislikeButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_DISLIKE_BUTTON,
|
||||
"shorts_dislike_button.eml"
|
||||
"shorts_dislike_button.eml",
|
||||
"reel_dislike_button.eml"
|
||||
);
|
||||
|
||||
joinButton = new StringFilterGroup(
|
||||
@@ -168,12 +170,13 @@ public final class ShortsFilter extends Filter {
|
||||
|
||||
shortsActionBar = new StringFilterGroup(
|
||||
null,
|
||||
"shorts_action_bar.eml"
|
||||
"shorts_action_bar.eml",
|
||||
"reel_action_bar.eml"
|
||||
);
|
||||
|
||||
actionButton = new StringFilterGroup(
|
||||
null,
|
||||
// Can be simply 'button.eml' or 'shorts_video_action_button.eml'
|
||||
// Can be simply 'button.eml', 'shorts_video_action_button.eml' or 'reel_action_button.eml'
|
||||
"button.eml"
|
||||
);
|
||||
|
||||
@@ -195,15 +198,18 @@ public final class ShortsFilter extends Filter {
|
||||
videoActionButtonGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
|
||||
"reel_comment_button"
|
||||
"reel_comment_button",
|
||||
"youtube_shorts_comment_outline"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SHARE_BUTTON,
|
||||
"reel_share_button"
|
||||
"reel_share_button",
|
||||
"youtube_shorts_share_outline"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_REMIX_BUTTON,
|
||||
"reel_remix_button"
|
||||
"reel_remix_button",
|
||||
"youtube_shorts_remix_outline"
|
||||
)
|
||||
);
|
||||
|
||||
@@ -211,6 +217,12 @@ public final class ShortsFilter extends Filter {
|
||||
// Suggested actions.
|
||||
//
|
||||
suggestedActionsGroupList.addAll(
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_PREVIEW_COMMENT,
|
||||
// Preview comment that can popup while a Short is playing.
|
||||
// Uses no bundled icons, and instead the users profile photo is shown.
|
||||
"shorts-comments-panel"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SHOP_BUTTON,
|
||||
"yt_outline_bag_"
|
||||
@@ -255,6 +267,10 @@ public final class ShortsFilter extends Filter {
|
||||
Settings.HIDE_SHORTS_GREEN_SCREEN_BUTTON,
|
||||
"greenscreen_temp"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_NEW_POSTS_BUTTON,
|
||||
"yt_outline_box_pencil"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_HASHTAG_BUTTON,
|
||||
"yt_outline_hashtag_"
|
||||
|
||||
@@ -1,24 +1,57 @@
|
||||
package app.revanced.extension.youtube.patches.playback.speed;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.icu.text.NumberFormat;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Function;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.ThemeHelper;
|
||||
import app.revanced.extension.youtube.patches.VideoInformation;
|
||||
import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilterPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
import app.revanced.extension.youtube.shared.PlayerType;
|
||||
import kotlin.Unit;
|
||||
import kotlin.jvm.functions.Function1;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class CustomPlaybackSpeedPatch {
|
||||
|
||||
/**
|
||||
* Maximum playback speed, exclusive value. Custom speeds must be less than this value.
|
||||
* Maximum playback speed, inclusive. Custom speeds must be this or less.
|
||||
* <p>
|
||||
* Going over 8x does not increase the actual playback speed any higher,
|
||||
* and the UI selector starts flickering and acting weird.
|
||||
@@ -26,6 +59,11 @@ public class CustomPlaybackSpeedPatch {
|
||||
*/
|
||||
public static final float PLAYBACK_SPEED_MAXIMUM = 8;
|
||||
|
||||
/**
|
||||
* Scale used to convert user speed to {@link android.widget.ProgressBar#setProgress(int)}.
|
||||
*/
|
||||
private static final float PROGRESS_BAR_VALUE_SCALE = 100;
|
||||
|
||||
/**
|
||||
* Tap and hold speed.
|
||||
*/
|
||||
@@ -34,16 +72,28 @@ public class CustomPlaybackSpeedPatch {
|
||||
/**
|
||||
* Custom playback speeds.
|
||||
*/
|
||||
public static float[] customPlaybackSpeeds;
|
||||
public static final float[] customPlaybackSpeeds;
|
||||
|
||||
/**
|
||||
* The last time the old playback menu was forcefully called.
|
||||
* Formats speeds to UI strings.
|
||||
*/
|
||||
private static long lastTimeOldPlaybackMenuInvoked;
|
||||
private static final NumberFormat speedFormatter = NumberFormat.getNumberInstance();
|
||||
|
||||
/**
|
||||
* Weak reference to the currently open dialog.
|
||||
*/
|
||||
private static WeakReference<Dialog> currentDialog = new WeakReference<>(null);
|
||||
|
||||
/**
|
||||
* Minimum and maximum custom playback speeds of {@link #customPlaybackSpeeds}.
|
||||
*/
|
||||
private static final float customPlaybackSpeedsMin, customPlaybackSpeedsMax;
|
||||
|
||||
static {
|
||||
final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get();
|
||||
// Cap at 2 decimals (rounds automatically).
|
||||
speedFormatter.setMaximumFractionDigits(2);
|
||||
|
||||
final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get();
|
||||
if (holdSpeed > 0 && holdSpeed <= PLAYBACK_SPEED_MAXIMUM) {
|
||||
TAP_AND_HOLD_SPEED = holdSpeed;
|
||||
} else {
|
||||
@@ -51,7 +101,9 @@ public class CustomPlaybackSpeedPatch {
|
||||
TAP_AND_HOLD_SPEED = Settings.SPEED_TAP_AND_HOLD.resetToDefault();
|
||||
}
|
||||
|
||||
loadCustomSpeeds();
|
||||
customPlaybackSpeeds = loadCustomSpeeds();
|
||||
customPlaybackSpeedsMin = customPlaybackSpeeds[0];
|
||||
customPlaybackSpeedsMax = customPlaybackSpeeds[customPlaybackSpeeds.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,37 +117,41 @@ public class CustomPlaybackSpeedPatch {
|
||||
Utils.showToastLong(str("revanced_custom_playback_speeds_invalid", PLAYBACK_SPEED_MAXIMUM));
|
||||
}
|
||||
|
||||
private static void loadCustomSpeeds() {
|
||||
private static float[] loadCustomSpeeds() {
|
||||
try {
|
||||
String[] speedStrings = Settings.CUSTOM_PLAYBACK_SPEEDS.get().split("\\s+");
|
||||
// Automatically replace commas with periods,
|
||||
// if the user added speeds in a localized format.
|
||||
String[] speedStrings = Settings.CUSTOM_PLAYBACK_SPEEDS.get()
|
||||
.replace(',', '.').split("\\s+");
|
||||
Arrays.sort(speedStrings);
|
||||
if (speedStrings.length == 0) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
customPlaybackSpeeds = new float[speedStrings.length];
|
||||
float[] speeds = new float[speedStrings.length];
|
||||
|
||||
int i = 0;
|
||||
for (String speedString : speedStrings) {
|
||||
final float speedFloat = Float.parseFloat(speedString);
|
||||
if (speedFloat <= 0 || arrayContains(customPlaybackSpeeds, speedFloat)) {
|
||||
if (speedFloat <= 0 || arrayContains(speeds, speedFloat)) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
if (speedFloat >= PLAYBACK_SPEED_MAXIMUM) {
|
||||
if (speedFloat > PLAYBACK_SPEED_MAXIMUM) {
|
||||
showInvalidCustomSpeedToast();
|
||||
Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
|
||||
loadCustomSpeeds();
|
||||
return;
|
||||
return loadCustomSpeeds();
|
||||
}
|
||||
|
||||
customPlaybackSpeeds[i++] = speedFloat;
|
||||
speeds[i++] = speedFloat;
|
||||
}
|
||||
|
||||
return speeds;
|
||||
} catch (Exception ex) {
|
||||
Logger.printInfo(() -> "parse error", ex);
|
||||
Utils.showToastLong(str("revanced_custom_playback_speeds_parse_exception"));
|
||||
Logger.printInfo(() -> "Parse error", ex);
|
||||
Utils.showToastShort(str("revanced_custom_playback_speeds_parse_exception"));
|
||||
Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
|
||||
loadCustomSpeeds();
|
||||
return loadCustomSpeeds();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,38 +169,28 @@ public class CustomPlaybackSpeedPatch {
|
||||
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
||||
try {
|
||||
if (PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible) {
|
||||
if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 5)) {
|
||||
if (hideLithoMenuAndShowCustomSpeedMenu(recyclerView, 5)) {
|
||||
PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "isPlaybackRateSelectorMenuVisible failure", ex);
|
||||
}
|
||||
|
||||
try {
|
||||
if (PlaybackSpeedMenuFilterPatch.isOldPlaybackSpeedMenuVisible) {
|
||||
if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 8)) {
|
||||
PlaybackSpeedMenuFilterPatch.isOldPlaybackSpeedMenuVisible = false;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "isOldPlaybackSpeedMenuVisible failure", ex);
|
||||
Logger.printException(() -> "onFlyoutMenuCreate failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean hideLithoMenuAndShowOldSpeedMenu(RecyclerView recyclerView, int expectedChildCount) {
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static boolean hideLithoMenuAndShowCustomSpeedMenu(RecyclerView recyclerView, int expectedChildCount) {
|
||||
if (recyclerView.getChildCount() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
View firstChild = recyclerView.getChildAt(0);
|
||||
if (!(firstChild instanceof ViewGroup PlaybackSpeedParentView)) {
|
||||
if (!(firstChild instanceof ViewGroup playbackSpeedParentView)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (PlaybackSpeedParentView.getChildCount() != expectedChildCount) {
|
||||
if (playbackSpeedParentView.getChildCount() != expectedChildCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -168,23 +214,418 @@ public class CustomPlaybackSpeedPatch {
|
||||
((ViewGroup) parentView3rd).setVisibility(View.GONE);
|
||||
((ViewGroup) parentView4th).setVisibility(View.GONE);
|
||||
|
||||
// Close the litho speed menu and show the old one.
|
||||
showOldPlaybackSpeedMenu();
|
||||
// Close the litho speed menu and show the modern custom speed dialog.
|
||||
showModernCustomPlaybackSpeedDialog(recyclerView.getContext());
|
||||
Logger.printDebug(() -> "Modern playback speed dialog shown");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void showOldPlaybackSpeedMenu() {
|
||||
// This method is sometimes used multiple times.
|
||||
// To prevent this, ignore method reuse within 1 second.
|
||||
final long now = System.currentTimeMillis();
|
||||
if (now - lastTimeOldPlaybackMenuInvoked < 1000) {
|
||||
Logger.printDebug(() -> "Ignoring call to showOldPlaybackSpeedMenu");
|
||||
return;
|
||||
}
|
||||
lastTimeOldPlaybackMenuInvoked = now;
|
||||
Logger.printDebug(() -> "Old video quality menu shown");
|
||||
/**
|
||||
* Displays a modern custom dialog for adjusting video playback speed.
|
||||
* <p>
|
||||
* This method creates a dialog with a slider, plus/minus buttons, and preset speed buttons
|
||||
* to allow the user to modify the video playback speed. The dialog is styled with rounded
|
||||
* corners and themed colors, positioned at the bottom of the screen. The playback speed
|
||||
* can be adjusted in 0.05 increments using the slider or buttons, or set directly to preset
|
||||
* values. The dialog updates the displayed speed in real-time and applies changes to the
|
||||
* video playback. The dialog is dismissed if the player enters Picture-in-Picture (PiP) mode.
|
||||
*/
|
||||
@SuppressLint("SetTextI18n")
|
||||
public static void showModernCustomPlaybackSpeedDialog(Context context) {
|
||||
// Create a dialog without a theme for custom appearance.
|
||||
Dialog dialog = new Dialog(context);
|
||||
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
|
||||
|
||||
// Rest of the implementation added by patch.
|
||||
// Store the dialog reference.
|
||||
currentDialog = new WeakReference<>(dialog);
|
||||
|
||||
// Create main vertical LinearLayout for dialog content.
|
||||
LinearLayout mainLayout = new LinearLayout(context);
|
||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
// Preset size constants.
|
||||
final int dip4 = dipToPixels(4); // Height for handle bar.
|
||||
final int dip5 = dipToPixels(5);
|
||||
final int dip6 = dipToPixels(6); // Padding for mainLayout from bottom.
|
||||
final int dip8 = dipToPixels(8); // Padding for mainLayout from left and right.
|
||||
final int dip20 = dipToPixels(20);
|
||||
final int dip32 = dipToPixels(32); // Height for in-rows speed buttons.
|
||||
final int dip36 = dipToPixels(36); // Height for minus and plus buttons.
|
||||
final int dip40 = dipToPixels(40); // Width for handle bar.
|
||||
final int dip60 = dipToPixels(60); // Height for speed button container.
|
||||
|
||||
mainLayout.setPadding(dip5, dip8, dip5, dip8);
|
||||
|
||||
// Set rounded rectangle background for the main layout.
|
||||
RoundRectShape roundRectShape = new RoundRectShape(
|
||||
createCornerRadii(12), null, null);
|
||||
ShapeDrawable background = new ShapeDrawable(roundRectShape);
|
||||
background.getPaint().setColor(ThemeHelper.getDialogBackgroundColor());
|
||||
mainLayout.setBackground(background);
|
||||
|
||||
// Add handle bar at the top.
|
||||
View handleBar = new View(context);
|
||||
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
|
||||
createCornerRadii(4), null, null));
|
||||
handleBackground.getPaint().setColor(getAdjustedBackgroundColor(true));
|
||||
handleBar.setBackground(handleBackground);
|
||||
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(
|
||||
dip40, // handle bar width.
|
||||
dip4 // handle bar height.
|
||||
);
|
||||
handleParams.gravity = Gravity.CENTER_HORIZONTAL; // Center horizontally.
|
||||
handleParams.setMargins(0, 0, 0, dip20); // 20dp bottom margins.
|
||||
handleBar.setLayoutParams(handleParams);
|
||||
// Add handle bar view to main layout.
|
||||
mainLayout.addView(handleBar);
|
||||
|
||||
// Display current playback speed.
|
||||
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.setTextColor(ThemeHelper.getForegroundColor());
|
||||
currentSpeedText.setTextSize(16);
|
||||
currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD);
|
||||
currentSpeedText.setGravity(Gravity.CENTER);
|
||||
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
textParams.setMargins(0, 0, 0, 0);
|
||||
currentSpeedText.setLayoutParams(textParams);
|
||||
// Add current speed text view to main layout.
|
||||
mainLayout.addView(currentSpeedText);
|
||||
|
||||
// Create horizontal layout for slider and +/- buttons.
|
||||
LinearLayout sliderLayout = new LinearLayout(context);
|
||||
sliderLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
sliderLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||
sliderLayout.setPadding(dip5, dip5, dip5, dip5); // 5dp padding.
|
||||
|
||||
// Create minus button.
|
||||
Button minusButton = new Button(context, null, 0); // Disable default theme style.
|
||||
minusButton.setText(""); // No text on button.
|
||||
ShapeDrawable minusBackground = new ShapeDrawable(new RoundRectShape(createCornerRadii(20), null, null));
|
||||
minusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
|
||||
minusButton.setBackground(minusBackground);
|
||||
OutlineSymbolDrawable minusDrawable = new OutlineSymbolDrawable(false); // Minus symbol.
|
||||
minusButton.setForeground(minusDrawable);
|
||||
LinearLayout.LayoutParams minusParams = new LinearLayout.LayoutParams(dip36, dip36);
|
||||
minusParams.setMargins(0, 0, dip5, 0); // 5dp to slider.
|
||||
minusButton.setLayoutParams(minusParams);
|
||||
|
||||
// Create slider for speed adjustment.
|
||||
SeekBar speedSlider = new SeekBar(context);
|
||||
speedSlider.setMax(speedToProgressValue(customPlaybackSpeedsMax));
|
||||
speedSlider.setProgress(speedToProgressValue(currentSpeed));
|
||||
speedSlider.getProgressDrawable().setColorFilter(
|
||||
ThemeHelper.getForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar.
|
||||
speedSlider.getThumb().setColorFilter(
|
||||
ThemeHelper.getForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme slider thumb.
|
||||
LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams(
|
||||
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
|
||||
sliderParams.setMargins(dip5, 0, dip5, 0); // 5dp to -/+ buttons.
|
||||
speedSlider.setLayoutParams(sliderParams);
|
||||
|
||||
// Create plus button.
|
||||
Button plusButton = new Button(context, null, 0); // Disable default theme style.
|
||||
plusButton.setText(""); // No text on button.
|
||||
ShapeDrawable plusBackground = new ShapeDrawable(new RoundRectShape(
|
||||
createCornerRadii(20), null, null));
|
||||
plusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
|
||||
plusButton.setBackground(plusBackground);
|
||||
OutlineSymbolDrawable plusDrawable = new OutlineSymbolDrawable(true); // Plus symbol.
|
||||
plusButton.setForeground(plusDrawable);
|
||||
LinearLayout.LayoutParams plusParams = new LinearLayout.LayoutParams(dip36, dip36);
|
||||
plusParams.setMargins(dip5, 0, 0, 0); // 5dp to slider.
|
||||
plusButton.setLayoutParams(plusParams);
|
||||
|
||||
// Add -/+ and slider views to slider layout.
|
||||
sliderLayout.addView(minusButton);
|
||||
sliderLayout.addView(speedSlider);
|
||||
sliderLayout.addView(plusButton);
|
||||
|
||||
LinearLayout.LayoutParams sliderLayoutParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
sliderLayoutParams.setMargins(0, 0, 0, dip5); // 5dp bottom margin.
|
||||
sliderLayout.setLayoutParams(sliderLayoutParams);
|
||||
|
||||
// Add slider layout to main layout.
|
||||
mainLayout.addView(sliderLayout);
|
||||
|
||||
Function<Float, Void> userSelectedSpeed = newSpeed -> {
|
||||
final float roundedSpeed = roundSpeedToNearestIncrement(newSpeed);
|
||||
if (VideoInformation.getPlaybackSpeed() == roundedSpeed) {
|
||||
// Nothing has changed. New speed rounds to the current speed.
|
||||
return null;
|
||||
}
|
||||
|
||||
VideoInformation.overridePlaybackSpeed(roundedSpeed);
|
||||
RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
|
||||
currentSpeedText.setText(formatSpeedStringX(roundedSpeed, 2)); // Update display.
|
||||
speedSlider.setProgress(speedToProgressValue(roundedSpeed)); // Update slider.
|
||||
return null;
|
||||
};
|
||||
|
||||
// Set listener for slider to update playback speed.
|
||||
speedSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
// Convert from progress value to video playback speed.
|
||||
userSelectedSpeed.apply(customPlaybackSpeedsMin + (progress / PROGRESS_BAR_VALUE_SCALE));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||
});
|
||||
|
||||
minusButton.setOnClickListener(v -> userSelectedSpeed.apply(
|
||||
VideoInformation.getPlaybackSpeed() - 0.05f));
|
||||
plusButton.setOnClickListener(v -> userSelectedSpeed.apply(
|
||||
VideoInformation.getPlaybackSpeed() + 0.05f));
|
||||
|
||||
// Create GridLayout for preset speed buttons.
|
||||
GridLayout gridLayout = new GridLayout(context);
|
||||
gridLayout.setColumnCount(5); // 5 columns for speed buttons.
|
||||
gridLayout.setAlignmentMode(GridLayout.ALIGN_BOUNDS);
|
||||
gridLayout.setRowCount((int) Math.ceil(customPlaybackSpeeds.length / 5.0));
|
||||
LinearLayout.LayoutParams gridParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
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").
|
||||
speedFormatter.setMinimumFractionDigits(1);
|
||||
|
||||
// Add buttons for each preset playback speed.
|
||||
for (float speed : customPlaybackSpeeds) {
|
||||
// Container for button and optional label.
|
||||
FrameLayout buttonContainer = new FrameLayout(context);
|
||||
|
||||
// Set layout parameters for each grid cell.
|
||||
GridLayout.LayoutParams containerParams = new GridLayout.LayoutParams();
|
||||
containerParams.width = 0; // Equal width for columns.
|
||||
containerParams.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1, 1f);
|
||||
containerParams.setMargins(dip5, 0, dip5, 0); // Button margins.
|
||||
containerParams.height = dip60; // Fixed height for button and label.
|
||||
buttonContainer.setLayoutParams(containerParams);
|
||||
|
||||
// Create speed button.
|
||||
Button speedButton = new Button(context, null, 0);
|
||||
speedButton.setText(speedFormatter.format(speed)); // Do not use 'x' speed format.
|
||||
speedButton.setTextColor(ThemeHelper.getForegroundColor());
|
||||
speedButton.setTextSize(12);
|
||||
speedButton.setAllCaps(false);
|
||||
speedButton.setGravity(Gravity.CENTER);
|
||||
|
||||
ShapeDrawable buttonBackground = new ShapeDrawable(new RoundRectShape(
|
||||
createCornerRadii(20), null, null));
|
||||
buttonBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
|
||||
speedButton.setBackground(buttonBackground);
|
||||
speedButton.setPadding(dip5, dip5, dip5, dip5);
|
||||
|
||||
// Center button vertically and stretch horizontally in container.
|
||||
FrameLayout.LayoutParams buttonParams = new FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT, dip32, Gravity.CENTER);
|
||||
speedButton.setLayoutParams(buttonParams);
|
||||
|
||||
// Add speed buttons view to buttons container layout.
|
||||
buttonContainer.addView(speedButton);
|
||||
|
||||
// Add "Normal" label for 1.0x speed.
|
||||
if (speed == 1.0f) {
|
||||
TextView normalLabel = new TextView(context);
|
||||
// Use same 'Normal' string as stock YouTube.
|
||||
normalLabel.setText(str("normal_playback_rate_label"));
|
||||
normalLabel.setTextColor(ThemeHelper.getForegroundColor());
|
||||
normalLabel.setTextSize(10);
|
||||
normalLabel.setGravity(Gravity.CENTER);
|
||||
|
||||
FrameLayout.LayoutParams labelParams = new FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
|
||||
labelParams.bottomMargin = 0; // Position label below button.
|
||||
normalLabel.setLayoutParams(labelParams);
|
||||
|
||||
buttonContainer.addView(normalLabel);
|
||||
}
|
||||
|
||||
speedButton.setOnClickListener(v -> userSelectedSpeed.apply(speed));
|
||||
|
||||
gridLayout.addView(buttonContainer);
|
||||
}
|
||||
|
||||
// Add in-rows speed buttons layout to main layout.
|
||||
mainLayout.addView(gridLayout);
|
||||
|
||||
// Wrap mainLayout in another LinearLayout for side margins.
|
||||
LinearLayout wrapperLayout = new LinearLayout(context);
|
||||
wrapperLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
wrapperLayout.setPadding(dip8, 0, dip8, 0); // 8dp side margins.
|
||||
wrapperLayout.addView(mainLayout);
|
||||
dialog.setContentView(wrapperLayout);
|
||||
|
||||
// Configure dialog window to appear at the bottom.
|
||||
Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
WindowManager.LayoutParams params = window.getAttributes();
|
||||
params.gravity = Gravity.BOTTOM; // Position at bottom of screen.
|
||||
params.y = dip6; // 6dp margin from bottom.
|
||||
// In landscape, use the smaller dimension (height) as portrait width.
|
||||
int portraitWidth = context.getResources().getDisplayMetrics().widthPixels;
|
||||
if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
portraitWidth = Math.min(
|
||||
portraitWidth,
|
||||
context.getResources().getDisplayMetrics().heightPixels);
|
||||
}
|
||||
params.width = portraitWidth; // Use portrait width.
|
||||
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||
window.setAttributes(params);
|
||||
window.setBackgroundDrawable(null); // Remove default dialog background.
|
||||
}
|
||||
|
||||
// Create observer for PlayerType changes.
|
||||
Function1<PlayerType, Unit> playerTypeObserver = new Function1<>() {
|
||||
@Override
|
||||
public Unit invoke(PlayerType type) {
|
||||
Dialog current = currentDialog.get();
|
||||
if (current == null || !current.isShowing()) {
|
||||
// Should never happen.
|
||||
PlayerType.getOnChange().removeObserver(this);
|
||||
Logger.printException(() -> "Removing player type listener as dialog is null or closed");
|
||||
} else if (type == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) {
|
||||
current.dismiss();
|
||||
Logger.printDebug(() -> "Playback speed dialog dismissed due to PiP mode");
|
||||
}
|
||||
return Unit.INSTANCE;
|
||||
}
|
||||
};
|
||||
|
||||
// Add observer to dismiss dialog when entering PiP mode.
|
||||
PlayerType.getOnChange().addObserver(playerTypeObserver);
|
||||
|
||||
// Remove observer when dialog is dismissed.
|
||||
dialog.setOnDismissListener(d -> {
|
||||
PlayerType.getOnChange().removeObserver(playerTypeObserver);
|
||||
Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss");
|
||||
});
|
||||
|
||||
// Apply slide-in animation when showing the dialog.
|
||||
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
|
||||
Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
|
||||
slideInABottomAnimation.setDuration(fadeDurationFast);
|
||||
mainLayout.startAnimation(slideInABottomAnimation);
|
||||
|
||||
dialog.show(); // Display the dialog.
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an array of corner radii for a rounded rectangle shape.
|
||||
*
|
||||
* @param dp The radius in density-independent pixels (dp) to apply to all corners.
|
||||
* @return An array of eight float values representing the corner radii
|
||||
* (top-left, top-right, bottom-right, bottom-left).
|
||||
*/
|
||||
private static float[] createCornerRadii(float dp) {
|
||||
final float radius = dipToPixels(dp);
|
||||
return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
return speedFormatter.format(speed) + 'x';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return user speed converted to a value for {@link SeekBar#setProgress(int)}.
|
||||
*/
|
||||
private static int speedToProgressValue(float speed) {
|
||||
return (int) ((speed - customPlaybackSpeedsMin) * PROGRESS_BAR_VALUE_SCALE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds the given playback speed to the nearest 0.05 increment and ensures it is within valid bounds.
|
||||
*
|
||||
* @param speed The playback speed to round.
|
||||
* @return The rounded speed, constrained to the specified bounds.
|
||||
*/
|
||||
private static float roundSpeedToNearestIncrement(float speed) {
|
||||
// Round to nearest 0.05 speed.
|
||||
final float roundedSpeed = Math.round(speed / 0.05f) * 0.05f;
|
||||
return Utils.clamp(roundedSpeed, 0.05f, PLAYBACK_SPEED_MAXIMUM);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the background color based on the current theme.
|
||||
*
|
||||
* @param isHandleBar If true, applies a stronger darkening factor (0.9) for the handle bar in light theme;
|
||||
* if false, applies a standard darkening factor (0.95) for other elements in light theme.
|
||||
* @return A modified background color, lightened by 20% for dark themes or darkened by 5% (or 10% for handle bar)
|
||||
* for light themes to ensure visual contrast.
|
||||
*/
|
||||
public static int getAdjustedBackgroundColor(boolean isHandleBar) {
|
||||
final int baseColor = ThemeHelper.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 ThemeHelper.isDarkTheme()
|
||||
? ThemeHelper.adjustColorBrightness(baseColor, darkThemeFactor) // Lighten for dark theme.
|
||||
: ThemeHelper.adjustColorBrightness(baseColor, lightThemeFactor); // Darken for light theme.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Drawable for rendering outlined plus and minus symbols on buttons.
|
||||
*/
|
||||
class OutlineSymbolDrawable extends Drawable {
|
||||
private final boolean isPlus; // Determines if the symbol is a plus or minus.
|
||||
private final Paint paint;
|
||||
|
||||
OutlineSymbolDrawable(boolean isPlus) {
|
||||
this.isPlus = isPlus;
|
||||
paint = new Paint(Paint.ANTI_ALIAS_FLAG); // Enable anti-aliasing for smooth rendering.
|
||||
paint.setColor(ThemeHelper.getForegroundColor());
|
||||
paint.setStyle(Paint.Style.STROKE); // Use stroke style for outline.
|
||||
paint.setStrokeWidth(dipToPixels(1)); // 1dp stroke width.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(Canvas canvas) {
|
||||
Rect bounds = getBounds();
|
||||
final int width = bounds.width();
|
||||
final int height = bounds.height();
|
||||
final float centerX = width / 2f; // Center X coordinate.
|
||||
final float centerY = height / 2f; // Center Y coordinate.
|
||||
final float size = Math.min(width, height) * 0.25f; // Symbol size is 25% of button dimensions.
|
||||
|
||||
// Draw horizontal line for both plus and minus symbols.
|
||||
canvas.drawLine(centerX - size, centerY, centerX + size, centerY, paint);
|
||||
if (isPlus) {
|
||||
// Draw vertical line for plus symbol.
|
||||
canvas.drawLine(centerX, centerY - size, centerX, centerY + size, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(ColorFilter colorFilter) {
|
||||
paint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.TRANSLUCENT;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,10 @@ public final class RememberPlaybackSpeedPatch {
|
||||
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
|
||||
try {
|
||||
if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
|
||||
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
|
||||
// With the 0.05x menu, if the speed is set by a patch to higher than 2.0x
|
||||
// then the menu will allow increasing without bounds but the max speed is
|
||||
// still capped to under 8.0x.
|
||||
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
|
||||
// still capped to 8.0x.
|
||||
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM);
|
||||
|
||||
// Prevent toast spamming if using the 0.05x adjustments.
|
||||
// Show exactly one toast after the user stops interacting with the speed menu.
|
||||
@@ -57,7 +57,7 @@ public final class RememberPlaybackSpeedPatch {
|
||||
}
|
||||
Settings.PLAYBACK_SPEED_DEFAULT.save(finalPlaybackSpeed);
|
||||
|
||||
Utils.showToastLong(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
|
||||
Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
|
||||
}, TOAST_DELAY_MILLISECONDS);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.revanced.extension.youtube.patches.theme;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.clamp;
|
||||
import static app.revanced.extension.youtube.patches.theme.ThemePatch.SplashScreenAnimationStyle;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
@@ -60,7 +61,7 @@ public final class SeekbarColorPatch {
|
||||
* this is the color value of {@link Settings#SEEKBAR_CUSTOM_COLOR_PRIMARY}.
|
||||
* Otherwise this is {@link #ORIGINAL_SEEKBAR_COLOR}.
|
||||
*/
|
||||
private static int customSeekbarColor = ORIGINAL_SEEKBAR_COLOR;
|
||||
private static final int customSeekbarColor;
|
||||
|
||||
/**
|
||||
* Custom seekbar hue, saturation, and brightness values.
|
||||
@@ -77,24 +78,25 @@ public final class SeekbarColorPatch {
|
||||
Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv);
|
||||
ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2];
|
||||
|
||||
if (SEEKBAR_CUSTOM_COLOR_ENABLED) {
|
||||
loadCustomSeekbarColor();
|
||||
}
|
||||
customSeekbarColor = SEEKBAR_CUSTOM_COLOR_ENABLED
|
||||
? loadCustomSeekbarColor()
|
||||
: ORIGINAL_SEEKBAR_COLOR;
|
||||
}
|
||||
|
||||
private static void loadCustomSeekbarColor() {
|
||||
private static int loadCustomSeekbarColor() {
|
||||
try {
|
||||
customSeekbarColor = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_PRIMARY.get());
|
||||
Color.colorToHSV(customSeekbarColor, customSeekbarColorHSV);
|
||||
|
||||
customSeekbarColorGradient[0] = customSeekbarColor;
|
||||
final int color = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_PRIMARY.get());
|
||||
Color.colorToHSV(color, customSeekbarColorHSV);
|
||||
customSeekbarColorGradient[0] = color;
|
||||
customSeekbarColorGradient[1] = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_ACCENT.get());
|
||||
|
||||
return color;
|
||||
} catch (Exception ex) {
|
||||
Utils.showToastShort(str("revanced_seekbar_custom_color_invalid"));
|
||||
Settings.SEEKBAR_CUSTOM_COLOR_PRIMARY.resetToDefault();
|
||||
Settings.SEEKBAR_CUSTOM_COLOR_ACCENT.resetToDefault();
|
||||
|
||||
loadCustomSeekbarColor();
|
||||
return loadCustomSeekbarColor();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +116,7 @@ public final class SeekbarColorPatch {
|
||||
: (int) channel3Bits;
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private static String get9BitStyleIdentifier(int color24Bit) {
|
||||
final int r3 = colorChannelTo3Bits(Color.red(color24Bit));
|
||||
final int g3 = colorChannelTo3Bits(Color.green(color24Bit));
|
||||
@@ -171,23 +174,15 @@ public final class SeekbarColorPatch {
|
||||
*/
|
||||
public static void setSplashAnimationLottie(LottieAnimationView view, int resourceId) {
|
||||
try {
|
||||
if (!SEEKBAR_CUSTOM_COLOR_ENABLED) {
|
||||
SplashScreenAnimationStyle animationStyle = Settings.SPLASH_SCREEN_ANIMATION_STYLE.get();
|
||||
if (!SEEKBAR_CUSTOM_COLOR_ENABLED
|
||||
// Black and white animations cannot use color replacements.
|
||||
|| animationStyle == SplashScreenAnimationStyle.FPS_30_BLACK_AND_WHITE
|
||||
|| animationStyle == SplashScreenAnimationStyle.FPS_60_BLACK_AND_WHITE) {
|
||||
view.patch_setAnimation(resourceId);
|
||||
return;
|
||||
}
|
||||
|
||||
//noinspection ConstantConditions
|
||||
if (false) { // Set true to force slow animation for development.
|
||||
final int longAnimation = Utils.getResourceIdentifier(
|
||||
Utils.isDarkModeEnabled()
|
||||
? "startup_animation_5s_30fps_dark"
|
||||
: "startup_animation_5s_30fps_light",
|
||||
"raw");
|
||||
if (longAnimation != 0) {
|
||||
resourceId = longAnimation;
|
||||
}
|
||||
}
|
||||
|
||||
// Must specify primary key name otherwise the morphing YT logo color is also changed.
|
||||
String originalKey = "\"k\":";
|
||||
String originalPrimary = originalKey + "[1,0,0.2,1]";
|
||||
@@ -197,21 +192,16 @@ public final class SeekbarColorPatch {
|
||||
String replacementAccent = originalKey + getColorStringArray(customSeekbarColorGradient[1]);
|
||||
|
||||
String json = loadRawResourceAsString(resourceId);
|
||||
if (json == null) {
|
||||
return; // Should never happen.
|
||||
}
|
||||
String replacement = json
|
||||
.replace(originalPrimary, replacementPrimary)
|
||||
.replace(originalAccent, replacementAccent);
|
||||
|
||||
if (BaseSettings.DEBUG.get() && (!json.contains(originalPrimary) || !json.contains(originalAccent))) {
|
||||
String jsonFinal = json;
|
||||
Logger.printException(() -> "Could not replace launch animation colors: " + jsonFinal);
|
||||
Logger.printException(() -> "Could not replace splash animation colors: " + json);
|
||||
}
|
||||
|
||||
Logger.printDebug(() -> "Replacing Lottie animation JSON");
|
||||
json = json.replace(originalPrimary, replacementPrimary);
|
||||
json = json.replace(originalAccent, replacementAccent);
|
||||
|
||||
// cacheKey is not needed since the animation will not be reused.
|
||||
view.patch_setAnimation(new ByteArrayInputStream(json.getBytes()), null);
|
||||
view.patch_setAnimation(new ByteArrayInputStream(replacement.getBytes()), null);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setSplashAnimationLottie failure", ex);
|
||||
}
|
||||
@@ -232,8 +222,7 @@ public final class SeekbarColorPatch {
|
||||
Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name()).useDelimiter("\\A")) {
|
||||
return scanner.next();
|
||||
} catch (IOException e) {
|
||||
Logger.printException(() -> "Could not load resource: " + resourceId);
|
||||
return null;
|
||||
throw new IllegalStateException("Could not load resource: " + resourceId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,49 @@
|
||||
package app.revanced.extension.youtube.patches.theme;
|
||||
|
||||
import static app.revanced.extension.youtube.patches.theme.ThemePatch.SplashScreenAnimationStyle.styleFromOrdinal;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.ThemeHelper;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ThemePatch {
|
||||
|
||||
public enum SplashScreenAnimationStyle {
|
||||
DEFAULT(0),
|
||||
FPS_60_ONE_SECOND(1),
|
||||
FPS_60_TWO_SECOND(2),
|
||||
FPS_60_FIVE_SECOND(3),
|
||||
FPS_60_BLACK_AND_WHITE(4),
|
||||
FPS_30_ONE_SECOND(5),
|
||||
FPS_30_TWO_SECOND(6),
|
||||
FPS_30_FIVE_SECOND(7),
|
||||
FPS_30_BLACK_AND_WHITE(8);
|
||||
// There exists a 10th json style used as the switch statement default,
|
||||
// but visually it is identical to 60fps one second.
|
||||
|
||||
@Nullable
|
||||
static SplashScreenAnimationStyle styleFromOrdinal(int style) {
|
||||
// Alternatively can return using values()[style]
|
||||
for (SplashScreenAnimationStyle value : values()) {
|
||||
if (value.style == style) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
final int style;
|
||||
|
||||
SplashScreenAnimationStyle(int style) {
|
||||
this.style = style;
|
||||
}
|
||||
}
|
||||
|
||||
// color constants used in relation with litho components
|
||||
private static final int[] WHITE_VALUES = {
|
||||
-1, // comments chip background
|
||||
@@ -58,4 +96,22 @@ public class ThemePatch {
|
||||
public static boolean gradientLoadingScreenEnabled(boolean original) {
|
||||
return GRADIENT_LOADING_SCREEN_ENABLED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getLoadingScreenType(int original) {
|
||||
SplashScreenAnimationStyle style = Settings.SPLASH_SCREEN_ANIMATION_STYLE.get();
|
||||
if (style == SplashScreenAnimationStyle.DEFAULT) {
|
||||
return original;
|
||||
}
|
||||
|
||||
final int replacement = style.style;
|
||||
if (original != replacement) {
|
||||
Logger.printDebug(() -> "Overriding splash screen style from: "
|
||||
+ styleFromOrdinal(original) + " to: " + style);
|
||||
}
|
||||
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,12 @@ import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerT
|
||||
import static app.revanced.extension.youtube.patches.OpenShortsInRegularPlayerPatch.ShortsPlayerType;
|
||||
import static app.revanced.extension.youtube.patches.SeekbarThumbnailsPatch.SeekbarThumbnailsHighQualityAvailability;
|
||||
import static app.revanced.extension.youtube.patches.components.PlayerFlyoutMenuItemsFilter.HideAudioFlyoutMenuAvailability;
|
||||
import static app.revanced.extension.youtube.patches.theme.ThemePatch.SplashScreenAnimationStyle;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.IGNORE;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE;
|
||||
|
||||
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
@@ -40,12 +38,14 @@ import app.revanced.extension.shared.settings.IntegerSetting;
|
||||
import app.revanced.extension.shared.settings.LongSetting;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.DeArrowAvailability;
|
||||
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.StillImagesAvailability;
|
||||
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption;
|
||||
import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.ThumbnailStillTime;
|
||||
import app.revanced.extension.youtube.patches.MiniplayerPatch;
|
||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
|
||||
|
||||
public class Settings extends BaseSettings {
|
||||
// Video
|
||||
@@ -63,7 +63,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting CUSTOM_SPEED_MENU = new BooleanSetting("revanced_custom_speed_menu", TRUE);
|
||||
public static final FloatSetting PLAYBACK_SPEED_DEFAULT = new FloatSetting("revanced_playback_speed_default", -2.0f);
|
||||
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds",
|
||||
"0.25\n0.5\n0.75\n0.9\n0.95\n1.0\n1.05\n1.1\n1.25\n1.5\n1.75\n2.0\n3.0\n4.0\n5.0", true);
|
||||
"0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.5\n3.0\n4.0\n5.0\n6.0\n7.0\n8.0", true);
|
||||
// Audio
|
||||
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", FALSE, new ForceOriginalAudioAvailability());
|
||||
|
||||
@@ -135,6 +135,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_autoplay_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_captions_button", FALSE);
|
||||
public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE, true);
|
||||
public static final BooleanSetting HIDE_PLAYER_CONTROL_BUTTONS_BACKGROUND = new BooleanSetting("revanced_hide_player_control_buttons_background", FALSE, true);
|
||||
public static final BooleanSetting HIDE_CHANNEL_BAR = new BooleanSetting("revanced_hide_channel_bar", FALSE);
|
||||
public static final BooleanSetting HIDE_CHANNEL_MEMBER_SHELF = new BooleanSetting("revanced_hide_channel_member_shelf", TRUE);
|
||||
public static final BooleanSetting HIDE_COMMUNITY_GUIDELINES = new BooleanSetting("revanced_hide_community_guidelines", TRUE);
|
||||
@@ -179,7 +180,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_COMMENTS_AI_SUMMARY = new BooleanSetting("revanced_hide_comments_ai_summary", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_BY_MEMBERS_HEADER = new BooleanSetting("revanced_hide_comments_by_members_header", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_CREATE_A_SHORT_BUTTON = new BooleanSetting("revanced_hide_comments_create_a_short_button", TRUE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_TIMESTAMP_AND_EMOJI_BUTTONS = new BooleanSetting("revanced_hide_comments_timestamp_and_emoji_buttons", TRUE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_TIMESTAMP_BUTTON = new BooleanSetting("revanced_hide_comments_timestamp_button", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_PREVIEW_COMMENT = new BooleanSetting("revanced_hide_comments_preview_comment", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_SECTION = new BooleanSetting("revanced_hide_comments_section", FALSE);
|
||||
public static final BooleanSetting HIDE_COMMENTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_comments_thanks_button", TRUE);
|
||||
@@ -226,6 +227,8 @@ public class Settings extends BaseSettings {
|
||||
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
|
||||
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
|
||||
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<>("splash_screen_animation_style", SplashScreenAnimationStyle.FPS_60_ONE_SECOND, true);
|
||||
|
||||
public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE,
|
||||
"revanced_remove_viewer_discretion_dialog_user_dialog_message");
|
||||
public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", FALSE, true, "revanced_spoof_app_version_user_dialog_message");
|
||||
@@ -260,6 +263,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_SHORTS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_dislike_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_GREEN_SCREEN_BUTTON = new BooleanSetting("revanced_hide_shorts_green_screen_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_NEW_POSTS_BUTTON = new BooleanSetting("revanced_hide_shorts_new_posts_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_HASHTAG_BUTTON = new BooleanSetting("revanced_hide_shorts_hashtag_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_HISTORY = new BooleanSetting("revanced_hide_shorts_history", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_HOME = new BooleanSetting("revanced_hide_shorts_home", FALSE);
|
||||
@@ -275,6 +279,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_SHORTS_SEARCH = new BooleanSetting("revanced_hide_shorts_search", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS = new BooleanSetting("revanced_hide_shorts_search_suggestions", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHARE_BUTTON = new BooleanSetting("revanced_hide_shorts_share_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_PREVIEW_COMMENT = new BooleanSetting("revanced_hide_shorts_preview_comment", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SHOP_BUTTON = new BooleanSetting("revanced_hide_shorts_shop_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_sound_button", FALSE);
|
||||
public static final BooleanSetting HIDE_SHORTS_SOUND_METADATA_LABEL = new BooleanSetting("revanced_hide_shorts_sound_metadata_label", FALSE);
|
||||
@@ -309,16 +314,16 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting AUTO_REPEAT = new BooleanSetting("revanced_auto_repeat", FALSE);
|
||||
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
|
||||
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
|
||||
public static final BooleanSetting DISABLE_ZOOM_HAPTICS = new BooleanSetting("revanced_disable_zoom_haptics", TRUE);
|
||||
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE);
|
||||
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING = new BooleanSetting("revanced_disable_haptic_feedback_precise_seeking", FALSE);
|
||||
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO = new BooleanSetting("revanced_disable_haptic_feedback_seek_undo", FALSE);
|
||||
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_ZOOM = new BooleanSetting("revanced_disable_haptic_feedback_zoom", FALSE);
|
||||
public static final BooleanSetting EXTERNAL_BROWSER = new BooleanSetting("revanced_external_browser", TRUE, true);
|
||||
public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE);
|
||||
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true,
|
||||
"revanced_spoof_device_dimensions_user_dialog_message");
|
||||
/**
|
||||
* When enabled, share the debug logs with care.
|
||||
* The buffer contains select user data, including the client ip address and information that could identify the end user.
|
||||
*/
|
||||
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, parent(BaseSettings.DEBUG));
|
||||
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
|
||||
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
|
||||
|
||||
// Swipe controls
|
||||
public static final BooleanSetting SWIPE_CHANGE_VIDEO = new BooleanSetting("revanced_swipe_change_video", FALSE, true);
|
||||
@@ -337,13 +342,17 @@ public class Settings extends BaseSettings {
|
||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
||||
public static final IntegerSetting SWIPE_OVERLAY_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true,
|
||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
||||
public static final StringSetting SWIPE_OVERLAY_PROGRESS_COLOR = new StringSetting("revanced_swipe_overlay_progress_color", "#FFFFFF", true,
|
||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
||||
public static final StringSetting SWIPE_OVERLAY_BRIGHTNESS_COLOR = new StringSetting("revanced_swipe_overlay_progress_brightness_color", "#FFFFFF", true,
|
||||
parent(SWIPE_BRIGHTNESS));
|
||||
public static final StringSetting SWIPE_OVERLAY_VOLUME_COLOR = new StringSetting("revanced_swipe_overlay_progress_volume_color", "#FFFFFF", true,
|
||||
parent(SWIPE_VOLUME));
|
||||
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true,
|
||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
||||
public static final BooleanSetting SWIPE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_swipe_save_and_restore_brightness", TRUE, true, parent(SWIPE_BRIGHTNESS));
|
||||
public static final BooleanSetting SWIPE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_swipe_save_and_restore_brightness", TRUE, true,
|
||||
parent(SWIPE_BRIGHTNESS));
|
||||
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1f);
|
||||
public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true, parent(SWIPE_BRIGHTNESS));
|
||||
public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true,
|
||||
parent(SWIPE_BRIGHTNESS));
|
||||
|
||||
// ReturnYoutubeDislike
|
||||
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("revanced_ryd_enabled", TRUE);
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.preference.Preference;
|
||||
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
||||
|
||||
/**
|
||||
* A custom preference that clears the ReVanced debug log buffer when clicked.
|
||||
* Invokes the {@link LogBufferManager#clearLogBuffer} method.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public class ClearLogBufferPreference extends Preference {
|
||||
|
||||
{
|
||||
setOnPreferenceClickListener(pref -> {
|
||||
LogBufferManager.clearLogBuffer();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
public ClearLogBufferPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
public ClearLogBufferPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.preference.Preference;
|
||||
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
||||
|
||||
/**
|
||||
* A custom preference that triggers exporting ReVanced debug logs to the clipboard when clicked.
|
||||
* Invokes the {@link LogBufferManager#exportToClipboard} method.
|
||||
*/
|
||||
@SuppressWarnings({"deprecation", "unused"})
|
||||
public class ExportLogToClipboardPreference extends Preference {
|
||||
|
||||
{
|
||||
setOnPreferenceClickListener(pref -> {
|
||||
LogBufferManager.exportToClipboard();
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
public ExportLogToClipboardPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
public ExportLogToClipboardPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
@@ -376,7 +376,11 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
|
||||
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
||||
Utils.setClipboard(getEditText().getText().toString());
|
||||
try {
|
||||
Utils.setClipboard(getEditText().getText());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Copy settings failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -433,7 +437,11 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
|
||||
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
||||
Utils.setClipboard(getEditText().getText().toString());
|
||||
try {
|
||||
Utils.setClipboard(getEditText().getText());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Copy settings failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package app.revanced.extension.youtube.swipecontrols
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color
|
||||
import app.revanced.extension.shared.Logger
|
||||
import app.revanced.extension.shared.StringRef.str
|
||||
import app.revanced.extension.shared.Utils
|
||||
import app.revanced.extension.shared.settings.StringSetting
|
||||
import app.revanced.extension.youtube.settings.Settings
|
||||
import app.revanced.extension.youtube.shared.PlayerType
|
||||
|
||||
@@ -51,105 +51,112 @@ class SwipeControlsConfigurationProvider {
|
||||
/**
|
||||
* Indicates whether press-to-swipe mode is enabled, requiring a press before swiping to activate controls.
|
||||
*/
|
||||
val shouldEnablePressToSwipe: Boolean
|
||||
get() = Settings.SWIPE_PRESS_TO_ENGAGE.get()
|
||||
val shouldEnablePressToSwipe = Settings.SWIPE_PRESS_TO_ENGAGE.get()
|
||||
|
||||
/**
|
||||
* The threshold for detecting swipe gestures, in pixels.
|
||||
* Loaded once to ensure consistent behavior during rapid scroll events.
|
||||
*/
|
||||
val swipeMagnitudeThreshold: Int
|
||||
get() = Settings.SWIPE_MAGNITUDE_THRESHOLD.get()
|
||||
val swipeMagnitudeThreshold = Settings.SWIPE_MAGNITUDE_THRESHOLD.get()
|
||||
|
||||
/**
|
||||
* The sensitivity of volume swipe gestures, determining how much volume changes per swipe.
|
||||
* Resets to default if set to 0, as it would disable swiping.
|
||||
*/
|
||||
val volumeSwipeSensitivity: Int
|
||||
get() {
|
||||
val sensitivity = Settings.SWIPE_VOLUME_SENSITIVITY.get()
|
||||
val volumeSwipeSensitivity: Int by lazy {
|
||||
val sensitivity = Settings.SWIPE_VOLUME_SENSITIVITY.get()
|
||||
|
||||
if (sensitivity < 1) {
|
||||
return Settings.SWIPE_VOLUME_SENSITIVITY.resetToDefault()
|
||||
}
|
||||
|
||||
return sensitivity
|
||||
if (sensitivity < 1) {
|
||||
return@lazy Settings.SWIPE_VOLUME_SENSITIVITY.resetToDefault()
|
||||
}
|
||||
|
||||
sensitivity
|
||||
}
|
||||
//endregion
|
||||
|
||||
//region overlay adjustments
|
||||
/**
|
||||
* Indicates whether haptic feedback should be enabled for swipe control interactions.
|
||||
*/
|
||||
val shouldEnableHapticFeedback: Boolean
|
||||
get() = Settings.SWIPE_HAPTIC_FEEDBACK.get()
|
||||
val shouldEnableHapticFeedback = Settings.SWIPE_HAPTIC_FEEDBACK.get()
|
||||
|
||||
/**
|
||||
* The duration in milliseconds that the overlay should remain visible after a change.
|
||||
*/
|
||||
val overlayShowTimeoutMillis: Long
|
||||
get() = Settings.SWIPE_OVERLAY_TIMEOUT.get()
|
||||
val overlayShowTimeoutMillis = Settings.SWIPE_OVERLAY_TIMEOUT.get()
|
||||
|
||||
/**
|
||||
* The background opacity of the overlay, converted from a percentage (0-100) to an alpha value (0-255).
|
||||
* Resets to default and shows a toast if the value is out of range.
|
||||
*/
|
||||
val overlayBackgroundOpacity: Int
|
||||
get() {
|
||||
var opacity = Settings.SWIPE_OVERLAY_OPACITY.get()
|
||||
val overlayBackgroundOpacity: Int by lazy {
|
||||
var opacity = Settings.SWIPE_OVERLAY_OPACITY.get()
|
||||
|
||||
if (opacity < 0 || opacity > 100) {
|
||||
Utils.showToastLong(str("revanced_swipe_overlay_background_opacity_invalid_toast"))
|
||||
opacity = Settings.SWIPE_OVERLAY_OPACITY.resetToDefault()
|
||||
}
|
||||
|
||||
opacity = opacity * 255 / 100
|
||||
return Color.argb(opacity, 0, 0, 0)
|
||||
if (opacity < 0 || opacity > 100) {
|
||||
Utils.showToastLong(str("revanced_swipe_overlay_background_opacity_invalid_toast"))
|
||||
opacity = Settings.SWIPE_OVERLAY_OPACITY.resetToDefault()
|
||||
}
|
||||
|
||||
opacity = opacity * 255 / 100
|
||||
Color.argb(opacity, 0, 0, 0)
|
||||
}
|
||||
|
||||
/**
|
||||
* The color of the progress bar in the overlay.
|
||||
* The color of the progress bar in the overlay for brightness.
|
||||
* Resets to default and shows a toast if the color string is invalid or empty.
|
||||
*/
|
||||
val overlayProgressColor: Int
|
||||
get() {
|
||||
try {
|
||||
@SuppressLint("UseKtx")
|
||||
val color = Color.parseColor(Settings.SWIPE_OVERLAY_PROGRESS_COLOR.get())
|
||||
return (0xBF000000.toInt() or (color and 0xFFFFFF))
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
Logger.printDebug({ "Could not parse color" }, ex)
|
||||
Utils.showToastLong(str("revanced_swipe_overlay_progress_color_invalid_toast"))
|
||||
Settings.SWIPE_OVERLAY_PROGRESS_COLOR.resetToDefault()
|
||||
return overlayProgressColor // Recursively return.
|
||||
}
|
||||
val overlayBrightnessProgressColor: Int by lazy {
|
||||
// Use lazy to avoid repeat parsing. Changing color requires app restart.
|
||||
getSettingColor(Settings.SWIPE_OVERLAY_BRIGHTNESS_COLOR)
|
||||
}
|
||||
|
||||
/**
|
||||
* The color of the progress bar in the overlay for volume.
|
||||
* Resets to default and shows a toast if the color string is invalid or empty.
|
||||
*/
|
||||
val overlayVolumeProgressColor: Int by lazy {
|
||||
getSettingColor(Settings.SWIPE_OVERLAY_VOLUME_COLOR)
|
||||
}
|
||||
|
||||
private fun getSettingColor(setting: StringSetting): Int {
|
||||
try {
|
||||
//noinspection UseKtx
|
||||
val color = Color.parseColor(setting.get())
|
||||
return (0xBF000000.toInt() or (color and 0x00FFFFFF))
|
||||
} catch (ex: IllegalArgumentException) {
|
||||
// This code should never be reached.
|
||||
// Color picker rejects and will not save bad colors to a setting.
|
||||
// If a user imports bad data, the color picker preference resets the
|
||||
// bad color before this method can be called.
|
||||
Logger.printDebug({ "Could not parse color: $setting" }, ex)
|
||||
Utils.showToastLong(str("revanced_settings_color_invalid"))
|
||||
setting.resetToDefault()
|
||||
return getSettingColor(setting) // Recursively return.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The background color used for the filled portion of the progress bar in the overlay.
|
||||
*/
|
||||
val overlayFillBackgroundPaint: Int
|
||||
get() = 0x80D3D3D3.toInt()
|
||||
val overlayFillBackgroundPaint = 0x80D3D3D3.toInt()
|
||||
|
||||
/**
|
||||
* The color used for text and icons in the overlay.
|
||||
*/
|
||||
val overlayTextColor: Int
|
||||
get() = Color.WHITE
|
||||
val overlayTextColor = Color.WHITE
|
||||
|
||||
/**
|
||||
* The text size in the overlay, in density-independent pixels (dp).
|
||||
* Must be between 1 and 30 dp; resets to default and shows a toast if invalid.
|
||||
*/
|
||||
val overlayTextSize: Int
|
||||
get() {
|
||||
val size = Settings.SWIPE_OVERLAY_TEXT_SIZE.get()
|
||||
if (size < 1 || size > 30) {
|
||||
Utils.showToastLong(str("revanced_swipe_text_overlay_size_invalid_toast"))
|
||||
return Settings.SWIPE_OVERLAY_TEXT_SIZE.resetToDefault()
|
||||
}
|
||||
return size
|
||||
val overlayTextSize: Int by lazy {
|
||||
val size = Settings.SWIPE_OVERLAY_TEXT_SIZE.get()
|
||||
if (size < 1 || size > 30) {
|
||||
Utils.showToastLong(str("revanced_swipe_text_overlay_size_invalid_toast"))
|
||||
return@lazy Settings.SWIPE_OVERLAY_TEXT_SIZE.resetToDefault()
|
||||
}
|
||||
size
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the style of the swipe controls overlay, determining its layout and appearance.
|
||||
@@ -199,28 +206,25 @@ class SwipeControlsConfigurationProvider {
|
||||
/**
|
||||
* A minimal vertical progress bar.
|
||||
*/
|
||||
VERTICAL_MINIMAL(isMinimal = true, isVertical = true)
|
||||
VERTICAL_MINIMAL(isMinimal = true, isVertical = true)
|
||||
}
|
||||
|
||||
/**
|
||||
* The current style of the overlay, determining its layout and appearance.
|
||||
*/
|
||||
val overlayStyle: SwipeOverlayStyle
|
||||
get() = Settings.SWIPE_OVERLAY_STYLE.get()
|
||||
val overlayStyle = Settings.SWIPE_OVERLAY_STYLE.get()
|
||||
//endregion
|
||||
|
||||
//region behaviour
|
||||
/**
|
||||
* Indicates whether the brightness level should be saved and restored when entering or exiting fullscreen mode.
|
||||
*/
|
||||
val shouldSaveAndRestoreBrightness: Boolean
|
||||
get() = Settings.SWIPE_SAVE_AND_RESTORE_BRIGHTNESS.get()
|
||||
val shouldSaveAndRestoreBrightness = Settings.SWIPE_SAVE_AND_RESTORE_BRIGHTNESS.get()
|
||||
|
||||
/**
|
||||
* Indicates whether auto-brightness should be enabled when the brightness gesture reaches its lowest value.
|
||||
*/
|
||||
val shouldLowestValueEnableAutoBrightness: Boolean
|
||||
get() = Settings.SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS.get()
|
||||
val shouldLowestValueEnableAutoBrightness = Settings.SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS.get()
|
||||
|
||||
/**
|
||||
* The saved brightness value for the swipe gesture, used to restore brightness in fullscreen mode.
|
||||
@@ -229,4 +233,4 @@ class SwipeControlsConfigurationProvider {
|
||||
get() = Settings.SWIPE_BRIGHTNESS_VALUE.get()
|
||||
set(value) = Settings.SWIPE_BRIGHTNESS_VALUE.save(value)
|
||||
//endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class SwipeControlsOverlayLayout(
|
||||
|
||||
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider())
|
||||
|
||||
// Drawable icons for brightness and volume
|
||||
// Drawable icons for brightness and volume.
|
||||
private val autoBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_auto")
|
||||
private val lowBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_low")
|
||||
private val mediumBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_medium")
|
||||
@@ -50,7 +50,7 @@ class SwipeControlsOverlayLayout(
|
||||
private val normalVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_normal")
|
||||
private val fullVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_high")
|
||||
|
||||
// Function to retrieve drawable resources by name
|
||||
// Function to retrieve drawable resources by name.
|
||||
private fun getDrawable(name: String): Drawable {
|
||||
val drawable = resources.getDrawable(
|
||||
Utils.getResourceIdentifier(context, name, "drawable"),
|
||||
@@ -60,19 +60,19 @@ class SwipeControlsOverlayLayout(
|
||||
return drawable
|
||||
}
|
||||
|
||||
// Initialize progress bars
|
||||
// Initialize progress bars.
|
||||
private val circularProgressView: CircularProgressView
|
||||
private val horizontalProgressView: HorizontalProgressView
|
||||
private val verticalBrightnessProgressView: VerticalProgressView
|
||||
private val verticalVolumeProgressView: VerticalProgressView
|
||||
|
||||
init {
|
||||
// Initialize circular progress bar
|
||||
// Initialize circular progress bar.
|
||||
circularProgressView = CircularProgressView(
|
||||
context,
|
||||
config.overlayBackgroundOpacity,
|
||||
config.overlayStyle.isMinimal,
|
||||
config.overlayProgressColor,
|
||||
config.overlayBrightnessProgressColor, // Placeholder, updated in showFeedbackView.
|
||||
config.overlayFillBackgroundPaint,
|
||||
config.overlayTextColor,
|
||||
config.overlayTextSize
|
||||
@@ -80,18 +80,18 @@ class SwipeControlsOverlayLayout(
|
||||
layoutParams = LayoutParams(100f.toDisplayPixels().toInt(), 100f.toDisplayPixels().toInt()).apply {
|
||||
addRule(CENTER_IN_PARENT, TRUE)
|
||||
}
|
||||
visibility = GONE // Initially hidden
|
||||
visibility = GONE // Initially hidden.
|
||||
}
|
||||
addView(circularProgressView)
|
||||
|
||||
// Initialize horizontal progress bar
|
||||
// Initialize horizontal progress bar.
|
||||
val screenWidth = resources.displayMetrics.widthPixels
|
||||
val layoutWidth = (screenWidth * 4 / 5).toInt() // Cap at ~360dp
|
||||
val layoutWidth = (screenWidth * 4 / 5).toInt() // Cap at ~360dp.
|
||||
horizontalProgressView = HorizontalProgressView(
|
||||
context,
|
||||
config.overlayBackgroundOpacity,
|
||||
config.overlayStyle.isMinimal,
|
||||
config.overlayProgressColor,
|
||||
config.overlayBrightnessProgressColor, // Placeholder, updated in showFeedbackView.
|
||||
config.overlayFillBackgroundPaint,
|
||||
config.overlayTextColor,
|
||||
config.overlayTextSize
|
||||
@@ -104,16 +104,16 @@ class SwipeControlsOverlayLayout(
|
||||
topMargin = 20f.toDisplayPixels().toInt()
|
||||
}
|
||||
}
|
||||
visibility = GONE // Initially hidden
|
||||
visibility = GONE // Initially hidden.
|
||||
}
|
||||
addView(horizontalProgressView)
|
||||
|
||||
// Initialize vertical progress bar for brightness (right side)
|
||||
// Initialize vertical progress bar for brightness (right side).
|
||||
verticalBrightnessProgressView = VerticalProgressView(
|
||||
context,
|
||||
config.overlayBackgroundOpacity,
|
||||
config.overlayStyle.isMinimal,
|
||||
config.overlayProgressColor,
|
||||
config.overlayBrightnessProgressColor,
|
||||
config.overlayFillBackgroundPaint,
|
||||
config.overlayTextColor,
|
||||
config.overlayTextSize
|
||||
@@ -123,16 +123,16 @@ class SwipeControlsOverlayLayout(
|
||||
rightMargin = 40f.toDisplayPixels().toInt()
|
||||
addRule(CENTER_VERTICAL)
|
||||
}
|
||||
visibility = GONE // Initially hidden
|
||||
visibility = GONE // Initially hidden.
|
||||
}
|
||||
addView(verticalBrightnessProgressView)
|
||||
|
||||
// Initialize vertical progress bar for volume (left side)
|
||||
// Initialize vertical progress bar for volume (left side).
|
||||
verticalVolumeProgressView = VerticalProgressView(
|
||||
context,
|
||||
config.overlayBackgroundOpacity,
|
||||
config.overlayStyle.isMinimal,
|
||||
config.overlayProgressColor,
|
||||
config.overlayVolumeProgressColor,
|
||||
config.overlayFillBackgroundPaint,
|
||||
config.overlayTextColor,
|
||||
config.overlayTextSize
|
||||
@@ -142,12 +142,12 @@ class SwipeControlsOverlayLayout(
|
||||
leftMargin = 40f.toDisplayPixels().toInt()
|
||||
addRule(CENTER_VERTICAL)
|
||||
}
|
||||
visibility = GONE // Initially hidden
|
||||
visibility = GONE // Initially hidden.
|
||||
}
|
||||
addView(verticalVolumeProgressView)
|
||||
}
|
||||
|
||||
// Handler and callback for hiding progress bars
|
||||
// Handler and callback for hiding progress bars.
|
||||
private val feedbackHideHandler = Handler(Looper.getMainLooper())
|
||||
private val feedbackHideCallback = Runnable {
|
||||
circularProgressView.visibility = GONE
|
||||
@@ -165,29 +165,42 @@ class SwipeControlsOverlayLayout(
|
||||
|
||||
val viewToShow = when {
|
||||
config.overlayStyle.isCircular -> circularProgressView
|
||||
config.overlayStyle.isVertical -> if (isBrightness) verticalBrightnessProgressView else verticalVolumeProgressView
|
||||
config.overlayStyle.isVertical ->
|
||||
if (isBrightness)
|
||||
verticalBrightnessProgressView
|
||||
else
|
||||
verticalVolumeProgressView
|
||||
else -> horizontalProgressView
|
||||
}
|
||||
viewToShow.apply {
|
||||
// Set the appropriate progress color.
|
||||
if (this is CircularProgressView || this is HorizontalProgressView) {
|
||||
setProgressColor(
|
||||
if (isBrightness)
|
||||
config.overlayBrightnessProgressColor
|
||||
else
|
||||
config.overlayVolumeProgressColor
|
||||
)
|
||||
}
|
||||
setProgress(progress, max, value, isBrightness)
|
||||
this.icon = icon
|
||||
visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
// Handle volume change
|
||||
// Handle volume change.
|
||||
override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
|
||||
val volumePercentage = (newVolume.toFloat() / maximumVolume) * 100
|
||||
val icon = when {
|
||||
newVolume == 0 -> mutedVolumeIcon
|
||||
volumePercentage < 33 -> lowVolumeIcon
|
||||
volumePercentage < 66 -> normalVolumeIcon
|
||||
volumePercentage < 25 -> lowVolumeIcon
|
||||
volumePercentage < 50 -> normalVolumeIcon
|
||||
else -> fullVolumeIcon
|
||||
}
|
||||
showFeedbackView("$newVolume", newVolume, maximumVolume, icon, isBrightness = false)
|
||||
}
|
||||
|
||||
// Handle brightness change
|
||||
// Handle brightness change.
|
||||
override fun onBrightnessChanged(brightness: Double) {
|
||||
if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) {
|
||||
val displayText = if (config.overlayStyle.isVertical) "А"
|
||||
@@ -195,18 +208,19 @@ class SwipeControlsOverlayLayout(
|
||||
showFeedbackView(displayText, 0, 100, autoBrightnessIcon, isBrightness = true)
|
||||
} else {
|
||||
val brightnessValue = round(brightness).toInt()
|
||||
val clampedProgress = max(0, brightnessValue)
|
||||
val icon = when {
|
||||
brightnessValue < 25 -> lowBrightnessIcon
|
||||
brightnessValue < 50 -> mediumBrightnessIcon
|
||||
brightnessValue < 75 -> highBrightnessIcon
|
||||
clampedProgress < 25 -> lowBrightnessIcon
|
||||
clampedProgress < 50 -> mediumBrightnessIcon
|
||||
clampedProgress < 75 -> highBrightnessIcon
|
||||
else -> fullBrightnessIcon
|
||||
}
|
||||
val displayText = if (config.overlayStyle.isVertical) "$brightnessValue" else "$brightnessValue%"
|
||||
showFeedbackView(displayText, brightnessValue, 100, icon, isBrightness = true)
|
||||
val displayText = if (config.overlayStyle.isVertical) "$clampedProgress" else "$clampedProgress%"
|
||||
showFeedbackView(displayText, clampedProgress, 100, icon, isBrightness = true)
|
||||
}
|
||||
}
|
||||
|
||||
// Begin swipe session
|
||||
// Begin swipe session.
|
||||
override fun onEnterSwipeSession() {
|
||||
if (config.shouldEnableHapticFeedback) {
|
||||
@Suppress("DEPRECATION")
|
||||
@@ -233,25 +247,41 @@ abstract class AbstractProgressView(
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
// Combined paint creation function for both fill and stroke styles
|
||||
private fun createPaint(color: Int, style: Paint.Style = Paint.Style.FILL, strokeCap: Paint.Cap = Paint.Cap.BUTT, strokeWidth: Float = 0f) = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
// Combined paint creation function for both fill and stroke styles.
|
||||
private fun createPaint(
|
||||
color: Int,
|
||||
style: Paint.Style = Paint.Style.FILL,
|
||||
strokeCap: Paint.Cap = Paint.Cap.BUTT,
|
||||
strokeWidth: Float = 0f
|
||||
) = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
this.style = style
|
||||
this.color = color
|
||||
this.strokeCap = strokeCap
|
||||
this.strokeWidth = strokeWidth
|
||||
}
|
||||
|
||||
// Initialize paints
|
||||
val backgroundPaint = createPaint(overlayBackgroundOpacity, style = Paint.Style.FILL)
|
||||
val progressPaint = createPaint(overlayProgressColor, style = Paint.Style.STROKE, strokeCap = Paint.Cap.ROUND, strokeWidth = 6f.toDisplayPixels())
|
||||
val fillBackgroundPaint = createPaint(overlayFillBackgroundPaint, style = Paint.Style.FILL)
|
||||
val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = overlayTextColor
|
||||
// Initialize paints.
|
||||
val backgroundPaint = createPaint(
|
||||
overlayBackgroundOpacity,
|
||||
style = Paint.Style.FILL
|
||||
)
|
||||
val progressPaint = createPaint(
|
||||
overlayProgressColor,
|
||||
style = Paint.Style.STROKE,
|
||||
strokeCap = Paint.Cap.ROUND,
|
||||
strokeWidth = 6f.toDisplayPixels()
|
||||
)
|
||||
val fillBackgroundPaint = createPaint(
|
||||
overlayFillBackgroundPaint,
|
||||
style = Paint.Style.FILL
|
||||
)
|
||||
val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = overlayTextColor
|
||||
textAlign = Paint.Align.CENTER
|
||||
textSize = overlayTextSize.toFloat().toDisplayPixels()
|
||||
textSize = overlayTextSize.toFloat().toDisplayPixels()
|
||||
}
|
||||
|
||||
// Rect for text measurement
|
||||
// Rect for text measurement.
|
||||
protected val textBounds = Rect()
|
||||
|
||||
protected var progress = 0
|
||||
@@ -268,13 +298,18 @@ abstract class AbstractProgressView(
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun setProgressColor(color: Int) {
|
||||
progressPaint.color = color
|
||||
invalidate()
|
||||
}
|
||||
|
||||
protected fun measureTextWidth(text: String, paint: Paint): Int {
|
||||
paint.getTextBounds(text, 0, text.length, textBounds)
|
||||
return textBounds.width()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
// Base class implementation can be empty
|
||||
// Base class implementation can be empty.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,8 +428,8 @@ class HorizontalProgressView(
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate required width based on content
|
||||
* @return Required width to display all elements
|
||||
* Calculate required width based on content.
|
||||
* @return Required width to display all elements.
|
||||
*/
|
||||
private fun calculateRequiredWidth(): Float {
|
||||
textWidth = measureTextWidth(displayText, textPaint).toFloat()
|
||||
@@ -537,8 +572,8 @@ class VerticalProgressView(
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate required height based on content
|
||||
* @return Required height to display all elements
|
||||
* Calculate required height based on content.
|
||||
* @return Required height to display all elements.
|
||||
*/
|
||||
private fun calculateRequiredHeight(): Float {
|
||||
return if (!isMinimalStyle) {
|
||||
@@ -633,4 +668,4 @@ class VerticalProgressView(
|
||||
super.setProgress(value, max, text, isBrightnessMode)
|
||||
requestLayout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@ 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.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
|
||||
@@ -23,8 +27,27 @@ public class PlaybackSpeedDialogButton {
|
||||
"revanced_playback_speed_dialog_button",
|
||||
"revanced_playback_speed_dialog_button_placeholder",
|
||||
Settings.PLAYBACK_SPEED_DIALOG_BUTTON::get,
|
||||
view -> CustomPlaybackSpeedPatch.showOldPlaybackSpeedMenu(),
|
||||
null
|
||||
view -> {
|
||||
try {
|
||||
CustomPlaybackSpeedPatch.showModernCustomPlaybackSpeedDialog(view.getContext());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "speed button onClick failure", ex);
|
||||
}
|
||||
},
|
||||
view -> {
|
||||
try {
|
||||
final float speed = (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get() ||
|
||||
VideoInformation.getPlaybackSpeed() == Settings.PLAYBACK_SPEED_DEFAULT.get())
|
||||
? 1.0f
|
||||
: Settings.PLAYBACK_SPEED_DEFAULT.get();
|
||||
|
||||
VideoInformation.overridePlaybackSpeed(speed);
|
||||
showToastShort(str("revanced_custom_playback_speeds_reset_toast", (speed + "x")));
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "speed button reset failure", ex);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "initializeButton failure", ex);
|
||||
|
||||
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
|
||||
org.gradle.parallel = true
|
||||
android.useAndroidX = true
|
||||
kotlin.code.style = official
|
||||
version = 5.25.0-dev.9
|
||||
version = 5.27.0-dev.9
|
||||
|
||||
58
package-lock.json
generated
58
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.1"
|
||||
"semantic-release": "^24.2.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -1964,9 +1964,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "5.3.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
||||
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -3460,9 +3460,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/marked": {
|
||||
"version": "12.0.2",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
|
||||
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
|
||||
"version": "15.0.12",
|
||||
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -3473,24 +3473,38 @@
|
||||
}
|
||||
},
|
||||
"node_modules/marked-terminal": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.1.0.tgz",
|
||||
"integrity": "sha512-+pvwa14KZL74MVXjYdPR3nSInhGhNvPce/3mqLVZT2oUvt654sL1XImFuLZ1pkA866IYZ3ikDTOFUIC7XzpZZg==",
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz",
|
||||
"integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-escapes": "^7.0.0",
|
||||
"chalk": "^5.3.0",
|
||||
"ansi-regex": "^6.1.0",
|
||||
"chalk": "^5.4.1",
|
||||
"cli-highlight": "^2.1.11",
|
||||
"cli-table3": "^0.6.5",
|
||||
"node-emoji": "^2.1.3",
|
||||
"supports-hyperlinks": "^3.0.0"
|
||||
"node-emoji": "^2.2.0",
|
||||
"supports-hyperlinks": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"marked": ">=1 <14"
|
||||
"marked": ">=1 <16"
|
||||
}
|
||||
},
|
||||
"node_modules/marked-terminal/node_modules/ansi-regex": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/meow": {
|
||||
@@ -3607,9 +3621,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-emoji": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz",
|
||||
"integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==",
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz",
|
||||
"integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6760,9 +6774,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semantic-release": {
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.1.tgz",
|
||||
"integrity": "sha512-z0/3cutKNkLQ4Oy0HTi3lubnjTsdjjgOqmxdPjeYWe6lhFqUPfwslZxRHv3HDZlN4MhnZitb9SLihDkZNxOXfQ==",
|
||||
"version": "24.2.5",
|
||||
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.5.tgz",
|
||||
"integrity": "sha512-9xV49HNY8C0/WmPWxTlaNleiXhWb//qfMzG2c5X8/k7tuWcu8RssbuS+sujb/h7PiWSXv53mrQvV9hrO9b7vuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6784,8 +6798,8 @@
|
||||
"hosted-git-info": "^8.0.0",
|
||||
"import-from-esm": "^2.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^12.0.0",
|
||||
"marked-terminal": "^7.0.0",
|
||||
"marked": "^15.0.0",
|
||||
"marked-terminal": "^7.3.0",
|
||||
"micromatch": "^4.0.2",
|
||||
"p-each-series": "^3.0.0",
|
||||
"p-reduce": "^3.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.1"
|
||||
"semantic-release": "^24.2.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
public final class FixFacebookLoginPatchKt {
|
||||
public static final fun getFixFacebookLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/all/misc/activity/exportall/ExportAllActivitiesPatchKt {
|
||||
public static final fun getExportAllActivitiesPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
@@ -10,6 +6,10 @@ public final class app/revanced/patches/all/misc/adb/HideAdbPatchKt {
|
||||
public static final fun getHideAdbStatusPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/all/misc/appicon/HideAppIconPatchKt {
|
||||
public static final fun getHideAppIconPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/all/misc/build/BaseSpoofBuildInfoPatchKt {
|
||||
public static final fun baseSpoofBuildInfoPatch (Lkotlin/jvm/functions/Function0;)Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -196,6 +196,10 @@ public final class app/revanced/patches/googlenews/misc/gms/GmsCoreSupportPatchK
|
||||
public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/googlephotos/misc/backup/EnableDCIMFoldersBackupControlPatchKt {
|
||||
public static final fun getEnableDCIMFoldersBackupControlPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/googlephotos/misc/extension/ExtensionPatchKt {
|
||||
public static final fun getExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -288,6 +292,10 @@ public final class app/revanced/patches/messenger/inputfield/DisableTypingIndica
|
||||
public static final fun getDisableTypingIndicatorPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/messenger/layout/HideFacebookButtonPatchKt {
|
||||
public static final fun getHideFacebookButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/messenger/metaai/RemoveMetaAIPatchKt {
|
||||
public static final fun getRemoveMetaAIPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -300,6 +308,10 @@ public final class app/revanced/patches/messenger/navbar/RemoveMetaAITabPatchKt
|
||||
public static final fun getRemoveMetaAITabPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/meta/ads/HideAdsPatchKt {
|
||||
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/mifitness/misc/locale/ForceEnglishLocalePatchKt {
|
||||
public static final fun getForceEnglishLocalePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -444,6 +456,14 @@ public final class app/revanced/patches/primevideo/misc/extension/ExtensionPatch
|
||||
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/primevideo/misc/permissions/RenamePermissionsPatchKt {
|
||||
public static final fun getRenamePermissionsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/protonmail/account/RemoveFreeAccountsLimitPatchKt {
|
||||
public static final fun getRemoveFreeAccountsLimitPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/protonmail/signature/RemoveSentFromSignaturePatchKt {
|
||||
public static final fun getRemoveSentFromSignaturePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
@@ -567,6 +587,10 @@ public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/
|
||||
public static final fun getFixSLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/thumbnail/FixPostThumbnailsPatchKt {
|
||||
public static final fun getFixPostThumbnailsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/user/UseUserEndpointPatchKt {
|
||||
public static final fun getUseUserEndpointPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -585,6 +609,7 @@ public final class app/revanced/patches/reddit/layout/disablescreenshotpopup/Dis
|
||||
|
||||
public final class app/revanced/patches/reddit/layout/premiumicon/UnlockPremiumIconPatchKt {
|
||||
public static final fun getUnlockPremiumIconPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
public static final fun getUnlockPremiumIconsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/reddit/misc/extension/ExtensionPatchKt {
|
||||
@@ -864,6 +889,10 @@ public final class app/revanced/patches/soundcloud/offlinesync/EnableOfflineSync
|
||||
public static final fun getEnableOfflineSync ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatchKt {
|
||||
public static final fun getHideCreateButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/spotify/layout/theme/CustomThemePatchKt {
|
||||
public static final fun getCustomThemePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
}
|
||||
@@ -888,6 +917,10 @@ public final class app/revanced/patches/spotify/misc/fix/SpoofSignaturePatchKt {
|
||||
public static final fun getSpoofSignaturePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginPatchKt {
|
||||
public static final fun getFixFacebookLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt {
|
||||
public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -1399,6 +1432,10 @@ public final class app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatchKt {
|
||||
public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/misc/hapticfeedback/DisableHapticFeedbackPatchKt {
|
||||
public static final fun getDisableHapticFeedbackPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHookKt {
|
||||
public static final fun addImageUrlErrorCallbackHook (Ljava/lang/String;)V
|
||||
public static final fun addImageUrlHook (Ljava/lang/String;Z)V
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package app.revanced.patches.all.misc.appicon
|
||||
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.util.asSequence
|
||||
import app.revanced.util.childElementsSequence
|
||||
import java.util.logging.Logger
|
||||
import org.w3c.dom.Element
|
||||
|
||||
@Suppress("unused")
|
||||
val hideAppIconPatch = resourcePatch(
|
||||
name = "Hide app icon",
|
||||
description = "Hides the app icon from the Android launcher.",
|
||||
use = false,
|
||||
) {
|
||||
execute {
|
||||
document("AndroidManifest.xml").use { document ->
|
||||
var changed = false
|
||||
|
||||
val intentFilters = document.getElementsByTagName("intent-filter")
|
||||
for (node in intentFilters.asSequence().filterIsInstance<Element>()) {
|
||||
var hasMainAction = false
|
||||
var launcherCategory: Element? = null
|
||||
|
||||
for (child in node.childElementsSequence()) {
|
||||
when (child.tagName) {
|
||||
"action" -> if (child.getAttribute("android:name") == "android.intent.action.MAIN") {
|
||||
hasMainAction = true
|
||||
}
|
||||
"category" -> if (child.getAttribute("android:name") == "android.intent.category.LAUNCHER") {
|
||||
launcherCategory = child
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMainAction && launcherCategory != null) {
|
||||
launcherCategory.setAttribute("android:name", "android.intent.category.DEFAULT")
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
Logger.getLogger(this::class.java.name)
|
||||
.warning("No changes made: Did not find any launcher intent-filters to change.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import app.revanced.patcher.patch.stringOption
|
||||
@Suppress("unused")
|
||||
val spoofBuildInfoPatch = bytecodePatch(
|
||||
name = "Spoof build info",
|
||||
description = "Spoof the information about the current build.",
|
||||
description = "Spoofs the information about the current build.",
|
||||
use = false,
|
||||
) {
|
||||
val board by stringOption(
|
||||
@@ -141,14 +141,14 @@ val spoofBuildInfoPatch = bytecodePatch(
|
||||
val socManufacturer by stringOption(
|
||||
key = "soc-manufacturer",
|
||||
default = null,
|
||||
title = "SOC Manufacturer",
|
||||
title = "SOC manufacturer",
|
||||
description = "The manufacturer of the device's primary system-on-chip.",
|
||||
)
|
||||
|
||||
val socModel by stringOption(
|
||||
key = "soc-model",
|
||||
default = null,
|
||||
title = "SOC Model",
|
||||
title = "SOC model",
|
||||
description = "The model name of the device's primary system-on-chip.",
|
||||
)
|
||||
|
||||
|
||||
@@ -36,12 +36,12 @@ val spoofSimCountryPatch = bytecodePatch(
|
||||
|
||||
val networkCountryIso by isoCountryPatchOption(
|
||||
"networkCountryIso",
|
||||
"Network ISO Country Code",
|
||||
"Network ISO country code",
|
||||
)
|
||||
|
||||
val simCountryIso by isoCountryPatchOption(
|
||||
"simCountryIso",
|
||||
"Sim ISO Country Code",
|
||||
"SIM ISO country code",
|
||||
)
|
||||
|
||||
dependsOn(
|
||||
|
||||
@@ -3,5 +3,5 @@ package app.revanced.patches.bandcamp.limitations
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
internal val handlePlaybackLimitsFingerprint = fingerprint {
|
||||
strings("play limits processing track", "found play_count")
|
||||
strings("track_id", "play_count")
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.revanced.patches.bandcamp.limitations
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Suppress("unused")
|
||||
val removePlayLimitsPatch = bytecodePatch(
|
||||
@@ -11,6 +11,6 @@ val removePlayLimitsPatch = bytecodePatch(
|
||||
compatibleWith("com.bandcamp.android")
|
||||
|
||||
execute {
|
||||
handlePlaybackLimitsFingerprint.method.addInstructions(0, "return-void")
|
||||
handlePlaybackLimitsFingerprint.method.returnEarly()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package app.revanced.patches.googlephotos.misc.backup
|
||||
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Suppress("unused")
|
||||
val enableDCIMFoldersBackupControlPatch = bytecodePatch(
|
||||
name = "Enable DCIM folders backup control",
|
||||
description = "Disables always on backup for the Camera and other DCIM folders, allowing you to control backup " +
|
||||
"for each folder individually. This will make the app default to having no folders backed up.",
|
||||
use = false,
|
||||
) {
|
||||
compatibleWith("com.google.android.apps.photos")
|
||||
|
||||
execute {
|
||||
isDCIMFolderBackupControlDisabled.method.returnEarly(false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package app.revanced.patches.googlephotos.misc.backup
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
internal val isDCIMFolderBackupControlDisabled = fingerprint {
|
||||
returns("Z")
|
||||
strings("/dcim", "/mars_files/")
|
||||
}
|
||||
@@ -1,23 +1,9 @@
|
||||
package app.revanced.patches.instagram.ads
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
|
||||
@Deprecated("Patch was moved to different package: app.revanced.patches.meta.ads.hideAdsPatch")
|
||||
@Suppress("unused")
|
||||
val hideAdsPatch = bytecodePatch(
|
||||
name = "Hide ads",
|
||||
description = "Hides ads in stories, discover, profile, etc. " +
|
||||
"An ad can still appear once when refreshing the home feed.",
|
||||
) {
|
||||
compatibleWith("com.instagram.android")
|
||||
|
||||
execute {
|
||||
adInjectorFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
const/4 v0, 0x0
|
||||
return v0
|
||||
""",
|
||||
)
|
||||
}
|
||||
val hideAdsPatch = bytecodePatch {
|
||||
dependsOn(app.revanced.patches.meta.ads.hideAdsPatch)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import app.revanced.patcher.patch.bytecodePatch
|
||||
|
||||
@Suppress("unused")
|
||||
val unlockPremiumPatch = bytecodePatch(
|
||||
name = "Unlock premium",
|
||||
name = "Unlock Premium",
|
||||
) {
|
||||
compatibleWith("com.adobe.lrmobile"("10.0.2"))
|
||||
|
||||
|
||||
@@ -5,9 +5,14 @@ import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
|
||||
/**
|
||||
* This patch will be deleted soon.
|
||||
*
|
||||
* Pull requests to update this patch to the latest app target are invited.
|
||||
*/
|
||||
@Deprecated("This patch only works with an outdated app target that is no longer fully supported by Facebook.")
|
||||
@Suppress("unused")
|
||||
val disableSwitchingEmojiToStickerPatch = bytecodePatch(
|
||||
name = "Disable switching emoji to sticker",
|
||||
description = "Disables switching from emoji to sticker search mode in message input field.",
|
||||
) {
|
||||
compatibleWith("com.facebook.orca"("439.0.0.29.119"))
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package app.revanced.patches.messenger.layout
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
internal val isFacebookButtonEnabledFingerprint = fingerprint {
|
||||
parameters()
|
||||
returns("Z")
|
||||
strings("com.facebook.messaging.inbox.tab.plugins.core.tabtoolbarbutton." +
|
||||
"facebookbutton.facebooktoolbarbutton.FacebookButtonTabButtonImplementation")
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package app.revanced.patches.messenger.layout
|
||||
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Suppress("unused")
|
||||
val hideFacebookButtonPatch = bytecodePatch(
|
||||
name = "Hide Facebook button",
|
||||
description = "Hides the Facebook button in the top toolbar."
|
||||
) {
|
||||
compatibleWith("com.facebook.orca")
|
||||
|
||||
execute {
|
||||
isFacebookButtonEnabledFingerprint.method.returnEarly(false)
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,8 @@ import app.revanced.patcher.fingerprint
|
||||
internal val getMobileConfigBoolFingerprint = fingerprint {
|
||||
parameters("J")
|
||||
returns("Z")
|
||||
opcodes(Opcode.RETURN)
|
||||
custom { method, classDef ->
|
||||
method.implementation ?: return@custom false // unsure if this is necessary
|
||||
opcodes(Opcode.RETURN)
|
||||
custom { _, classDef ->
|
||||
classDef.interfaces.contains("Lcom/facebook/mobileconfig/factory/MobileConfigUnsafeContext;")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ val removeMetaAIPatch = bytecodePatch(
|
||||
addInstructions(
|
||||
returnIndex,
|
||||
"""
|
||||
invoke-static { p1, p2, v$returnRegister }, $EXTENSION_CLASS_DESCRIPTOR->overrideConfigBool(JZ)Z
|
||||
invoke-static { p1, p2, v$returnRegister }, $EXTENSION_CLASS_DESCRIPTOR->overrideBooleanFlag(JZ)Z
|
||||
move-result v$returnRegister
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ package app.revanced.patches.messenger.misc.extension
|
||||
|
||||
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||
|
||||
val sharedExtensionPatch = sharedExtensionPatch("messenger", mainActivityOnCreateHook)
|
||||
val sharedExtensionPatch = sharedExtensionPatch("messenger", mainActivityOnCreateHook)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package app.revanced.patches.instagram.ads
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val adInjectorFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PRIVATE)
|
||||
returns("Z")
|
||||
parameters("L", "L")
|
||||
strings(
|
||||
"SponsoredContentController.insertItem",
|
||||
)
|
||||
}
|
||||
package app.revanced.patches.meta.ads
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val adInjectorFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PRIVATE)
|
||||
returns("Z")
|
||||
parameters("L", "L")
|
||||
strings(
|
||||
"SponsoredContentController.insertItem",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package app.revanced.patches.meta.ads
|
||||
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Suppress("unused")
|
||||
val hideAdsPatch = bytecodePatch(
|
||||
name = "Hide ads",
|
||||
) {
|
||||
/**
|
||||
* Patch is identical for both Instagram and Threads app.
|
||||
*/
|
||||
compatibleWith(
|
||||
"com.instagram.android",
|
||||
"com.instagram.barcelona",
|
||||
)
|
||||
|
||||
execute {
|
||||
adInjectorFingerprint.method.returnEarly(false)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package app.revanced.patches.primevideo.misc.permissions
|
||||
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.util.asSequence
|
||||
import app.revanced.util.getNode
|
||||
import org.w3c.dom.Element
|
||||
|
||||
@Suppress("unused")
|
||||
val renamePermissionsPatch = resourcePatch(
|
||||
name = "Rename shared permissions",
|
||||
description = "Rename certain permissions shared across Amazon apps. " +
|
||||
"Applying this patch can fix installation errors, but can also break features in certain apps.",
|
||||
use = false
|
||||
) {
|
||||
compatibleWith("com.amazon.avod.thirdpartyclient")
|
||||
|
||||
val permissionNames = setOf(
|
||||
"com.amazon.identity.permission.CAN_CALL_MAP_INFORMATION_PROVIDER",
|
||||
"com.amazon.identity.auth.device.perm.AUTH_SDK",
|
||||
"com.amazon.dcp.sso.permission.account.changed",
|
||||
"com.amazon.dcp.sso.permission.AmazonAccountPropertyService.property.changed",
|
||||
"com.amazon.identity.permission.CALL_AMAZON_DEVICE_INFORMATION_PROVIDER",
|
||||
"com.amazon.appmanager.preload.permission.READ_PRELOAD_DEVICE_INFO_PROVIDER"
|
||||
)
|
||||
|
||||
execute {
|
||||
document("AndroidManifest.xml").use { document ->
|
||||
val manifest = document.getNode("manifest") as Element
|
||||
|
||||
val permissions = manifest
|
||||
.getElementsByTagName("permission")
|
||||
.asSequence()
|
||||
.map { Pair(it as Element, it.getAttribute("android:name")) }
|
||||
.filter { (_, name) -> name in permissionNames }
|
||||
|
||||
if (permissions.none()) throw PatchException("Could not find any permissions to rename")
|
||||
|
||||
permissions.forEach { (element, name) ->
|
||||
element.setAttribute("android:name", "revanced.$name")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package app.revanced.patches.protonmail.account
|
||||
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.util.findElementByAttributeValueOrThrow
|
||||
|
||||
@Suppress("unused")
|
||||
val removeFreeAccountsLimitPatch = resourcePatch(
|
||||
name = "Remove free accounts limit",
|
||||
description = "Removes the limit for maximum free accounts logged in.",
|
||||
) {
|
||||
compatibleWith("ch.protonmail.android")
|
||||
|
||||
execute {
|
||||
document("res/values/integers.xml").use { document ->
|
||||
document.documentElement.childNodes.findElementByAttributeValueOrThrow(
|
||||
"name",
|
||||
"core_feature_auth_user_check_max_free_user_count",
|
||||
).textContent = Int.MAX_VALUE.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,4 +39,4 @@ val removeSentFromSignaturePatch = resourcePatch(
|
||||
|
||||
if (!foundString) throw PatchException("Could not find 'sent from' string in resources")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package app.revanced.patches.reddit.customclients.sync.syncforreddit.fix.thumbnail
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val customImageViewLoadFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC)
|
||||
parameters("Ljava/lang/String;", "Z", "Z", "I", "I")
|
||||
custom { _, classDef ->
|
||||
classDef.endsWith("CustomImageView;")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package app.revanced.patches.reddit.customclients.sync.syncforreddit.fix.thumbnail
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
|
||||
@Suppress("unused")
|
||||
val fixPostThumbnailsPatch = bytecodePatch(
|
||||
name = "Fix post thumbnails",
|
||||
description = "Fixes loading post thumbnails by correcting their URLs.",
|
||||
) {
|
||||
|
||||
compatibleWith(
|
||||
"com.laurencedawson.reddit_sync",
|
||||
"com.laurencedawson.reddit_sync.pro",
|
||||
"com.laurencedawson.reddit_sync.dev"
|
||||
)
|
||||
|
||||
// Image URLs contain escaped ampersands (&), let's replace these with unescaped ones (&).
|
||||
execute {
|
||||
customImageViewLoadFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
# url = url.replace("&", "&");
|
||||
const-string v0, "&"
|
||||
const-string v1, "&"
|
||||
invoke-virtual { p1, v0, v1 }, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
|
||||
move-result-object p1
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,9 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
|
||||
@Suppress("unused")
|
||||
val unlockPremiumIconPatch = bytecodePatch(
|
||||
name = "Unlock premium Reddit icons",
|
||||
description = "Unlocks the premium Reddit icons.",
|
||||
val unlockPremiumIconsPatch = bytecodePatch(
|
||||
name = "Unlock Premium icons",
|
||||
description = "Unlocks the Reddit Premium icons.",
|
||||
) {
|
||||
compatibleWith("com.reddit.frontpage")
|
||||
|
||||
@@ -20,3 +20,9 @@ val unlockPremiumIconPatch = bytecodePatch(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Patch was renamed", ReplaceWith("unlockPremiumIconsPatch"))
|
||||
@Suppress("unused")
|
||||
val unlockPremiumIconPatch = bytecodePatch{
|
||||
dependsOn(unlockPremiumIconsPatch)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import java.util.logging.Logger
|
||||
@Suppress("unused")
|
||||
val disableLicenseCheckPatch = bytecodePatch(
|
||||
name = "Disable Pairip license check",
|
||||
description = "Disable Play Integrity Protect (Pairip) client-side license check.",
|
||||
description = "Disables Play Integrity API (Pairip) client-side license check.",
|
||||
use = false
|
||||
) {
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package app.revanced.patches.spotify.layout.hide.createbutton
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstruction
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
|
||||
internal val navigationBarItemSetClassFingerprint = fingerprint {
|
||||
strings("NavigationBarItemSet(")
|
||||
}
|
||||
|
||||
internal val navigationBarItemSetConstructorFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
|
||||
// Make sure the method checks whether navigation bar items are null before adding them.
|
||||
// If this is not true, then we cannot patch the method and potentially transform the parameters into null.
|
||||
opcodes(Opcode.IF_EQZ, Opcode.INVOKE_VIRTUAL)
|
||||
custom { method, _ ->
|
||||
method.indexOfFirstInstruction {
|
||||
getReference<MethodReference>()?.name == "add"
|
||||
} >= 0
|
||||
}
|
||||
}
|
||||
|
||||
internal val oldNavigationBarAddItemFingerprint = fingerprint {
|
||||
strings("Bottom navigation tabs exceeds maximum of 5 tabs")
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package app.revanced.patches.spotify.layout.hide.createbutton
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.smali.ExternalLabel
|
||||
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||
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
|
||||
import java.util.logging.Logger
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/spotify/layout/hide/createbutton/HideCreateButtonPatch;"
|
||||
|
||||
@Suppress("unused")
|
||||
val hideCreateButtonPatch = bytecodePatch(
|
||||
name = "Hide Create button",
|
||||
description = "Hides the \"Create\" button in the navigation bar."
|
||||
) {
|
||||
compatibleWith("com.spotify.music")
|
||||
|
||||
dependsOn(sharedExtensionPatch)
|
||||
|
||||
execute {
|
||||
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
Logger.getLogger(this::class.java.name).warning(
|
||||
"Create button does not exist in legacy app target. No changes applied."
|
||||
)
|
||||
return@execute
|
||||
}
|
||||
|
||||
val oldNavigationBarAddItemMethod = oldNavigationBarAddItemFingerprint.originalMethodOrNull
|
||||
// Only throw the fingerprint error when oldNavigationBarAddItemMethod does not exist.
|
||||
val navigationBarItemSetClassDef = if (oldNavigationBarAddItemMethod == null) {
|
||||
navigationBarItemSetClassFingerprint.originalClassDef
|
||||
} else {
|
||||
navigationBarItemSetClassFingerprint.originalClassDefOrNull
|
||||
}
|
||||
|
||||
if (navigationBarItemSetClassDef != null) {
|
||||
// Main patch for newest and most versions.
|
||||
// The NavigationBarItemSet constructor accepts multiple parameters which represent each navigation bar item.
|
||||
// Each item is manually checked whether it is not null and then added to a LinkedHashSet.
|
||||
// Since the order of the items can differ, we are required to check every parameter to see whether it is the
|
||||
// Create button. So, for every parameter passed to the method, invoke our extension method and overwrite it
|
||||
// to null in case it is the Create button.
|
||||
navigationBarItemSetConstructorFingerprint.match(navigationBarItemSetClassDef).method.apply {
|
||||
// Add 1 to the index because the first parameter register is `this`.
|
||||
val parameterTypesWithRegister = parameterTypes.mapIndexed { index, parameterType ->
|
||||
parameterType to (index + 1)
|
||||
}
|
||||
|
||||
val returnNullIfIsCreateButtonDescriptor =
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->returnNullIfIsCreateButton(Ljava/lang/Object;)Ljava/lang/Object;"
|
||||
|
||||
parameterTypesWithRegister.reversed().forEach { (parameterType, parameterRegister) ->
|
||||
addInstructions(
|
||||
0,
|
||||
"""
|
||||
invoke-static { p$parameterRegister }, $returnNullIfIsCreateButtonDescriptor
|
||||
move-result-object p$parameterRegister
|
||||
check-cast p$parameterRegister, $parameterType
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (oldNavigationBarAddItemMethod != null) {
|
||||
// In case an older version of the app is being patched, hook the old method which adds navigation bar items.
|
||||
// Return null early if the navigation bar item title resource id is the old Create button title resource id.
|
||||
oldNavigationBarAddItemFingerprint.methodOrNull?.apply {
|
||||
val getNavigationBarItemTitleStringIndex = indexOfFirstInstructionOrThrow {
|
||||
val reference = getReference<MethodReference>()
|
||||
reference?.definingClass == "Landroid/content/res/Resources;" && reference.name == "getString"
|
||||
}
|
||||
// This register is a parameter register, so it can be used at the start of the method when adding
|
||||
// the new instructions.
|
||||
val oldNavigationBarItemTitleResIdRegister =
|
||||
getInstruction<FiveRegisterInstruction>(getNavigationBarItemTitleStringIndex).registerD
|
||||
|
||||
// The instruction where the normal method logic starts.
|
||||
val firstInstruction = getInstruction(0)
|
||||
|
||||
val isOldCreateButtonDescriptor =
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->isOldCreateButton(I)Z"
|
||||
|
||||
addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
invoke-static { v$oldNavigationBarItemTitleResIdRegister }, $isOldCreateButtonDescriptor
|
||||
move-result v0
|
||||
|
||||
# If this navigation bar item is not the Create button, jump to the normal method logic.
|
||||
if-eqz v0, :normal-method-logic
|
||||
|
||||
# Return null early because this method return value is a BottomNavigationItemView.
|
||||
const/4 v0, 0
|
||||
return-object v0
|
||||
""",
|
||||
ExternalLabel("normal-method-logic", firstInstruction)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,65 +2,19 @@ package app.revanced.patches.spotify.layout.theme
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.patcher.patch.booleanOption
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.patcher.patch.stringOption
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||
import app.revanced.util.*
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
import org.w3c.dom.Element
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/layout/theme/CustomThemePatch;"
|
||||
|
||||
internal val spotifyBackgroundColor = stringOption(
|
||||
key = "backgroundColor",
|
||||
default = "@android:color/black",
|
||||
title = "Primary background color",
|
||||
description = "The background color. Can be a hex color or a resource reference.",
|
||||
required = true,
|
||||
)
|
||||
|
||||
internal val overridePlayerGradientColor = booleanOption(
|
||||
key = "overridePlayerGradientColor",
|
||||
default = false,
|
||||
title = "Override player gradient color",
|
||||
description = "Apply primary background color to the player gradient color, which changes dynamically with the song.",
|
||||
required = false
|
||||
)
|
||||
|
||||
internal val spotifyBackgroundColorSecondary = stringOption(
|
||||
key = "backgroundColorSecondary",
|
||||
default = "#FF121212",
|
||||
title = "Secondary background color",
|
||||
description =
|
||||
"The secondary background color. (e.g. playlist list in home, player artist, song credits). Can be a hex color or a resource reference.",
|
||||
required = true,
|
||||
)
|
||||
|
||||
internal val spotifyAccentColor = stringOption(
|
||||
key = "accentColor",
|
||||
default = "#FF1ED760",
|
||||
title = "Accent color",
|
||||
description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.",
|
||||
required = true,
|
||||
)
|
||||
|
||||
internal val spotifyAccentColorPressed = stringOption(
|
||||
key = "accentColorPressed",
|
||||
default = "#FF169C46",
|
||||
title = "Pressed dark theme accent color",
|
||||
description =
|
||||
"The color when accented buttons are pressed, by default slightly darker than accent. Can be a hex color or a resource reference.",
|
||||
required = true,
|
||||
)
|
||||
|
||||
private val customThemeBytecodePatch = bytecodePatch {
|
||||
dependsOn(sharedExtensionPatch)
|
||||
|
||||
@@ -71,60 +25,60 @@ private val customThemeBytecodePatch = bytecodePatch {
|
||||
return@execute
|
||||
}
|
||||
|
||||
fun MutableMethod.addColorChangeInstructions(literal: Long, colorString: String) {
|
||||
val index = indexOfFirstLiteralInstructionOrThrow(literal)
|
||||
val register = getInstruction<OneRegisterInstruction>(index).registerA
|
||||
val colorSpaceUtilsClassDef = colorSpaceUtilsClassFingerprint.originalClassDef
|
||||
|
||||
// Hook a util method that converts ARGB to RGBA in the sRGB color space to replace hardcoded accent colors.
|
||||
convertArgbToRgbaFingerprint.match(colorSpaceUtilsClassDef).method.apply {
|
||||
addInstructions(
|
||||
0,
|
||||
"""
|
||||
long-to-int p0, p0
|
||||
invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I
|
||||
move-result p0
|
||||
int-to-long p0, p0
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// Lottie JSON parser method. It parses the JSON Lottie animation into its own class,
|
||||
// including the solid color of it.
|
||||
parseLottieJsonFingerprint.method.apply {
|
||||
val invokeParseColorIndex = indexOfFirstInstructionOrThrow {
|
||||
val reference = getReference<MethodReference>()
|
||||
reference?.definingClass == "Landroid/graphics/Color;"
|
||||
&& reference.name == "parseColor"
|
||||
}
|
||||
val parsedColorRegister = getInstruction<OneRegisterInstruction>(invokeParseColorIndex + 1).registerA
|
||||
|
||||
val replaceColorDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I"
|
||||
|
||||
addInstructions(
|
||||
index + 1,
|
||||
invokeParseColorIndex + 2,
|
||||
"""
|
||||
const-string v$register, "$colorString"
|
||||
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getThemeColor(Ljava/lang/String;)J
|
||||
move-result-wide v$register
|
||||
# Use invoke-static/range because the register number is too large.
|
||||
invoke-static/range { v$parsedColorRegister .. v$parsedColorRegister }, $replaceColorDescriptor
|
||||
move-result v$parsedColorRegister
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
val encoreColorsClassName = with(encoreThemeFingerprint.originalMethod) {
|
||||
// "Encore" colors are referenced right before the value of POSITIVE_INFINITY is returned.
|
||||
// Begin the instruction find using the index of where POSITIVE_INFINITY is set into the register.
|
||||
val positiveInfinityIndex = indexOfFirstLiteralInstructionOrThrow(
|
||||
Float.POSITIVE_INFINITY
|
||||
)
|
||||
val encoreColorsFieldReferenceIndex = indexOfFirstInstructionReversedOrThrow(
|
||||
positiveInfinityIndex,
|
||||
Opcode.SGET_OBJECT
|
||||
)
|
||||
|
||||
getInstruction(encoreColorsFieldReferenceIndex)
|
||||
.getReference<FieldReference>()!!.definingClass
|
||||
}
|
||||
|
||||
val encoreColorsConstructorFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
|
||||
custom { method, classDef ->
|
||||
classDef.type == encoreColorsClassName &&
|
||||
method.containsLiteralInstruction(PLAYLIST_BACKGROUND_COLOR_LITERAL)
|
||||
// Lottie animated color parser.
|
||||
parseAnimatedColorFingerprint.method.apply {
|
||||
val invokeArgbIndex = indexOfFirstInstructionOrThrow {
|
||||
val reference = getReference<MethodReference>()
|
||||
reference?.definingClass == "Landroid/graphics/Color;"
|
||||
&& reference.name == "argb"
|
||||
}
|
||||
val argbColorRegister = getInstruction<OneRegisterInstruction>(invokeArgbIndex + 1).registerA
|
||||
|
||||
addInstructions(
|
||||
invokeArgbIndex + 2,
|
||||
"""
|
||||
invoke-static { v$argbColorRegister }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I
|
||||
move-result v$argbColorRegister
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
val backgroundColor by spotifyBackgroundColor
|
||||
val backgroundColorSecondary by spotifyBackgroundColorSecondary
|
||||
|
||||
encoreColorsConstructorFingerprint.method.apply {
|
||||
addColorChangeInstructions(PLAYLIST_BACKGROUND_COLOR_LITERAL, backgroundColor!!)
|
||||
addColorChangeInstructions(SHARE_MENU_BACKGROUND_COLOR_LITERAL, backgroundColorSecondary!!)
|
||||
}
|
||||
|
||||
homeCategoryPillColorsFingerprint.method.addColorChangeInstructions(
|
||||
HOME_CATEGORY_PILL_COLOR_LITERAL,
|
||||
backgroundColorSecondary!!
|
||||
)
|
||||
|
||||
settingsHeaderColorFingerprint.method.addColorChangeInstructions(
|
||||
SETTINGS_HEADER_COLOR_LITERAL,
|
||||
backgroundColorSecondary!!
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +92,48 @@ val customThemePatch = resourcePatch(
|
||||
|
||||
dependsOn(customThemeBytecodePatch)
|
||||
|
||||
val backgroundColor by spotifyBackgroundColor()
|
||||
val overridePlayerGradientColor by overridePlayerGradientColor()
|
||||
val backgroundColorSecondary by spotifyBackgroundColorSecondary()
|
||||
val accentColor by spotifyAccentColor()
|
||||
val accentColorPressed by spotifyAccentColorPressed()
|
||||
val backgroundColor by stringOption(
|
||||
key = "backgroundColor",
|
||||
default = "@android:color/black",
|
||||
title = "Primary background color",
|
||||
description = "The background color. Can be a hex color or a resource reference.",
|
||||
required = true,
|
||||
)
|
||||
|
||||
val overridePlayerGradientColor by booleanOption(
|
||||
key = "overridePlayerGradientColor",
|
||||
default = false,
|
||||
title = "Override player gradient color",
|
||||
description =
|
||||
"Apply primary background color to the player gradient color, which changes dynamically with the song.",
|
||||
required = false,
|
||||
)
|
||||
|
||||
val backgroundColorSecondary by stringOption(
|
||||
key = "backgroundColorSecondary",
|
||||
default = "#FF121212",
|
||||
title = "Secondary background color",
|
||||
description = "The secondary background color. (e.g. playlist list in home, player artist, song credits). " +
|
||||
"Can be a hex color or a resource reference.\",",
|
||||
required = true,
|
||||
)
|
||||
|
||||
val accentColor by stringOption(
|
||||
key = "accentColor",
|
||||
default = "#FF1ED760",
|
||||
title = "Accent color",
|
||||
description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.",
|
||||
required = true,
|
||||
)
|
||||
|
||||
val accentColorPressed by stringOption(
|
||||
key = "accentColorPressed",
|
||||
default = "#FF1ABC54",
|
||||
title = "Pressed accent color",
|
||||
description = "The color when accented buttons are pressed, by default slightly darker than accent. " +
|
||||
"Can be a hex color or a resource reference.",
|
||||
required = true,
|
||||
)
|
||||
|
||||
execute {
|
||||
document("res/values/colors.xml").use { document ->
|
||||
@@ -161,34 +152,41 @@ val customThemePatch = resourcePatch(
|
||||
}
|
||||
|
||||
node.textContent = when (name) {
|
||||
// Main background color.
|
||||
"gray_7",
|
||||
// Left sidebar background color in tablet mode.
|
||||
"gray_10",
|
||||
// Gradient next to user photo and "All" in home page.
|
||||
"dark_base_background_base",
|
||||
// Main background.
|
||||
"gray_7",
|
||||
// Left sidebar background in tablet mode.
|
||||
"gray_10",
|
||||
// "Add account", "Settings and privacy", "View Profile" left sidebar background.
|
||||
// "Add account", "Settings and privacy", "View Profile" left sidebar background color.
|
||||
"dark_base_background_elevated_base",
|
||||
// Song/player gradient start/end color.
|
||||
"bg_gradient_start_color", "bg_gradient_end_color",
|
||||
// Login screen background and gradient start.
|
||||
// Login screen background color and gradient start.
|
||||
"sthlm_blk", "sthlm_blk_grad_start",
|
||||
// Misc.
|
||||
"image_placeholder_color",
|
||||
-> backgroundColor
|
||||
|
||||
// Track credits, merch background in song player.
|
||||
// "About the artist" background color in song player.
|
||||
"gray_15",
|
||||
// Track credits, merch background color in song player.
|
||||
"track_credits_card_bg", "benefit_list_default_color", "merch_card_background",
|
||||
// Playlist list background in home page.
|
||||
"opacity_white_10",
|
||||
// "About the artist" background in song player.
|
||||
"gray_15",
|
||||
// "What's New" pills background.
|
||||
"dark_base_background_tinted_highlight"
|
||||
-> backgroundColorSecondary
|
||||
|
||||
"dark_brightaccent_background_base", "dark_base_text_brightaccent", "green_light" -> accentColor
|
||||
"dark_brightaccent_background_press" -> accentColorPressed
|
||||
"dark_brightaccent_background_base",
|
||||
"dark_base_text_brightaccent",
|
||||
"green_light",
|
||||
"spotify_green_157"
|
||||
-> accentColor
|
||||
|
||||
"dark_brightaccent_background_press"
|
||||
-> accentColorPressed
|
||||
|
||||
else -> continue
|
||||
}
|
||||
}
|
||||
@@ -198,8 +196,8 @@ val customThemePatch = resourcePatch(
|
||||
document("res/drawable/start_screen_gradient.xml").use { document ->
|
||||
val gradientNode = document.getElementsByTagName("gradient").item(0) as Element
|
||||
|
||||
gradientNode.setAttribute("android:startColor", backgroundColor)
|
||||
gradientNode.setAttribute("android:endColor", backgroundColor)
|
||||
gradientNode.setAttribute("android:startColor", "@color/gray_7")
|
||||
gradientNode.setAttribute("android:endColor", "@color/gray_7")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,30 +4,25 @@ import app.revanced.patcher.fingerprint
|
||||
import app.revanced.util.containsLiteralInstruction
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val encoreThemeFingerprint = fingerprint {
|
||||
strings("Encore theme was not provided.") // Partial string match.
|
||||
custom { method, _ ->
|
||||
method.name == "invoke"
|
||||
}
|
||||
internal val colorSpaceUtilsClassFingerprint = fingerprint {
|
||||
strings("The specified color must be encoded in an RGB color space.") // Partial string match.
|
||||
}
|
||||
|
||||
internal const val PLAYLIST_BACKGROUND_COLOR_LITERAL = 0xFF121212
|
||||
internal const val SHARE_MENU_BACKGROUND_COLOR_LITERAL = 0xFF1F1F1F
|
||||
internal const val HOME_CATEGORY_PILL_COLOR_LITERAL = 0xFF333333
|
||||
internal const val SETTINGS_HEADER_COLOR_LITERAL = 0xFF282828
|
||||
|
||||
internal val homeCategoryPillColorsFingerprint = fingerprint{
|
||||
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
|
||||
custom { method, _ ->
|
||||
method.containsLiteralInstruction(HOME_CATEGORY_PILL_COLOR_LITERAL) &&
|
||||
method.containsLiteralInstruction(0x33000000)
|
||||
}
|
||||
internal val convertArgbToRgbaFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL)
|
||||
returns("J")
|
||||
parameters("J")
|
||||
}
|
||||
|
||||
internal val settingsHeaderColorFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
|
||||
internal val parseLottieJsonFingerprint = fingerprint {
|
||||
strings("Unsupported matte type: ")
|
||||
}
|
||||
|
||||
internal val parseAnimatedColorFingerprint = fingerprint {
|
||||
parameters("L", "F")
|
||||
returns("Ljava/lang/Object;")
|
||||
custom { method, _ ->
|
||||
method.containsLiteralInstruction(SETTINGS_HEADER_COLOR_LITERAL) &&
|
||||
method.containsLiteralInstruction(0)
|
||||
method.containsLiteralInstruction(255.0) &&
|
||||
method.containsLiteralInstruction(1.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ package app.revanced.patches.spotify.lite.ondemand
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
|
||||
@Deprecated("Patch no longer works and will be deleted soon")
|
||||
@Suppress("unused")
|
||||
val onDemandPatch = bytecodePatch(
|
||||
name = "Enable on demand",
|
||||
description = "Enables listening to songs on-demand, allowing to play any song from playlists, albums or artists without limitations. This does not remove ads.",
|
||||
) {
|
||||
compatibleWith("com.spotify.lite")
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package app.revanced.patches.spotify.misc
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstruction
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
import com.android.tools.smali.dexlib2.iface.reference.TypeReference
|
||||
|
||||
internal val accountAttributeFingerprint = fingerprint {
|
||||
context(BytecodePatchContext)
|
||||
internal val accountAttributeFingerprint get() = fingerprint {
|
||||
custom { _, classDef ->
|
||||
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
"Lcom/spotify/useraccount/v1/AccountAttribute;"
|
||||
@@ -19,7 +22,8 @@ internal val accountAttributeFingerprint = fingerprint {
|
||||
}
|
||||
}
|
||||
|
||||
internal val productStateProtoGetMapFingerprint = fingerprint {
|
||||
context(BytecodePatchContext)
|
||||
internal val productStateProtoGetMapFingerprint get() = fingerprint {
|
||||
returns("Ljava/util/Map;")
|
||||
custom { _, classDef ->
|
||||
classDef.type == if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
@@ -34,9 +38,22 @@ internal val buildQueryParametersFingerprint = fingerprint {
|
||||
strings("trackRows", "device_type:tablet")
|
||||
}
|
||||
|
||||
internal val contextMenuExperimentsFingerprint = fingerprint {
|
||||
internal val contextMenuViewModelClassFingerprint = fingerprint {
|
||||
strings("ContextMenuViewModel(header=")
|
||||
}
|
||||
|
||||
internal val contextMenuViewModelAddItemFingerprint = fingerprint {
|
||||
parameters("L")
|
||||
strings("remove_ads_upsell_enabled")
|
||||
returns("V")
|
||||
custom { method, _ ->
|
||||
method.indexOfFirstInstruction {
|
||||
getReference<MethodReference>()?.name == "add"
|
||||
} >= 0
|
||||
}
|
||||
}
|
||||
|
||||
internal val getViewModelFingerprint = fingerprint {
|
||||
custom { method, _ -> method.name == "getViewModel" }
|
||||
}
|
||||
|
||||
internal val contextFromJsonFingerprint = fingerprint {
|
||||
@@ -47,15 +64,15 @@ internal val contextFromJsonFingerprint = fingerprint {
|
||||
Opcode.MOVE_RESULT_OBJECT,
|
||||
Opcode.INVOKE_STATIC
|
||||
)
|
||||
custom { methodDef, classDef ->
|
||||
methodDef.name == "fromJson" &&
|
||||
custom { method, classDef ->
|
||||
method.name == "fromJson" &&
|
||||
classDef.endsWith("voiceassistants/playermodels/ContextJsonAdapter;")
|
||||
}
|
||||
}
|
||||
|
||||
internal val readPlayerOptionOverridesFingerprint = fingerprint {
|
||||
custom { methodDef, classDef ->
|
||||
methodDef.name == "readPlayerOptionOverrides" &&
|
||||
custom { method, classDef ->
|
||||
method.name == "readPlayerOptionOverrides" &&
|
||||
classDef.endsWith("voiceassistants/playermodels/PreparePlayOptionsJsonAdapter;")
|
||||
}
|
||||
}
|
||||
@@ -65,8 +82,15 @@ internal val protobufListsFingerprint = fingerprint {
|
||||
custom { method, _ -> method.name == "emptyProtobufList" }
|
||||
}
|
||||
|
||||
internal val protobufListRemoveFingerprint = fingerprint {
|
||||
custom { method, _ -> method.name == "remove" }
|
||||
internal val abstractProtobufListEnsureIsMutableFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
parameters()
|
||||
returns("V")
|
||||
custom { method, _ ->
|
||||
method.indexOfFirstInstruction {
|
||||
getReference<TypeReference>()?.type == "Ljava/lang/UnsupportedOperationException;"
|
||||
} >= 0
|
||||
}
|
||||
}
|
||||
|
||||
internal val homeSectionFingerprint = fingerprint {
|
||||
@@ -84,7 +108,8 @@ internal val homeStructureGetSectionsFingerprint = fingerprint {
|
||||
internal fun reactivexFunctionApplyWithClassInitFingerprint(className: String) = fingerprint {
|
||||
returns("Ljava/lang/Object;")
|
||||
parameters("Ljava/lang/Object;")
|
||||
custom { method, _ -> method.name == "apply" && method.indexOfFirstInstruction {
|
||||
custom { method, _ ->
|
||||
method.name == "apply" && method.indexOfFirstInstruction {
|
||||
opcode == Opcode.NEW_INSTANCE && getReference<TypeReference>()?.type?.endsWith(className) == true
|
||||
} >= 0
|
||||
}
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
package app.revanced.patches.spotify.misc.extension
|
||||
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.shared.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.spotify.shared.SPOTIFY_MAIN_ACTIVITY_LEGACY
|
||||
|
||||
/**
|
||||
* If patching a legacy 8.x target. This may also be set if patching slightly older/newer app targets,
|
||||
* but the only legacy target of interest is 8.6.98.900 as it's the last version that
|
||||
* supports Spotify integration on Kenwood/Pioneer car stereos.
|
||||
*/
|
||||
internal var IS_SPOTIFY_LEGACY_APP_TARGET = false
|
||||
|
||||
val sharedExtensionPatch = bytecodePatch {
|
||||
dependsOn(sharedExtensionPatch("spotify", mainActivityOnCreateHook))
|
||||
|
||||
execute {
|
||||
IS_SPOTIFY_LEGACY_APP_TARGET = mainActivityOnCreateHook.fingerprint
|
||||
.originalClassDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY
|
||||
}
|
||||
}
|
||||
val sharedExtensionPatch = sharedExtensionPatch("spotify", mainActivityOnCreateHook)
|
||||
|
||||
@@ -15,7 +15,7 @@ val fixFacebookLoginPatch = bytecodePatch(
|
||||
// The Facebook SDK tries to handle the login using the Facebook app in case it is installed.
|
||||
// However, the Facebook app does signature checks with the app that is requesting the authentication,
|
||||
// which ends up making the Facebook server reject with an invalid key hash for the app signature.
|
||||
// Override the Faceboook SDK to always handle the login using the web browser, which does not perform
|
||||
// Override the Facebook SDK to always handle the login using the web browser, which does not perform
|
||||
// signature checks.
|
||||
|
||||
val katanaProxyLoginMethodHandlerClass = katanaProxyLoginMethodHandlerClassFingerprint.originalClassDef
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
package app.revanced.patches.spotify.misc.privacy
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.util.literal
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val shareCopyUrlFingerprint = fingerprint {
|
||||
returns("Ljava/lang/Object;")
|
||||
parameters("Ljava/lang/Object;")
|
||||
strings("clipboard", "Spotify Link")
|
||||
custom { method, _ ->
|
||||
method.name == "invokeSuspend"
|
||||
}
|
||||
}
|
||||
|
||||
internal val shareCopyUrlLegacyFingerprint = fingerprint {
|
||||
returns("Ljava/lang/Object;")
|
||||
parameters("Ljava/lang/Object;")
|
||||
strings("clipboard", "createNewSession failed")
|
||||
custom { method, _ ->
|
||||
method.name == "apply"
|
||||
}
|
||||
}
|
||||
|
||||
internal val formatAndroidShareSheetUrlFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||
returns("Ljava/lang/String;")
|
||||
parameters("L", "Ljava/lang/String;")
|
||||
literal {
|
||||
'\n'.code.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
internal val formatAndroidShareSheetUrlLegacyFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC)
|
||||
returns("Ljava/lang/String;")
|
||||
parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;")
|
||||
literal {
|
||||
'\n'.code.toLong()
|
||||
}
|
||||
}
|
||||
package app.revanced.patches.spotify.misc.privacy
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.util.literal
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val shareCopyUrlFingerprint = fingerprint {
|
||||
returns("Ljava/lang/Object;")
|
||||
parameters("Ljava/lang/Object;")
|
||||
strings("clipboard", "Spotify Link")
|
||||
custom { method, _ ->
|
||||
method.name == "invokeSuspend"
|
||||
}
|
||||
}
|
||||
|
||||
internal val shareCopyUrlLegacyFingerprint = fingerprint {
|
||||
returns("Ljava/lang/Object;")
|
||||
parameters("Ljava/lang/Object;")
|
||||
strings("clipboard", "createNewSession failed")
|
||||
custom { method, _ ->
|
||||
method.name == "apply"
|
||||
}
|
||||
}
|
||||
|
||||
internal val formatAndroidShareSheetUrlFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||
returns("Ljava/lang/String;")
|
||||
parameters("L", "Ljava/lang/String;")
|
||||
literal {
|
||||
'\n'.code.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
internal val formatAndroidShareSheetUrlLegacyFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC)
|
||||
returns("Ljava/lang/String;")
|
||||
parameters("Lcom/spotify/share/social/sharedata/ShareData;", "Ljava/lang/String;")
|
||||
literal {
|
||||
'\n'.code.toLong()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,70 +1,70 @@
|
||||
package app.revanced.patches.spotify.misc.privacy
|
||||
|
||||
import app.revanced.patcher.Fingerprint
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||
import app.revanced.patches.spotify.misc.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
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch;"
|
||||
|
||||
@Suppress("unused")
|
||||
val sanitizeSharingLinksPatch = bytecodePatch(
|
||||
name = "Sanitize sharing links",
|
||||
description = "Removes the tracking query parameters from links before they are shared.",
|
||||
) {
|
||||
compatibleWith("com.spotify.music")
|
||||
|
||||
dependsOn(sharedExtensionPatch)
|
||||
|
||||
execute {
|
||||
val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" +
|
||||
"sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;"
|
||||
|
||||
val copyFingerprint = if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
shareCopyUrlLegacyFingerprint
|
||||
} else {
|
||||
shareCopyUrlFingerprint
|
||||
}
|
||||
|
||||
copyFingerprint.method.apply {
|
||||
val newPlainTextInvokeIndex = indexOfFirstInstructionOrThrow {
|
||||
getReference<MethodReference>()?.name == "newPlainText"
|
||||
}
|
||||
val register = getInstruction<FiveRegisterInstruction>(newPlainTextInvokeIndex).registerD
|
||||
|
||||
addInstructions(
|
||||
newPlainTextInvokeIndex,
|
||||
"""
|
||||
invoke-static { v$register }, $extensionMethodDescriptor
|
||||
move-result-object v$register
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// Android native share sheet is used for all other quick share types (X, WhatsApp, etc).
|
||||
val shareUrlParameter : String
|
||||
val shareSheetFingerprint : Fingerprint
|
||||
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
shareSheetFingerprint = formatAndroidShareSheetUrlLegacyFingerprint
|
||||
shareUrlParameter = "p2"
|
||||
} else {
|
||||
shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint
|
||||
shareUrlParameter = "p1"
|
||||
}
|
||||
|
||||
shareSheetFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
invoke-static { $shareUrlParameter }, $extensionMethodDescriptor
|
||||
move-result-object $shareUrlParameter
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
package app.revanced.patches.spotify.misc.privacy
|
||||
|
||||
import app.revanced.patcher.Fingerprint
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||
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 =
|
||||
"Lapp/revanced/extension/spotify/misc/privacy/SanitizeSharingLinksPatch;"
|
||||
|
||||
@Suppress("unused")
|
||||
val sanitizeSharingLinksPatch = bytecodePatch(
|
||||
name = "Sanitize sharing links",
|
||||
description = "Removes the tracking query parameters from links before they are shared.",
|
||||
) {
|
||||
compatibleWith("com.spotify.music")
|
||||
|
||||
dependsOn(sharedExtensionPatch)
|
||||
|
||||
execute {
|
||||
val extensionMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->" +
|
||||
"sanitizeUrl(Ljava/lang/String;)Ljava/lang/String;"
|
||||
|
||||
val copyFingerprint = if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
shareCopyUrlLegacyFingerprint
|
||||
} else {
|
||||
shareCopyUrlFingerprint
|
||||
}
|
||||
|
||||
copyFingerprint.method.apply {
|
||||
val newPlainTextInvokeIndex = indexOfFirstInstructionOrThrow {
|
||||
getReference<MethodReference>()?.name == "newPlainText"
|
||||
}
|
||||
val urlRegister = getInstruction<FiveRegisterInstruction>(newPlainTextInvokeIndex).registerD
|
||||
|
||||
addInstructions(
|
||||
newPlainTextInvokeIndex,
|
||||
"""
|
||||
invoke-static { v$urlRegister }, $extensionMethodDescriptor
|
||||
move-result-object v$urlRegister
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// Android native share sheet is used for all other quick share types (X, WhatsApp, etc).
|
||||
val shareUrlParameter : String
|
||||
val shareSheetFingerprint : Fingerprint
|
||||
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
shareSheetFingerprint = formatAndroidShareSheetUrlLegacyFingerprint
|
||||
shareUrlParameter = "p2"
|
||||
} else {
|
||||
shareSheetFingerprint = formatAndroidShareSheetUrlFingerprint
|
||||
shareUrlParameter = "p1"
|
||||
}
|
||||
|
||||
shareSheetFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
invoke-static { $shareUrlParameter }, $extensionMethodDescriptor
|
||||
move-result-object $shareUrlParameter
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.revanced.patches.spotify.misc.widgets
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.util.indexOfFirstInstruction
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
|
||||
internal val canBindAppWidgetPermissionFingerprint = fingerprint {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package app.revanced.patches.spotify.misc.widgets
|
||||
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
|
||||
import app.revanced.util.returnEarly
|
||||
import java.util.logging.Logger
|
||||
|
||||
@Suppress("unused")
|
||||
val fixThirdPartyLaunchersWidgets = bytecodePatch(
|
||||
@@ -11,6 +13,14 @@ val fixThirdPartyLaunchersWidgets = bytecodePatch(
|
||||
compatibleWith("com.spotify.music")
|
||||
|
||||
execute {
|
||||
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
|
||||
// The permission check does not exist in legacy versions.
|
||||
Logger.getLogger(this::class.java.name).warning(
|
||||
"Legacy app target does not have any third party launcher restrictions. No changes applied."
|
||||
)
|
||||
return@execute
|
||||
}
|
||||
|
||||
// Only system app launchers are granted the BIND_APPWIDGET permission.
|
||||
// Override the method that checks for it to always return true, as this permission is not actually required
|
||||
// for the widgets to work.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package app.revanced.patches.spotify.shared
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patches.spotify.misc.extension.mainActivityOnCreateHook
|
||||
|
||||
private const val SPOTIFY_MAIN_ACTIVITY = "Lcom/spotify/music/SpotifyMainActivity;"
|
||||
|
||||
@@ -15,3 +17,18 @@ internal val mainActivityOnCreateFingerprint = fingerprint {
|
||||
|| classDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY)
|
||||
}
|
||||
}
|
||||
|
||||
private var isLegacyAppTarget: Boolean? = null
|
||||
|
||||
/**
|
||||
* If patching a legacy 8.x target. This may also be set if patching slightly older/newer app targets,
|
||||
* but the only legacy target of interest is 8.6.98.900 as it's the last version that
|
||||
* supports Spotify integration on Kenwood/Pioneer car stereos.
|
||||
*/
|
||||
context(BytecodePatchContext)
|
||||
internal val IS_SPOTIFY_LEGACY_APP_TARGET get(): Boolean {
|
||||
if (isLegacyAppTarget == null) {
|
||||
isLegacyAppTarget = mainActivityOnCreateHook.fingerprint.originalClassDef.type == SPOTIFY_MAIN_ACTIVITY_LEGACY
|
||||
}
|
||||
return isLegacyAppTarget!!
|
||||
}
|
||||
|
||||
@@ -48,7 +48,10 @@ private val swipeControlsResourcePatch = resourcePatch {
|
||||
summaryKey = null,
|
||||
),
|
||||
TextPreference("revanced_swipe_overlay_background_opacity", inputType = InputType.NUMBER),
|
||||
TextPreference("revanced_swipe_overlay_progress_color",
|
||||
TextPreference("revanced_swipe_overlay_progress_brightness_color",
|
||||
tag = "app.revanced.extension.shared.settings.preference.ColorPickerPreference",
|
||||
inputType = InputType.TEXT_CAP_CHARACTERS),
|
||||
TextPreference("revanced_swipe_overlay_progress_volume_color",
|
||||
tag = "app.revanced.extension.shared.settings.preference.ColorPickerPreference",
|
||||
inputType = InputType.TEXT_CAP_CHARACTERS),
|
||||
TextPreference("revanced_swipe_text_overlay_size", inputType = InputType.NUMBER),
|
||||
|
||||
@@ -2,6 +2,7 @@ package app.revanced.patches.youtube.layout.buttons.overlay
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.util.containsLiteralInstruction
|
||||
import app.revanced.util.literal
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val playerControlsPreviousNextOverlayTouchFingerprint = fingerprint {
|
||||
@@ -20,3 +21,10 @@ internal val mediaRouteButtonFingerprint = fingerprint {
|
||||
methodDef.definingClass.endsWith("/MediaRouteButton;") && methodDef.name == "setVisibility"
|
||||
}
|
||||
}
|
||||
|
||||
internal val inflateControlsGroupLayoutStubFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
parameters()
|
||||
returns("V")
|
||||
literal { controlsButtonGroupLayoutStub }
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ internal var playerControlPreviousButtonTouchArea = -1L
|
||||
private set
|
||||
internal var playerControlNextButtonTouchArea = -1L
|
||||
private set
|
||||
internal var controlsButtonGroupLayoutStub = -1L
|
||||
private set
|
||||
|
||||
private val hidePlayerOverlayButtonsResourcePatch = resourcePatch {
|
||||
dependsOn(resourceMappingPatch)
|
||||
@@ -35,6 +37,7 @@ private val hidePlayerOverlayButtonsResourcePatch = resourcePatch {
|
||||
execute {
|
||||
playerControlPreviousButtonTouchArea = resourceMappings["id", "player_control_previous_button_touch_area"]
|
||||
playerControlNextButtonTouchArea = resourceMappings["id", "player_control_next_button_touch_area"]
|
||||
controlsButtonGroupLayoutStub = resourceMappings["id", "youtube_controls_button_group_layout_stub"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +46,8 @@ private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
|
||||
val hidePlayerOverlayButtonsPatch = bytecodePatch(
|
||||
name = "Hide player overlay buttons",
|
||||
description = "Adds options to hide the player Cast, Autoplay, Captions, and Previous & Next buttons.",
|
||||
description = "Adds options to hide the player Cast, Autoplay, Captions, Previous & Next buttons, and the player " +
|
||||
"control buttons background.",
|
||||
) {
|
||||
dependsOn(
|
||||
sharedExtensionPatch,
|
||||
@@ -72,6 +76,7 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch(
|
||||
SwitchPreference("revanced_hide_cast_button"),
|
||||
SwitchPreference("revanced_hide_captions_button"),
|
||||
SwitchPreference("revanced_hide_autoplay_button"),
|
||||
SwitchPreference("revanced_hide_player_control_buttons_background"),
|
||||
)
|
||||
|
||||
// region Hide player next/previous button.
|
||||
@@ -147,5 +152,32 @@ val hidePlayerOverlayButtonsPatch = bytecodePatch(
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Hide player control buttons background.
|
||||
|
||||
inflateControlsGroupLayoutStubFingerprint.method.apply {
|
||||
val controlsButtonGroupLayoutStubResIdConstIndex =
|
||||
indexOfFirstLiteralInstructionOrThrow(controlsButtonGroupLayoutStub)
|
||||
val inflateControlsGroupLayoutStubIndex =
|
||||
indexOfFirstInstruction(controlsButtonGroupLayoutStubResIdConstIndex) {
|
||||
getReference<MethodReference>()?.name == "inflate"
|
||||
}
|
||||
|
||||
val freeRegister = findFreeRegister(inflateControlsGroupLayoutStubIndex)
|
||||
val hidePlayerControlButtonsBackgroundDescriptor =
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->hidePlayerControlButtonsBackground(Landroid/view/View;)V"
|
||||
|
||||
addInstructions(
|
||||
inflateControlsGroupLayoutStubIndex + 1,
|
||||
"""
|
||||
# Move the inflated layout to a temporary register.
|
||||
# The result of the inflate method is by default not moved to a register after the method is called.
|
||||
move-result-object v$freeRegister
|
||||
invoke-static { v$freeRegister }, $hidePlayerControlButtonsBackgroundDescriptor
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ val hideLayoutComponentsPatch = bytecodePatch(
|
||||
SwitchPreference("revanced_hide_comments_by_members_header"),
|
||||
SwitchPreference("revanced_hide_comments_section"),
|
||||
SwitchPreference("revanced_hide_comments_create_a_short_button"),
|
||||
SwitchPreference("revanced_hide_comments_timestamp_and_emoji_buttons"),
|
||||
SwitchPreference("revanced_hide_comments_timestamp_button"),
|
||||
SwitchPreference("revanced_hide_comments_preview_comment"),
|
||||
SwitchPreference("revanced_hide_comments_thanks_button"),
|
||||
),
|
||||
|
||||
@@ -74,3 +74,21 @@ internal val setPivotBarVisibilityParentFingerprint = fingerprint {
|
||||
parameters("Z")
|
||||
strings("FEnotifications_inbox")
|
||||
}
|
||||
|
||||
internal val shortsExperimentalPlayerFeatureFlagFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returns("Z")
|
||||
parameters()
|
||||
literal {
|
||||
45677719L
|
||||
}
|
||||
}
|
||||
|
||||
internal val renderNextUIFeatureFlagFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returns("Z")
|
||||
parameters()
|
||||
literal {
|
||||
45649743L
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter
|
||||
import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch
|
||||
import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch
|
||||
import app.revanced.patches.youtube.misc.playservice.is_19_41_or_greater
|
||||
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
|
||||
@@ -26,6 +27,7 @@ import app.revanced.util.forEachLiteralValueInstruction
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import app.revanced.util.indexOfFirstLiteralInstruction
|
||||
import app.revanced.util.returnLate
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
|
||||
@@ -90,11 +92,13 @@ private val hideShortsComponentsResourcePatch = resourcePatch {
|
||||
SwitchPreference("revanced_hide_shorts_paused_overlay_buttons"),
|
||||
|
||||
// Suggested actions.
|
||||
SwitchPreference("revanced_hide_shorts_preview_comment"),
|
||||
SwitchPreference("revanced_hide_shorts_save_sound_button"),
|
||||
SwitchPreference("revanced_hide_shorts_use_template_button"),
|
||||
SwitchPreference("revanced_hide_shorts_upcoming_button"),
|
||||
SwitchPreference("revanced_hide_shorts_green_screen_button"),
|
||||
SwitchPreference("revanced_hide_shorts_hashtag_button"),
|
||||
SwitchPreference("revanced_hide_shorts_new_posts_button"),
|
||||
SwitchPreference("revanced_hide_shorts_shop_button"),
|
||||
SwitchPreference("revanced_hide_shorts_tagged_products"),
|
||||
SwitchPreference("revanced_hide_shorts_search_suggestions"),
|
||||
@@ -251,5 +255,27 @@ val hideShortsComponentsPatch = bytecodePatch(
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Disable experimental Shorts flags.
|
||||
|
||||
// Flags might be present in earlier targets, but they are not found in 19.47.53.
|
||||
// If these flags are forced on, the experimental layout is still not used and
|
||||
// it appears the features requires additional server side data to fully use.
|
||||
if (is_20_07_or_greater) {
|
||||
// Experimental Shorts player uses Android native buttons and not Litho,
|
||||
// and the layout is provided by the server.
|
||||
//
|
||||
// Since the buttons are native components and not Litho, it should be possible to
|
||||
// fix the RYD Shorts loading delay by asynchronously loading RYD and updating
|
||||
// the button text after RYD has loaded.
|
||||
shortsExperimentalPlayerFeatureFlagFingerprint.method.returnLate(false)
|
||||
|
||||
// Experimental UI renderer must also be disabled since it requires the
|
||||
// experimental Shorts player. If this is enabled but Shorts player
|
||||
// is disabled then the app crashes when the Shorts player is opened.
|
||||
renderNextUIFeatureFlagFingerprint.method.returnLate(false)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,12 @@
|
||||
package app.revanced.patches.youtube.layout.player.background
|
||||
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.util.doRecursively
|
||||
import org.w3c.dom.Element
|
||||
import app.revanced.patches.youtube.layout.buttons.overlay.hidePlayerOverlayButtonsPatch
|
||||
|
||||
@Suppress("unused")
|
||||
@Deprecated("Functionality added to hidePlayerOverlayButtonsPatch", ReplaceWith("hidePlayerOverlayButtonsPatch"))
|
||||
val playerControlsBackgroundPatch = resourcePatch(
|
||||
name = "Remove player controls background",
|
||||
description = "Removes the dark background surrounding the video player controls.",
|
||||
use = false,
|
||||
description = "Removes the dark background surrounding the video player control buttons.",
|
||||
) {
|
||||
compatibleWith(
|
||||
"com.google.android.youtube"(
|
||||
"19.16.39",
|
||||
"19.25.37",
|
||||
"19.34.42",
|
||||
"19.43.41",
|
||||
"19.47.53",
|
||||
"20.07.39",
|
||||
"20.12.46",
|
||||
)
|
||||
)
|
||||
|
||||
execute {
|
||||
document("res/drawable/player_button_circle_background.xml").use { document ->
|
||||
|
||||
document.doRecursively node@{ node ->
|
||||
if (node !is Element) return@node
|
||||
|
||||
node.getAttributeNode("android:color")?.let { attribute ->
|
||||
attribute.textContent = "@android:color/transparent"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
dependsOn(hidePlayerOverlayButtonsPatch)
|
||||
}
|
||||
|
||||
@@ -44,6 +44,19 @@ internal val themeHelperLightColorFingerprint = fingerprint {
|
||||
}
|
||||
}
|
||||
|
||||
internal const val GRADIENT_LOADING_SCREEN_AB_CONSTANT = 45412406L
|
||||
|
||||
internal val useGradientLoadingScreenFingerprint = fingerprint {
|
||||
literal { GRADIENT_LOADING_SCREEN_AB_CONSTANT }
|
||||
}
|
||||
|
||||
internal const val SPLASH_SCREEN_STYLE_FEATURE_FLAG = 269032877L
|
||||
|
||||
internal val splashScreenStyleFingerprint = fingerprint {
|
||||
returns("V")
|
||||
parameters("Landroid/os/Bundle;")
|
||||
literal { SPLASH_SCREEN_STYLE_FEATURE_FLAG }
|
||||
custom { method, classDef ->
|
||||
method.name == "onCreate" && classDef.endsWith("/MainActivity;")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
|
||||
import app.revanced.patches.shared.misc.settings.preference.BasePreference
|
||||
import app.revanced.patches.shared.misc.settings.preference.InputType
|
||||
import app.revanced.patches.shared.misc.settings.preference.ListPreference
|
||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory
|
||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting
|
||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||
@@ -17,6 +18,7 @@ import app.revanced.patches.shared.misc.settings.preference.TextPreference
|
||||
import app.revanced.patches.youtube.layout.seekbar.seekbarColorPatch
|
||||
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
|
||||
import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater
|
||||
import app.revanced.patches.youtube.misc.playservice.is_19_47_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
|
||||
@@ -27,8 +29,6 @@ import org.w3c.dom.Element
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/youtube/patches/theme/ThemePatch;"
|
||||
|
||||
internal const val GRADIENT_LOADING_SCREEN_AB_CONSTANT = 45412406L
|
||||
|
||||
val themePatch = bytecodePatch(
|
||||
name = "Theme",
|
||||
description = "Adds options for theming and applies a custom background theme (dark background theme defaults to amoled black).",
|
||||
@@ -232,15 +232,32 @@ val themePatch = bytecodePatch(
|
||||
addResources("youtube", "layout.theme.themePatch")
|
||||
|
||||
PreferenceScreen.GENERAL_LAYOUT.addPreferences(
|
||||
SwitchPreference("revanced_gradient_loading_screen"),
|
||||
SwitchPreference("revanced_gradient_loading_screen")
|
||||
)
|
||||
|
||||
if (is_19_47_or_greater) {
|
||||
PreferenceScreen.GENERAL_LAYOUT.addPreferences(
|
||||
ListPreference(
|
||||
key = "splash_screen_animation_style",
|
||||
summaryKey = null
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
useGradientLoadingScreenFingerprint.method.insertLiteralOverride(
|
||||
GRADIENT_LOADING_SCREEN_AB_CONSTANT,
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->gradientLoadingScreenEnabled(Z)Z"
|
||||
)
|
||||
|
||||
mapOf(
|
||||
if (is_19_47_or_greater) {
|
||||
// Lottie splash screen exists in earlier versions, but it may not be always on.
|
||||
splashScreenStyleFingerprint.method.insertLiteralOverride(
|
||||
SPLASH_SCREEN_STYLE_FEATURE_FLAG,
|
||||
"$EXTENSION_CLASS_DESCRIPTOR->getLoadingScreenType(I)I"
|
||||
)
|
||||
}
|
||||
|
||||
arrayOf(
|
||||
themeHelperLightColorFingerprint to lightThemeBackgroundColor,
|
||||
themeHelperDarkColorFingerprint to darkThemeBackgroundColor,
|
||||
).forEach { (fingerprint, color) ->
|
||||
|
||||
@@ -5,6 +5,7 @@ import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
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.settings.preference.NonInteractivePreference
|
||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting
|
||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||
@@ -23,7 +24,7 @@ private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
|
||||
val enableDebuggingPatch = bytecodePatch(
|
||||
name = "Enable debugging",
|
||||
description = "Adds options for debugging.",
|
||||
description = "Adds options for debugging and exporting ReVanced logs to the clipboard.",
|
||||
) {
|
||||
dependsOn(
|
||||
sharedExtensionPatch,
|
||||
@@ -56,6 +57,16 @@ val enableDebuggingPatch = bytecodePatch(
|
||||
SwitchPreference("revanced_debug_protobuffer"),
|
||||
SwitchPreference("revanced_debug_stacktrace"),
|
||||
SwitchPreference("revanced_debug_toast_on_error"),
|
||||
NonInteractivePreference(
|
||||
"revanced_debug_export_logs_to_clipboard",
|
||||
tag = "app.revanced.extension.youtube.settings.preference.ExportLogToClipboardPreference",
|
||||
selectable = true,
|
||||
),
|
||||
NonInteractivePreference(
|
||||
"revanced_debug_logs_clear_buffer",
|
||||
tag = "app.revanced.extension.youtube.settings.preference.ClearLogBufferPreference",
|
||||
selectable = true,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -107,7 +118,6 @@ val enableDebuggingPatch = bytecodePatch(
|
||||
return-wide v0
|
||||
"""
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
experimentalStringFeatureFlagFingerprint.match(
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
package app.revanced.patches.youtube.misc.hapticfeedback
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.smali.ExternalLabel
|
||||
import app.revanced.patches.all.misc.resources.addResources
|
||||
import app.revanced.patches.all.misc.resources.addResourcesPatch
|
||||
import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference
|
||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
|
||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/youtube/patches/DisableHapticFeedbackPatch;"
|
||||
|
||||
@Suppress("unused")
|
||||
val disableHapticFeedbackPatch = bytecodePatch(
|
||||
name = "Disable haptic feedback",
|
||||
description = "Adds an option to disable haptic feedback in the player for various actions.",
|
||||
) {
|
||||
dependsOn(
|
||||
settingsPatch,
|
||||
addResourcesPatch,
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
"com.google.android.youtube"(
|
||||
"19.16.39",
|
||||
"19.25.37",
|
||||
"19.34.42",
|
||||
"19.43.41",
|
||||
"19.47.53",
|
||||
"20.07.39",
|
||||
"20.12.46",
|
||||
)
|
||||
)
|
||||
|
||||
execute {
|
||||
addResources("youtube", "misc.hapticfeedback.disableHapticFeedbackPatch")
|
||||
|
||||
PreferenceScreen.PLAYER.addPreferences(
|
||||
PreferenceScreenPreference(
|
||||
"revanced_disable_haptic_feedback",
|
||||
preferences = setOf(
|
||||
SwitchPreference("revanced_disable_haptic_feedback_chapters"),
|
||||
SwitchPreference("revanced_disable_haptic_feedback_precise_seeking"),
|
||||
SwitchPreference("revanced_disable_haptic_feedback_seek_undo"),
|
||||
SwitchPreference("revanced_disable_haptic_feedback_zoom"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
arrayOf(
|
||||
markerHapticsFingerprint to "disableChapterVibrate",
|
||||
scrubbingHapticsFingerprint to "disablePreciseSeekingVibrate",
|
||||
seekUndoHapticsFingerprint to "disableSeekUndoVibrate",
|
||||
zoomHapticsFingerprint to "disableZoomVibrate"
|
||||
).forEach { (fingerprint, methodName) ->
|
||||
fingerprint.method.apply {
|
||||
addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->$methodName()Z
|
||||
move-result v0
|
||||
if-eqz v0, :vibrate
|
||||
return-void
|
||||
""",
|
||||
ExternalLabel("vibrate", getInstruction(0))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package app.revanced.patches.youtube.misc.hapticfeedback
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
internal val markerHapticsFingerprint = fingerprint {
|
||||
returns("V")
|
||||
strings("Failed to execute markers haptics vibrate.")
|
||||
}
|
||||
|
||||
internal val scrubbingHapticsFingerprint = fingerprint {
|
||||
returns("V")
|
||||
strings("Failed to haptics vibrate for fine scrubbing.")
|
||||
}
|
||||
|
||||
internal val seekUndoHapticsFingerprint = fingerprint {
|
||||
returns("V")
|
||||
strings("Failed to execute seek undo haptics vibrate.")
|
||||
}
|
||||
|
||||
internal val zoomHapticsFingerprint = fingerprint {
|
||||
returns("V")
|
||||
strings("Failed to haptics vibrate for video zoom")
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package app.revanced.patches.youtube.misc.zoomhaptics
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
internal val zoomHapticsFingerprint = fingerprint {
|
||||
strings("Failed to haptics vibrate for video zoom")
|
||||
}
|
||||
@@ -1,54 +1,11 @@
|
||||
package app.revanced.patches.youtube.misc.zoomhaptics
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.smali.ExternalLabel
|
||||
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.settings.PreferenceScreen
|
||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||
import app.revanced.patches.youtube.misc.hapticfeedback.disableHapticFeedbackPatch
|
||||
|
||||
@Deprecated("Superseded by disableHapticFeedbackPatch", ReplaceWith("disableHapticFeedbackPatch"))
|
||||
val zoomHapticsPatch = bytecodePatch(
|
||||
name = "Disable zoom haptics",
|
||||
description = "Adds an option to disable haptics when zooming.",
|
||||
) {
|
||||
dependsOn(
|
||||
settingsPatch,
|
||||
addResourcesPatch,
|
||||
)
|
||||
|
||||
compatibleWith(
|
||||
"com.google.android.youtube"(
|
||||
"19.16.39",
|
||||
"19.25.37",
|
||||
"19.34.42",
|
||||
"19.43.41",
|
||||
"19.47.53",
|
||||
"20.07.39",
|
||||
"20.12.46",
|
||||
)
|
||||
)
|
||||
|
||||
execute {
|
||||
addResources("youtube", "misc.zoomhaptics.zoomHapticsPatch")
|
||||
|
||||
PreferenceScreen.MISC.addPreferences(
|
||||
SwitchPreference("revanced_disable_zoom_haptics"),
|
||||
)
|
||||
|
||||
zoomHapticsFingerprint.method.apply {
|
||||
addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
invoke-static { }, Lapp/revanced/extension/youtube/patches/ZoomHapticsPatch;->shouldVibrate()Z
|
||||
move-result v0
|
||||
if-nez v0, :vibrate
|
||||
return-void
|
||||
""",
|
||||
ExternalLabel("vibrate", getInstruction(0)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
dependsOn(disableHapticFeedbackPatch)
|
||||
}
|
||||
@@ -123,3 +123,13 @@ internal val playbackSpeedMenuSpeedChangedFingerprint = fingerprint {
|
||||
Opcode.RETURN_OBJECT,
|
||||
)
|
||||
}
|
||||
|
||||
internal val playbackSpeedClassFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||
returns("L")
|
||||
parameters("L")
|
||||
opcodes(
|
||||
Opcode.RETURN_OBJECT
|
||||
)
|
||||
strings("PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT")
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
||||
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.video.playerresponse.Hook
|
||||
@@ -16,6 +17,8 @@ import app.revanced.patches.youtube.video.videoid.hookBackgroundPlayVideoId
|
||||
import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId
|
||||
import app.revanced.patches.youtube.video.videoid.hookVideoId
|
||||
import app.revanced.patches.youtube.video.videoid.videoIdPatch
|
||||
import app.revanced.util.addInstructionsAtControlFlowLabel
|
||||
import app.revanced.util.addStaticFieldToExtension
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
@@ -29,6 +32,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
|
||||
@@ -189,6 +193,72 @@ val videoInformationPatch = bytecodePatch(
|
||||
proxy(classes.first { it.type == setPlaybackSpeedMethodReference.definingClass })
|
||||
.mutableClass.methods.first { it.name == setPlaybackSpeedMethodReference.name }
|
||||
setPlaybackSpeedMethodIndex = 0
|
||||
|
||||
// Add override playback speed method.
|
||||
onPlaybackSpeedItemClickFingerprint.classDef.methods.add(
|
||||
ImmutableMethod(
|
||||
definingClass,
|
||||
"overridePlaybackSpeed",
|
||||
listOf(ImmutableMethodParameter("F", annotations, null)),
|
||||
"V",
|
||||
AccessFlags.PUBLIC.value or AccessFlags.PUBLIC.value,
|
||||
annotations,
|
||||
null,
|
||||
ImmutableMethodImplementation(
|
||||
4,
|
||||
"""
|
||||
# Check if the playback speed is not auto (-2.0f)
|
||||
const/4 v0, 0x0
|
||||
cmpg-float v0, v3, v0
|
||||
if-lez v0, :ignore
|
||||
|
||||
# Get the container class field.
|
||||
iget-object v0, v2, $setPlaybackSpeedContainerClassFieldReference
|
||||
|
||||
# For some reason, in YouTube 19.44.39 this value is sometimes null.
|
||||
if-eqz v0, :ignore
|
||||
|
||||
# Get the field from its class.
|
||||
iget-object v1, v0, $setPlaybackSpeedClassFieldReference
|
||||
|
||||
# Invoke setPlaybackSpeed on that class.
|
||||
invoke-virtual {v1, v3}, $setPlaybackSpeedMethodReference
|
||||
|
||||
:ignore
|
||||
return-void
|
||||
""".toInstructions(), null, null
|
||||
)
|
||||
).toMutable()
|
||||
)
|
||||
}
|
||||
|
||||
playbackSpeedClassFingerprint.method.apply {
|
||||
val index = indexOfFirstInstructionOrThrow(Opcode.RETURN_OBJECT)
|
||||
val register = getInstruction<OneRegisterInstruction>(index).registerA
|
||||
val playbackSpeedClass = this.returnType
|
||||
|
||||
// Set playback speed class.
|
||||
addInstructionsAtControlFlowLabel(
|
||||
index,
|
||||
"sput-object v$register, $EXTENSION_CLASS_DESCRIPTOR->playbackSpeedClass:$playbackSpeedClass"
|
||||
)
|
||||
|
||||
val smaliInstructions =
|
||||
"""
|
||||
if-eqz v0, :ignore
|
||||
invoke-virtual {v0, p0}, $playbackSpeedClass->overridePlaybackSpeed(F)V
|
||||
return-void
|
||||
:ignore
|
||||
nop
|
||||
"""
|
||||
|
||||
addStaticFieldToExtension(
|
||||
EXTENSION_CLASS_DESCRIPTOR,
|
||||
"overridePlaybackSpeed",
|
||||
"playbackSpeedClass",
|
||||
playbackSpeedClass,
|
||||
smaliInstructions
|
||||
)
|
||||
}
|
||||
|
||||
// Handle new playback speed menu.
|
||||
|
||||
@@ -52,14 +52,14 @@ val rememberVideoQualityPatch = bytecodePatch {
|
||||
ListPreference(
|
||||
key = "revanced_shorts_quality_default_mobile",
|
||||
summaryKey = null,
|
||||
entriesKey = "revanced_video_quality_default_entries",
|
||||
entryValuesKey = "revanced_video_quality_default_entry_values",
|
||||
entriesKey = "revanced_shorts_quality_default_entries",
|
||||
entryValuesKey = "revanced_shorts_quality_default_entry_values",
|
||||
),
|
||||
ListPreference(
|
||||
key = "revanced_shorts_quality_default_wifi",
|
||||
summaryKey = null,
|
||||
entriesKey = "revanced_video_quality_default_entries",
|
||||
entryValuesKey = "revanced_video_quality_default_entry_values",
|
||||
entriesKey = "revanced_shorts_quality_default_entries",
|
||||
entryValuesKey = "revanced_shorts_quality_default_entry_values",
|
||||
),
|
||||
SwitchPreference("revanced_remember_shorts_quality_last_selected")
|
||||
))
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
package app.revanced.patches.youtube.video.speed.custom
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||
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.InputType
|
||||
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
|
||||
import app.revanced.patches.shared.misc.settings.preference.TextPreference
|
||||
@@ -27,27 +19,11 @@ import app.revanced.patches.youtube.misc.recyclerviewtree.hook.addRecyclerViewTr
|
||||
import app.revanced.patches.youtube.misc.recyclerviewtree.hook.recyclerViewTreeHookPatch
|
||||
import app.revanced.patches.youtube.misc.settings.settingsPatch
|
||||
import app.revanced.patches.youtube.video.speed.settingsMenuVideoSpeedGroup
|
||||
import app.revanced.util.*
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import app.revanced.util.indexOfFirstLiteralInstruction
|
||||
import app.revanced.util.indexOfFirstLiteralInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableField
|
||||
|
||||
internal var speedUnavailableId = -1L
|
||||
private set
|
||||
|
||||
private val customPlaybackSpeedResourcePatch = resourcePatch {
|
||||
dependsOn(resourceMappingPatch)
|
||||
|
||||
execute {
|
||||
speedUnavailableId = resourceMappings[
|
||||
"string",
|
||||
"varispeed_unavailable_message",
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
private const val FILTER_CLASS_DESCRIPTOR =
|
||||
"Lapp/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch;"
|
||||
@@ -64,8 +40,7 @@ internal val customPlaybackSpeedPatch = bytecodePatch(
|
||||
addResourcesPatch,
|
||||
lithoFilterPatch,
|
||||
versionCheckPatch,
|
||||
recyclerViewTreeHookPatch,
|
||||
customPlaybackSpeedResourcePatch
|
||||
recyclerViewTreeHookPatch
|
||||
)
|
||||
|
||||
execute {
|
||||
@@ -87,38 +62,6 @@ internal val customPlaybackSpeedPatch = bytecodePatch(
|
||||
)
|
||||
}
|
||||
|
||||
// Replace the speeds float array with custom speeds.
|
||||
speedArrayGeneratorFingerprint.method.apply {
|
||||
val sizeCallIndex = indexOfFirstInstructionOrThrow { getReference<MethodReference>()?.name == "size" }
|
||||
val sizeCallResultRegister = getInstruction<OneRegisterInstruction>(sizeCallIndex + 1).registerA
|
||||
|
||||
replaceInstruction(sizeCallIndex + 1, "const/4 v$sizeCallResultRegister, 0x0")
|
||||
|
||||
val arrayLengthConstIndex = indexOfFirstLiteralInstructionOrThrow(7)
|
||||
val arrayLengthConstDestination = getInstruction<OneRegisterInstruction>(arrayLengthConstIndex).registerA
|
||||
val playbackSpeedsArrayType = "$EXTENSION_CLASS_DESCRIPTOR->customPlaybackSpeeds:[F"
|
||||
|
||||
addInstructions(
|
||||
arrayLengthConstIndex + 1,
|
||||
"""
|
||||
sget-object v$arrayLengthConstDestination, $playbackSpeedsArrayType
|
||||
array-length v$arrayLengthConstDestination, v$arrayLengthConstDestination
|
||||
""",
|
||||
)
|
||||
|
||||
val originalArrayFetchIndex = indexOfFirstInstructionOrThrow {
|
||||
val reference = getReference<FieldReference>()
|
||||
reference?.type == "[F" && reference.definingClass.endsWith("/PlayerConfigModel;")
|
||||
}
|
||||
val originalArrayFetchDestination =
|
||||
getInstruction<OneRegisterInstruction>(originalArrayFetchIndex).registerA
|
||||
|
||||
replaceInstruction(
|
||||
originalArrayFetchIndex,
|
||||
"sget-object v$originalArrayFetchDestination, $playbackSpeedsArrayType",
|
||||
)
|
||||
}
|
||||
|
||||
// Override the min/max speeds that can be used.
|
||||
speedLimiterFingerprint.method.apply {
|
||||
val limitMinIndex = indexOfFirstLiteralInstructionOrThrow(0.25f)
|
||||
@@ -135,47 +78,7 @@ internal val customPlaybackSpeedPatch = bytecodePatch(
|
||||
replaceInstruction(limitMaxIndex, "const/high16 v$limitMaxRegister, 8.0f")
|
||||
}
|
||||
|
||||
// Add a static INSTANCE field to the class.
|
||||
// This is later used to call "showOldPlaybackSpeedMenu" on the instance.
|
||||
|
||||
val instanceField = ImmutableField(
|
||||
getOldPlaybackSpeedsFingerprint.originalClassDef.type,
|
||||
"INSTANCE",
|
||||
getOldPlaybackSpeedsFingerprint.originalClassDef.type,
|
||||
AccessFlags.PUBLIC.value or AccessFlags.STATIC.value,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
).toMutable()
|
||||
|
||||
getOldPlaybackSpeedsFingerprint.classDef.staticFields.add(instanceField)
|
||||
// Set the INSTANCE field to the instance of the class.
|
||||
// In order to prevent a conflict with another patch, add the instruction at index 1.
|
||||
getOldPlaybackSpeedsFingerprint.method.addInstruction(1, "sput-object p0, $instanceField")
|
||||
|
||||
// Get the "showOldPlaybackSpeedMenu" method.
|
||||
// This is later called on the field INSTANCE.
|
||||
val showOldPlaybackSpeedMenuMethod = showOldPlaybackSpeedMenuFingerprint.match(
|
||||
getOldPlaybackSpeedsFingerprint.classDef,
|
||||
).method.toString()
|
||||
|
||||
// Insert the call to the "showOldPlaybackSpeedMenu" method on the field INSTANCE.
|
||||
showOldPlaybackSpeedMenuExtensionFingerprint.method.apply {
|
||||
addInstructionsWithLabels(
|
||||
instructions.lastIndex,
|
||||
"""
|
||||
sget-object v0, $instanceField
|
||||
if-nez v0, :not_null
|
||||
return-void
|
||||
:not_null
|
||||
invoke-virtual { v0 }, $showOldPlaybackSpeedMenuMethod
|
||||
""",
|
||||
)
|
||||
}
|
||||
|
||||
// region Force old video quality menu.
|
||||
// This is necessary, because there is no known way of adding custom playback speeds to the new menu.
|
||||
|
||||
// Close the unpatched playback dialog and show the modern custom dialog.
|
||||
addRecyclerViewTreeHook(EXTENSION_CLASS_DESCRIPTOR)
|
||||
|
||||
// Required to check if the playback speed menu is currently shown.
|
||||
|
||||
@@ -1,30 +1,9 @@
|
||||
package app.revanced.patches.youtube.video.speed.custom
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.util.literal
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
|
||||
internal val getOldPlaybackSpeedsFingerprint = fingerprint {
|
||||
parameters("[L", "I")
|
||||
strings("menu_item_playback_speed")
|
||||
}
|
||||
|
||||
internal val showOldPlaybackSpeedMenuFingerprint = fingerprint {
|
||||
literal { speedUnavailableId }
|
||||
}
|
||||
|
||||
internal val showOldPlaybackSpeedMenuExtensionFingerprint = fingerprint {
|
||||
custom { method, _ -> method.name == "showOldPlaybackSpeedMenu" }
|
||||
}
|
||||
|
||||
internal val speedArrayGeneratorFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
|
||||
returns("[L")
|
||||
parameters("Lcom/google/android/libraries/youtube/innertube/model/player/PlayerResponseModel;")
|
||||
strings("0.0#")
|
||||
}
|
||||
|
||||
internal val speedLimiterFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
|
||||
returns("V")
|
||||
|
||||
@@ -10,6 +10,7 @@ import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableClass
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
|
||||
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
|
||||
import app.revanced.patcher.util.smali.ExternalLabel
|
||||
import app.revanced.patches.shared.misc.mapping.get
|
||||
@@ -31,6 +32,7 @@ import com.android.tools.smali.dexlib2.iface.instruction.ThreeRegisterInstructio
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.Reference
|
||||
import com.android.tools.smali.dexlib2.immutable.ImmutableField
|
||||
import com.android.tools.smali.dexlib2.util.MethodUtil
|
||||
import java.util.EnumSet
|
||||
|
||||
@@ -962,6 +964,43 @@ private fun MutableMethod.overrideReturnValue(value: String, returnLate: Boolean
|
||||
}
|
||||
}
|
||||
|
||||
internal fun BytecodePatchContext.addStaticFieldToExtension(
|
||||
className: String,
|
||||
methodName: String,
|
||||
fieldName: String,
|
||||
objectClass: String,
|
||||
smaliInstructions: String
|
||||
) {
|
||||
val classDef = classes.find { classDef -> classDef.type == className }
|
||||
?: throw PatchException("No matching methods found in: $className")
|
||||
val mutableClass = proxy(classDef).mutableClass
|
||||
|
||||
val objectCall = "$mutableClass->$fieldName:$objectClass"
|
||||
|
||||
mutableClass.apply {
|
||||
methods.first { method -> method.name == methodName }.apply {
|
||||
staticFields.add(
|
||||
ImmutableField(
|
||||
definingClass,
|
||||
fieldName,
|
||||
objectClass,
|
||||
AccessFlags.PUBLIC.value or AccessFlags.STATIC.value,
|
||||
null,
|
||||
annotations,
|
||||
null
|
||||
).toMutable()
|
||||
)
|
||||
|
||||
addInstructionsWithLabels(
|
||||
0,
|
||||
"""
|
||||
sget-object v0, $objectCall
|
||||
""" + smaliInstructions
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the custom condition for this fingerprint to check for a literal value.
|
||||
*
|
||||
|
||||
@@ -199,6 +199,8 @@ Second \"item\" text"</string>
|
||||
</patch>
|
||||
<patch id="misc.gms.gmsCoreSupportResourcePatch">
|
||||
</patch>
|
||||
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">
|
||||
</patch>
|
||||
<patch id="misc.gms.accountCredentialsInvalidTextPatch">
|
||||
</patch>
|
||||
<patch id="misc.links.bypassURLRedirectsPatch">
|
||||
@@ -207,8 +209,6 @@ Second \"item\" text"</string>
|
||||
</patch>
|
||||
<patch id="misc.privacy.removeTrackingQueryParameterPatch">
|
||||
</patch>
|
||||
<patch id="misc.zoomhaptics.zoomHapticsPatch">
|
||||
</patch>
|
||||
<patch id="video.audio.forceOriginalAudioPatch">
|
||||
<!-- 'Spoof video streams' should be the same translation used for revanced_spoof_video_streams_screen_title -->
|
||||
</patch>
|
||||
|
||||
@@ -199,6 +199,8 @@ Second \"item\" text"</string>
|
||||
</patch>
|
||||
<patch id="misc.gms.gmsCoreSupportResourcePatch">
|
||||
</patch>
|
||||
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">
|
||||
</patch>
|
||||
<patch id="misc.gms.accountCredentialsInvalidTextPatch">
|
||||
</patch>
|
||||
<patch id="misc.links.bypassURLRedirectsPatch">
|
||||
@@ -207,8 +209,6 @@ Second \"item\" text"</string>
|
||||
</patch>
|
||||
<patch id="misc.privacy.removeTrackingQueryParameterPatch">
|
||||
</patch>
|
||||
<patch id="misc.zoomhaptics.zoomHapticsPatch">
|
||||
</patch>
|
||||
<patch id="video.audio.forceOriginalAudioPatch">
|
||||
<!-- 'Spoof video streams' should be the same translation used for revanced_spoof_video_streams_screen_title -->
|
||||
</patch>
|
||||
|
||||
@@ -35,7 +35,7 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_settings_submenu_title">الإعدادات</string>
|
||||
<string name="revanced_settings_confirm_user_dialog_title">هل ترغب في المتابعة؟</string>
|
||||
<string name="revanced_settings_reset">إعادة التعيين</string>
|
||||
<string name="revanced_settings_reset_color">إعادة تعيين اللون</string>
|
||||
<string name="revanced_settings_reset_color">Reset color</string>
|
||||
<string name="revanced_settings_color_invalid">لون غير صالح</string>
|
||||
<string name="revanced_settings_restart_title">تحديث وإعادة التشغيل</string>
|
||||
<string name="revanced_settings_restart">إعادة التشغيل</string>
|
||||
@@ -117,6 +117,11 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_debug_protobuffer_title">سجل بروتوكول التخزين المؤقت</string>
|
||||
<string name="revanced_debug_protobuffer_summary_on">تتضمن سجلات التصحيح التخزين المؤقت</string>
|
||||
<string name="revanced_debug_protobuffer_summary_off">لا تتضمن سجلات التصحيح التخزين المؤقت</string>
|
||||
<string name="revanced_debug_protobuffer_user_dialog_message">"سيؤدي تمكين هذا الإعداد إلى تسجيل بيانات تخطيط إضافية، بما في ذلك النص المعروض على الشاشة لبعض مكونات واجهة المستخدم.
|
||||
|
||||
يمكن أن يساعد هذا في تحديد المكونات عند إنشاء عوامل تصفية مخصصة.
|
||||
|
||||
ومع ذلك، سيؤدي تمكين هذا أيضًا إلى تسجيل بعض بيانات المستخدم مثل عنوان IP الخاص بك."</string>
|
||||
<string name="revanced_debug_stacktrace_title">سجل تتبع المكدس</string>
|
||||
<string name="revanced_debug_stacktrace_summary_on">تتضمن سجلات التصحيح سجل تتبع المكدس</string>
|
||||
<string name="revanced_debug_stacktrace_summary_off">لا تتضمن سجلات التصحيح سجل تتبع المكدس</string>
|
||||
@@ -126,6 +131,15 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_debug_toast_on_error_user_dialog_message">"يؤدي إيقاف تشغيل ملاحظات الأخطاء إلى إخفاء كافة إشعارات أخطاء ReVanced.
|
||||
|
||||
لن يتم إعلامك بأي أخطاء غير متوقعة."</string>
|
||||
<string name="revanced_debug_export_logs_to_clipboard_title">تصدير سجلات تصحيح الأخطاء</string>
|
||||
<string name="revanced_debug_export_logs_to_clipboard_summary">نسخ سجلات تصحيح أخطاء ReVanced إلى الحافظة</string>
|
||||
<string name="revanced_debug_logs_disabled">تم تعطيل تسجيلات تصحيح الأخطاء</string>
|
||||
<string name="revanced_debug_logs_none_found">لم يتم العثور على سجلات</string>
|
||||
<string name="revanced_debug_logs_copied_to_clipboard">تم نسخ السجلات</string>
|
||||
<string name="revanced_debug_logs_failed_to_export">فشل تصدير السجلات: $s</string>
|
||||
<string name="revanced_debug_logs_clear_buffer_title">مسح سجلات تصحيح الأخطاء</string>
|
||||
<string name="revanced_debug_logs_clear_buffer_summary">يمسح جميع سجلات تصحيح أخطاء ReVanced المخزنة</string>
|
||||
<string name="revanced_debug_logs_clear_toast">تم مسح السجلات</string>
|
||||
</patch>
|
||||
<patch id="layout.hide.general.hideLayoutComponentsPatch">
|
||||
<string name="revanced_hide_album_cards_title">إخفاء بطاقات الألبوم</string>
|
||||
@@ -297,9 +311,9 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_hide_comments_create_a_short_button_title">إخفاء زر \'إنشاء Short\'</string>
|
||||
<string name="revanced_hide_comments_create_a_short_button_summary_on">تم إخفاء زر إنشاء Short</string>
|
||||
<string name="revanced_hide_comments_create_a_short_button_summary_off">يتم عرض زر إنشاء Short</string>
|
||||
<string name="revanced_hide_comments_timestamp_and_emoji_buttons_title">إخفاء أزرار الرموز التعبيرية والطوابع الزمنية</string>
|
||||
<string name="revanced_hide_comments_timestamp_and_emoji_buttons_summary_on">تم إخفاء أزرار الرموز التعبيرية والطوابع الزمنية</string>
|
||||
<string name="revanced_hide_comments_timestamp_and_emoji_buttons_summary_off">يتم عرض أزرار الرموز التعبيرية والطوابع الزمنية</string>
|
||||
<string name="revanced_hide_comments_timestamp_button_title">زر إخفاء الطابع الزمني</string>
|
||||
<string name="revanced_hide_comments_timestamp_button_summary_on">زر الطابع الزمني مخفي</string>
|
||||
<string name="revanced_hide_comments_timestamp_button_summary_off">زر الطابع الزمني معروض</string>
|
||||
<string name="revanced_hide_comments_preview_comment_title">إخفاء تعليق المعاينة</string>
|
||||
<string name="revanced_hide_comments_preview_comment_summary_on">تم إخفاء تعليق المعاينة</string>
|
||||
<string name="revanced_hide_comments_preview_comment_summary_off">يتم عرض تعليق المعاينة</string>
|
||||
@@ -477,9 +491,10 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_swipe_overlay_background_opacity_title">تعتيم خلفية واجهة التمرير السريع</string>
|
||||
<string name="revanced_swipe_overlay_background_opacity_summary">قيمة التعتيم بين 0-100</string>
|
||||
<string name="revanced_swipe_overlay_background_opacity_invalid_toast">يجب أن يكون تعتيم التمرير السريع بين 0-100</string>
|
||||
<string name="revanced_swipe_overlay_progress_color_title">لون شريط تقدم واجهة التمرير</string>
|
||||
<string name="revanced_swipe_overlay_progress_color_summary">لون شريط التقدم لعناصر التحكم في مستوى الصوت والسطوع</string>
|
||||
<string name="revanced_swipe_overlay_progress_color_invalid_toast">لون شريط التقدم غير صالح</string>
|
||||
<string name="revanced_swipe_overlay_progress_brightness_color_title">لون سطوع واجهة التمرير</string>
|
||||
<string name="revanced_swipe_overlay_progress_brightness_color_summary">لون شريط التقدم لعناصر التحكم في السطوع</string>
|
||||
<string name="revanced_swipe_overlay_progress_volume_color_title">لون مستوى صوت واجهة التمرير</string>
|
||||
<string name="revanced_swipe_overlay_progress_volume_color_summary">لون شريط التقدم لعناصر التحكم في مستوى الصوت</string>
|
||||
<string name="revanced_swipe_text_overlay_size_title">حجم نص واجهة التمرير</string>
|
||||
<string name="revanced_swipe_text_overlay_size_summary">حجم النص لواجهة التمرير بين 1-30</string>
|
||||
<string name="revanced_swipe_text_overlay_size_invalid_toast">يجب أن يكون حجم النص بين 1-30</string>
|
||||
@@ -666,6 +681,9 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_hide_autoplay_button_title">إخفاء زر التشغيل التلقائي</string>
|
||||
<string name="revanced_hide_autoplay_button_summary_on">تم إخفاء زر التشغيل التلقائي</string>
|
||||
<string name="revanced_hide_autoplay_button_summary_off">يتم عرض زر التشغيل التلقائي</string>
|
||||
<string name="revanced_hide_player_control_buttons_background_title">إخفاء خلفية أزرار التحكم في المشغل</string>
|
||||
<string name="revanced_hide_player_control_buttons_background_summary_on">تم إخفاء خلفية أزرار التحكم في المشغل</string>
|
||||
<string name="revanced_hide_player_control_buttons_background_summary_off">تم إظهار خلفية أزرار التحكم في المشغل</string>
|
||||
</patch>
|
||||
<patch id="layout.hide.endscreencards.hideEndscreenCardsResourcePatch">
|
||||
<string name="revanced_hide_endscreen_cards_title">إخفاء بطاقات شاشة النهاية</string>
|
||||
@@ -735,6 +753,9 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_hide_shorts_location_label_title">إخفاء تسمية الموقع</string>
|
||||
<string name="revanced_hide_shorts_location_label_summary_on">تم إخفاء تسمية الموقع</string>
|
||||
<string name="revanced_hide_shorts_location_label_summary_off">يتم عرض تسمية الموقع</string>
|
||||
<string name="revanced_hide_shorts_preview_comment_title">إخفاء تعليق المعاينة</string>
|
||||
<string name="revanced_hide_shorts_preview_comment_summary_on">تم إخفاء تعليق المعاينة</string>
|
||||
<string name="revanced_hide_shorts_preview_comment_summary_off">يتم عرض تعليق المعاينة</string>
|
||||
<string name="revanced_hide_shorts_save_sound_button_title">إخفاء زر حفظ الموسيقى</string>
|
||||
<string name="revanced_hide_shorts_save_sound_button_summary_on">تم إخفاء زر حفظ الموسيقى</string>
|
||||
<string name="revanced_hide_shorts_save_sound_button_summary_off">يتم عرض زر حفظ الموسيقى</string>
|
||||
@@ -747,6 +768,9 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_hide_shorts_green_screen_button_title">إخفاء زر الشاشة الخضراء</string>
|
||||
<string name="revanced_hide_shorts_green_screen_button_summary_on">تم إخفاء زر الشاشة الخضراء</string>
|
||||
<string name="revanced_hide_shorts_green_screen_button_summary_off">يتم عرض زر الشاشة الخضراء</string>
|
||||
<string name="revanced_hide_shorts_new_posts_button_title">إخفاء زر \"مشاركات جديدة\"</string>
|
||||
<string name="revanced_hide_shorts_new_posts_button_summary_off">يتم عرض زر \"مشاركات جديدة\"</string>
|
||||
<string name="revanced_hide_shorts_new_posts_button_summary_on">تم إخفاء زر \"مشاركات جديدة\"</string>
|
||||
<string name="revanced_hide_shorts_hashtag_button_title">إخفاء زر الهاشتاج</string>
|
||||
<string name="revanced_hide_shorts_hashtag_button_summary_on">تم إخفاء زر الهاشتاج</string>
|
||||
<string name="revanced_hide_shorts_hashtag_button_summary_off">يتم عرض زر الهاشتاج</string>
|
||||
@@ -1238,6 +1262,9 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_gradient_loading_screen_title">تمكين شاشة التحميل المتدرجة</string>
|
||||
<string name="revanced_gradient_loading_screen_summary_on">ستحتوي شاشة التحميل على خلفية متدرجة</string>
|
||||
<string name="revanced_gradient_loading_screen_summary_off">ستحتوي شاشة التحميل على خلفية ثابتة</string>
|
||||
<string name="splash_screen_animation_style_title">نمط الشاشة الترحيبية</string>
|
||||
<string name="splash_screen_animation_style_entry_1">اللون</string>
|
||||
<string name="splash_screen_animation_style_entry_2">أبيض وأسود</string>
|
||||
<string name="revanced_seekbar_custom_color_title">تمكين لون شريط تقدم الفيديو المخصص</string>
|
||||
<string name="revanced_seekbar_custom_color_summary_on">يتم عرض لون شريط تقدم الفيديو المخصص</string>
|
||||
<string name="revanced_seekbar_custom_color_summary_off">يتم عرض لون شريط تقدم الفيديو الاصلي</string>
|
||||
@@ -1322,6 +1349,22 @@ Second \"item\" text"</string>
|
||||
<string name="microg_settings_title">إعدادات GmsCore</string>
|
||||
<string name="microg_settings_summary">إعدادات لـ GmsCore</string>
|
||||
</patch>
|
||||
<patch id="misc.hapticfeedback.disableHapticFeedbackPatch">
|
||||
<string name="revanced_disable_haptic_feedback_title">الاهتزاز عند الضغط</string>
|
||||
<string name="revanced_disable_haptic_feedback_summary">تغيير الاهتزاز عند الضغط</string>
|
||||
<string name="revanced_disable_haptic_feedback_chapters_title">تعطيل الاهتزاز للفصول</string>
|
||||
<string name="revanced_disable_haptic_feedback_chapters_summary_on">تم تعطيل الاهتزاز للفصول</string>
|
||||
<string name="revanced_disable_haptic_feedback_chapters_summary_off">تم تفعيل الاهتزاز للفصول</string>
|
||||
<string name="revanced_disable_haptic_feedback_precise_seeking_title">تعطيل الاهتزاز عند التمرير الدقيق</string>
|
||||
<string name="revanced_disable_haptic_feedback_precise_seeking_summary_on">تم تعطيل الاهتزاز الدقيق عند البحث</string>
|
||||
<string name="revanced_disable_haptic_feedback_precise_seeking_summary_off">تم تفعيل الاهتزاز عند التمرير الدقيق</string>
|
||||
<string name="revanced_disable_haptic_feedback_seek_undo_title">تعطيل الاهتزاز عند التراجع عن البحث</string>
|
||||
<string name="revanced_disable_haptic_feedback_seek_undo_summary_on">تم تعطيل الاهتزاز عند التراجع عن البحث</string>
|
||||
<string name="revanced_disable_haptic_feedback_seek_undo_summary_off">تم تمكين الاهتزاز عند التراجع عن البحث</string>
|
||||
<string name="revanced_disable_haptic_feedback_zoom_title">تعطيل الاهتزاز عند التكبير</string>
|
||||
<string name="revanced_disable_haptic_feedback_zoom_summary_on">تم تعطيل الاهتزاز عند التكبير</string>
|
||||
<string name="revanced_disable_haptic_feedback_zoom_summary_off">تم تمكين الاهتزاز عند التكبير</string>
|
||||
</patch>
|
||||
<patch id="misc.gms.accountCredentialsInvalidTextPatch">
|
||||
<string name="microg_offline_account_login_error">إذا قمت مؤخرًا بتغيير تفاصيل تسجيل الدخول إلى حسابك، فأزل تثبيت MicroG ثم أعد تثبيته.</string>
|
||||
</patch>
|
||||
@@ -1340,11 +1383,6 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_remove_tracking_query_parameter_summary_on">يتم إزالة معلمة استعلام التتبع من الروابط</string>
|
||||
<string name="revanced_remove_tracking_query_parameter_summary_off">لا يتم إزالة معلمة استعلام التتبع من الروابط</string>
|
||||
</patch>
|
||||
<patch id="misc.zoomhaptics.zoomHapticsPatch">
|
||||
<string name="revanced_disable_zoom_haptics_title">تعطيل الاهتزاز عند التكبير</string>
|
||||
<string name="revanced_disable_zoom_haptics_summary_on">تم تعطيل الاهتزاز</string>
|
||||
<string name="revanced_disable_zoom_haptics_summary_off">تم تمكين الاهتزاز</string>
|
||||
</patch>
|
||||
<patch id="video.audio.forceOriginalAudioPatch">
|
||||
<string name="revanced_force_original_audio_title">فرض لغة الصوت الأصلية</string>
|
||||
<string name="revanced_force_original_audio_summary_on">استخدام لغة الصوت الأصلية</string>
|
||||
@@ -1372,7 +1410,7 @@ Second \"item\" text"</string>
|
||||
</patch>
|
||||
<patch id="video.speed.button.playbackSpeedButtonPatch">
|
||||
<string name="revanced_playback_speed_dialog_button_title">عرض زر مربع حوار السرعة</string>
|
||||
<string name="revanced_playback_speed_dialog_button_summary_on">يتم عرض الزر</string>
|
||||
<string name="revanced_playback_speed_dialog_button_summary_on">الزر معروض. انقر مع الاستمرار لإعادة ضبط سرعة التشغيل إلى الوضع الافتراضي</string>
|
||||
<string name="revanced_playback_speed_dialog_button_summary_off">لا يتم عرض الزر</string>
|
||||
</patch>
|
||||
<patch id="video.speed.custom.customPlaybackSpeedPatch">
|
||||
@@ -1384,6 +1422,7 @@ Second \"item\" text"</string>
|
||||
<string name="revanced_custom_playback_speeds_invalid">يجب أن تكون سرعات التشغيل المخصصة أقل من %s</string>
|
||||
<string name="revanced_custom_playback_speeds_parse_exception">سرعة التشغيل المخصصة غير صالحة</string>
|
||||
<string name="revanced_custom_playback_speeds_auto">تلقائي</string>
|
||||
<string name="revanced_custom_playback_speeds_reset_toast">تمت إعادة ضبط سرعة التشغيل إلى: %s</string>
|
||||
<string name="revanced_speed_tap_and_hold_title">سرعة النقر مع الاستمرار المخصصة</string>
|
||||
<string name="revanced_speed_tap_and_hold_summary">سرعة التشغيل بين 0-8</string>
|
||||
</patch>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user