mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-10 19:33:55 +01:00
Compare commits
91 Commits
v5.26.0-de
...
v5.29.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e48c152b95 | ||
|
|
a678f178e1 | ||
|
|
2d8f5641f9 | ||
|
|
0dbd058099 | ||
|
|
c1a8fd0766 | ||
|
|
d338989cb4 | ||
|
|
b94daacf01 | ||
|
|
c764c4f197 | ||
|
|
6b719dfcd7 | ||
|
|
ccd169121a | ||
|
|
dcfbd8bf93 | ||
|
|
b65697603d | ||
|
|
25da5cca8b | ||
|
|
2b62fc2224 | ||
|
|
a9e9456b6b | ||
|
|
b01523e97d | ||
|
|
b8afb4e821 | ||
|
|
0d2198faed | ||
|
|
5c7c407b82 | ||
|
|
a8d2a1e028 | ||
|
|
d31624cae8 | ||
|
|
e790cfbf59 | ||
|
|
a54d408d3e | ||
|
|
5d3769e921 | ||
|
|
e138501657 | ||
|
|
a9235d6b62 | ||
|
|
90868ff025 | ||
|
|
345ec5c430 | ||
|
|
c8b95d475c | ||
|
|
0a93f44a5e | ||
|
|
a426e2af50 | ||
|
|
9e30c34e74 | ||
|
|
55401368b8 | ||
|
|
c0c56fef23 | ||
|
|
69df47602f | ||
|
|
1cea6bfdff | ||
|
|
e2a9552f91 | ||
|
|
7bbaca77ad | ||
|
|
246f3efe55 | ||
|
|
0fca3e8fb1 | ||
|
|
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 |
294
CHANGELOG.md
294
CHANGELOG.md
@@ -1,3 +1,297 @@
|
||||
# [5.29.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.3...v5.29.0-dev.4) (2025-06-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide Shorts components:** Fix "Hide Use this sound button" ([#5233](https://github.com/ReVanced/revanced-patches/issues/5233)) ([5d6ec9e](https://github.com/ReVanced/revanced-patches/commit/5d6ec9e94a6221a0f32762d5bede893e9e7457fc))
|
||||
|
||||
# [5.29.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.2...v5.29.0-dev.3) (2025-06-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube:** Fix refactoring app startup exception ([1b00c90](https://github.com/ReVanced/revanced-patches/commit/1b00c907f4b90f4659afb4a54ba61ac2835b460d))
|
||||
|
||||
# [5.29.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.29.0-dev.1...v5.29.0-dev.2) (2025-06-23)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Crunchyroll:** Add `Hide ads` patch ([#5201](https://github.com/ReVanced/revanced-patches/issues/5201)) ([46b4398](https://github.com/ReVanced/revanced-patches/commit/46b4398fd6ca223391ed8f497a8347c2313421b7))
|
||||
|
||||
# [5.29.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.28.1-dev.2...v5.29.0-dev.1) (2025-06-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube:** Always use single threaded layout to resolve layout bugs in unpatched YouTube ([#5226](https://github.com/ReVanced/revanced-patches/issues/5226)) ([1f539b1](https://github.com/ReVanced/revanced-patches/commit/1f539b1396526b2c767d77a804bd0308ee4a42ec))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **YouTube:** Add an option to disable toasts when changing default playback speed or quality ([#5230](https://github.com/ReVanced/revanced-patches/issues/5230)) ([c68cde3](https://github.com/ReVanced/revanced-patches/commit/c68cde3a896450874cc571be5c4723387db96032))
|
||||
|
||||
## [5.28.1-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.1-dev.1...v5.28.1-dev.2) (2025-06-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube - Hide ads:** Hide new type of product ad in video description ([#5225](https://github.com/ReVanced/revanced-patches/issues/5225)) ([1e2efad](https://github.com/ReVanced/revanced-patches/commit/1e2efad7b2714c395ed6b0a77cbbf8a2265df520))
|
||||
|
||||
## [5.28.1-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.28.0...v5.28.1-dev.1) (2025-06-22)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Add scrollable content to modern style settings dialogs ([#5211](https://github.com/ReVanced/revanced-patches/issues/5211)) ([e6876d5](https://github.com/ReVanced/revanced-patches/commit/e6876d510d28f6a3a41ec1722a033b3e30a22c65))
|
||||
|
||||
# [5.28.0](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0) (2025-06-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Google Photos:** Resolve startup crash if MicroG GmsCore does not already have granted permissions ([a93d74d](https://github.com/ReVanced/revanced-patches/commit/a93d74d26e7ef87a3745df2b9fe82722d65a0e59))
|
||||
* **Messenger - Remove Meta AI:** Improve patch logic ([#5153](https://github.com/ReVanced/revanced-patches/issues/5153)) ([4ad4887](https://github.com/ReVanced/revanced-patches/commit/4ad488744d87543c31e453dc7b6d8182b3a7f440))
|
||||
* **Pandora - Disable ads:** Support latest app target ([#5185](https://github.com/ReVanced/revanced-patches/issues/5185)) ([ca83047](https://github.com/ReVanced/revanced-patches/commit/ca83047f5c4acbb267d5b98db80ad111999086e0))
|
||||
* **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([e7dd061](https://github.com/ReVanced/revanced-patches/commit/e7dd061c513af90861c0ab0d7adc6ee43be57ce2))
|
||||
* **Threads - Hide ads:** Constrain patch to the last working app target ([#5189](https://github.com/ReVanced/revanced-patches/issues/5189)) ([3558c44](https://github.com/ReVanced/revanced-patches/commit/3558c44a05c13f19fefdbbf14b364181a79f17c0))
|
||||
* **YouTube:** Remove old app targets that are no longer supported by YouTube ([#5192](https://github.com/ReVanced/revanced-patches/issues/5192)) ([c9e54e1](https://github.com/ReVanced/revanced-patches/commit/c9e54e1d36243945ac1ec3108fe38edf0e15d772))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([8736b6a](https://github.com/ReVanced/revanced-patches/commit/8736b6a80b48cb1f4562c9f9919804006ddb18bd))
|
||||
* Use modern style settings dialogs ([#5109](https://github.com/ReVanced/revanced-patches/issues/5109)) ([312b6dc](https://github.com/ReVanced/revanced-patches/commit/312b6dc04e01c2758cd304ca8606306027aa2f01))
|
||||
|
||||
# [5.28.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.7...v5.28.0-dev.8) (2025-06-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Messenger - Remove Meta AI:** Improve patch logic ([#5153](https://github.com/ReVanced/revanced-patches/issues/5153)) ([4ad4887](https://github.com/ReVanced/revanced-patches/commit/4ad488744d87543c31e453dc7b6d8182b3a7f440))
|
||||
|
||||
# [5.28.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.6...v5.28.0-dev.7) (2025-06-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **YouTube:** Remove old app targets that are no longer supported by YouTube ([#5192](https://github.com/ReVanced/revanced-patches/issues/5192)) ([c9e54e1](https://github.com/ReVanced/revanced-patches/commit/c9e54e1d36243945ac1ec3108fe38edf0e15d772))
|
||||
|
||||
# [5.28.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.5...v5.28.0-dev.6) (2025-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Threads - Hide ads:** Constrain patch to the last working app target ([#5189](https://github.com/ReVanced/revanced-patches/issues/5189)) ([3558c44](https://github.com/ReVanced/revanced-patches/commit/3558c44a05c13f19fefdbbf14b364181a79f17c0))
|
||||
|
||||
# [5.28.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.4...v5.28.0-dev.5) (2025-06-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Pandora - Disable ads:** Support latest app target ([#5185](https://github.com/ReVanced/revanced-patches/issues/5185)) ([ca83047](https://github.com/ReVanced/revanced-patches/commit/ca83047f5c4acbb267d5b98db80ad111999086e0))
|
||||
|
||||
# [5.28.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.3...v5.28.0-dev.4) (2025-06-13)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Use modern style settings dialogs ([#5109](https://github.com/ReVanced/revanced-patches/issues/5109)) ([312b6dc](https://github.com/ReVanced/revanced-patches/commit/312b6dc04e01c2758cd304ca8606306027aa2f01))
|
||||
|
||||
# [5.28.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.2...v5.28.0-dev.3) (2025-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([e7dd061](https://github.com/ReVanced/revanced-patches/commit/e7dd061c513af90861c0ab0d7adc6ee43be57ce2))
|
||||
|
||||
# [5.28.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.28.0-dev.1...v5.28.0-dev.2) (2025-06-11)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Google Photos:** Resolve startup crash if MicroG GmsCore does not already have granted permissions ([a93d74d](https://github.com/ReVanced/revanced-patches/commit/a93d74d26e7ef87a3745df2b9fe82722d65a0e59))
|
||||
|
||||
# [5.28.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.27.0...v5.28.0-dev.1) (2025-06-11)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([8736b6a](https://github.com/ReVanced/revanced-patches/commit/8736b6a80b48cb1f4562c9f9919804006ddb18bd))
|
||||
|
||||
# [5.27.0](https://github.com/ReVanced/revanced-patches/compare/v5.26.0...v5.27.0) (2025-06-09)
|
||||
|
||||
|
||||
### 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))
|
||||
* **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))
|
||||
* **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))
|
||||
* **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))
|
||||
* **YouTube - Hide Shorts components:** Disable A/B player flags that prevents hiding buttons ([bef0dac](https://github.com/ReVanced/revanced-patches/commit/bef0dacac54caf1ca9511d7bc19b19140ccb4eaf))
|
||||
* **YouTube - Video quality:** Remove non-functional Shorts 144p default quality ([3113cd6](https://github.com/ReVanced/revanced-patches/commit/3113cd6d092952c8657454452f34c0ae85358ec9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add `Hide app icon` patch ([#4977](https://github.com/ReVanced/revanced-patches/issues/4977)) ([92311b8](https://github.com/ReVanced/revanced-patches/commit/92311b8e5675f3d4b80ed690d34b699fb847e3cd))
|
||||
* **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))
|
||||
* **Messenger:** Add `Hide Facebook button` patch ([#5057](https://github.com/ReVanced/revanced-patches/issues/5057)) ([9175b23](https://github.com/ReVanced/revanced-patches/commit/9175b23e8360d13c8c1c9c8602ca0b5931d13627))
|
||||
* **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))
|
||||
* **YouTube - Hide Shorts components:** Add hide 'New posts' button ([ac6b916](https://github.com/ReVanced/revanced-patches/commit/ac6b916c0c212167c4645e2110500dc811b3e54a))
|
||||
* **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.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)
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
package app.revanced.extension.messenger.metaai;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
|
||||
@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"))
|
||||
return false;
|
||||
private static final Set<Long> loggedIDs = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
public static boolean overrideBooleanFlag(long id, boolean value) {
|
||||
try {
|
||||
if (Long.toString(id).startsWith("REPLACED_BY_PATCH")) {
|
||||
if (loggedIDs.add(id))
|
||||
Logger.printInfo(() -> "Overriding " + id + " from " + value + " to false");
|
||||
|
||||
return false;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "overrideBooleanFlag failure", ex);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.app.SearchManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
@@ -15,6 +15,8 @@ import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.util.Pair;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
@@ -26,6 +28,7 @@ import java.util.Locale;
|
||||
|
||||
import app.revanced.extension.shared.requests.Requester;
|
||||
import app.revanced.extension.shared.requests.Route;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class GmsCoreSupport {
|
||||
@@ -78,13 +81,27 @@ public class GmsCoreSupport {
|
||||
// Use a delay to allow the activity to finish initializing.
|
||||
// Otherwise, if device is in dark mode the dialog is shown with wrong color scheme.
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
str("gms_core_dialog_title"), // Title.
|
||||
str(dialogMessageRef), // Message.
|
||||
null, // No EditText.
|
||||
str(positiveButtonTextRef), // OK button text.
|
||||
() -> onPositiveClickListener.onClick(null, 0), // Convert DialogInterface.OnClickListener to Runnable.
|
||||
null, // No Cancel button action.
|
||||
null, // No Neutral button text.
|
||||
null, // No Neutral button action.
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
Dialog dialog = dialogPair.first;
|
||||
|
||||
// Do not set cancelable to false, to allow using back button to skip the action,
|
||||
// just in case the battery change can never be satisfied.
|
||||
var dialog = new AlertDialog.Builder(context)
|
||||
.setTitle(str("gms_core_dialog_title"))
|
||||
.setMessage(str(dialogMessageRef))
|
||||
.setPositiveButton(str(positiveButtonTextRef), onPositiveClickListener)
|
||||
.create();
|
||||
dialog.setCancelable(true);
|
||||
|
||||
// Show the dialog
|
||||
Utils.showDialog(context, dialog);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ 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.
|
||||
* All methods are thread safe, and are safe to call even
|
||||
* if {@link Utils#getContext()} is not available.
|
||||
*/
|
||||
public class Logger {
|
||||
|
||||
@@ -138,6 +139,20 @@ public class Logger {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean shouldLogDebug() {
|
||||
// If the app is still starting up and the context is not yet set,
|
||||
// then allow debug logging regardless what the debug setting actually is.
|
||||
return Utils.context == null || DEBUG.get();
|
||||
}
|
||||
|
||||
private static boolean shouldShowErrorToast() {
|
||||
return Utils.context != null && DEBUG_TOAST_ON_ERROR.get();
|
||||
}
|
||||
|
||||
private static boolean includeStackTrace() {
|
||||
return Utils.context != null && DEBUG_STACKTRACE.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs debug messages under the outer class name of the code calling this method.
|
||||
* <p>
|
||||
@@ -157,8 +172,8 @@ public class Logger {
|
||||
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||
*/
|
||||
public static void printDebug(LogMessage message, @Nullable Exception ex) {
|
||||
if (DEBUG.get()) {
|
||||
logInternal(LogLevel.DEBUG, message, ex, DEBUG_STACKTRACE.get(), false);
|
||||
if (shouldLogDebug()) {
|
||||
logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +188,7 @@ public class Logger {
|
||||
* Logs information messages using the outer class name of the code calling this method.
|
||||
*/
|
||||
public static void printInfo(LogMessage message, @Nullable Exception ex) {
|
||||
logInternal(LogLevel.INFO, message, ex, DEBUG_STACKTRACE.get(), false);
|
||||
logInternal(LogLevel.INFO, message, ex, includeStackTrace(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,22 +209,6 @@ public class Logger {
|
||||
* @param ex exception (optional)
|
||||
*/
|
||||
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(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(LogMessage message, @Nullable Exception ex) {
|
||||
logInternal(LogLevel.ERROR, message, ex, false, false);
|
||||
logInternal(LogLevel.ERROR, message, ex, includeStackTrace(), shouldShowErrorToast());
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,15 +3,21 @@ package app.revanced.extension.shared.checks;
|
||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.app.Dialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.net.Uri;
|
||||
import android.text.Html;
|
||||
import android.util.Pair;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -86,38 +92,58 @@ abstract class Check {
|
||||
);
|
||||
|
||||
Utils.runOnMainThreadDelayed(() -> {
|
||||
AlertDialog alert = new AlertDialog.Builder(activity)
|
||||
.setCancelable(false)
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setTitle(str("revanced_check_environment_failed_title"))
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
" ",
|
||||
(dialog, which) -> {
|
||||
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
activity.startActivity(intent);
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
activity,
|
||||
str("revanced_check_environment_failed_title"), // Title.
|
||||
message, // Message.
|
||||
null, // No EditText.
|
||||
str("revanced_check_environment_dialog_open_official_source_button"), // OK button text.
|
||||
() -> {
|
||||
// Action for the OK (website) button.
|
||||
final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
activity.startActivity(intent);
|
||||
|
||||
// Shutdown to prevent the user from navigating back to this app,
|
||||
// which is no longer showing a warning dialog.
|
||||
activity.finishAffinity();
|
||||
System.exit(0);
|
||||
}
|
||||
).setNegativeButton(
|
||||
" ",
|
||||
(dialog, which) -> {
|
||||
// Cleanup data if the user incorrectly imported a huge negative number.
|
||||
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
||||
// Shutdown to prevent the user from navigating back to this app,
|
||||
// which is no longer showing a warning dialog.
|
||||
activity.finishAffinity();
|
||||
System.exit(0);
|
||||
},
|
||||
null, // No cancel button.
|
||||
str("revanced_check_environment_dialog_ignore_button"), // Neutral button text.
|
||||
() -> {
|
||||
// Neutral button action.
|
||||
// Cleanup data if the user incorrectly imported a huge negative number.
|
||||
final int current = Math.max(0, BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get());
|
||||
BaseSettings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1);
|
||||
},
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
dialog.dismiss();
|
||||
}
|
||||
).create();
|
||||
// Get the dialog and main layout.
|
||||
Dialog dialog = dialogPair.first;
|
||||
LinearLayout mainLayout = dialogPair.second;
|
||||
|
||||
Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() {
|
||||
// Add icon to the dialog.
|
||||
ImageView iconView = new ImageView(activity);
|
||||
iconView.setImageResource(Utils.getResourceIdentifier("revanced_ic_dialog_alert", "drawable"));
|
||||
iconView.setColorFilter(Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN);
|
||||
iconView.setPadding(0, 0, 0, 0);
|
||||
LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
iconParams.gravity = Gravity.CENTER;
|
||||
mainLayout.addView(iconView, 0); // Add icon at the top.
|
||||
|
||||
dialog.setCancelable(false);
|
||||
|
||||
// Show the dialog.
|
||||
Utils.showDialog(activity, dialog, false, new DialogFragmentOnStartAction() {
|
||||
boolean hasRun;
|
||||
@Override
|
||||
public void onStart(AlertDialog dialog) {
|
||||
public void onStart(Dialog dialog) {
|
||||
// Only run this once, otherwise if the user changes to a different app
|
||||
// then changes back, this handler will run again and disable the buttons.
|
||||
if (hasRun) {
|
||||
@@ -125,19 +151,43 @@ abstract class Check {
|
||||
}
|
||||
hasRun = true;
|
||||
|
||||
var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
||||
// Get the button container to access buttons.
|
||||
LinearLayout buttonContainer = (LinearLayout) mainLayout.getChildAt(mainLayout.getChildCount() - 1);
|
||||
|
||||
Button openWebsiteButton;
|
||||
Button ignoreButton;
|
||||
|
||||
// Check if buttons are in a single-row layout (buttonContainer has one child: rowContainer).
|
||||
if (buttonContainer.getChildCount() == 1 && buttonContainer.getChildAt(0) instanceof LinearLayout) {
|
||||
LinearLayout rowContainer = (LinearLayout) buttonContainer.getChildAt(0);
|
||||
// Neutral button is the first child (index 0).
|
||||
ignoreButton = (Button) rowContainer.getChildAt(0);
|
||||
// OK button is the last child.
|
||||
openWebsiteButton = (Button) rowContainer.getChildAt(rowContainer.getChildCount() - 1);
|
||||
} else {
|
||||
// Multi-row layout: buttons are in separate containers, ordered OK, Cancel, Neutral.
|
||||
LinearLayout okContainer =
|
||||
(LinearLayout) buttonContainer.getChildAt(0); // OK is first.
|
||||
openWebsiteButton = (Button) okContainer.getChildAt(0);
|
||||
LinearLayout neutralContainer =
|
||||
(LinearLayout)buttonContainer.getChildAt(buttonContainer.getChildCount() - 1); // Neutral is last.
|
||||
ignoreButton = (Button) neutralContainer.getChildAt(0);
|
||||
}
|
||||
|
||||
// Initially set buttons to INVISIBLE and disabled.
|
||||
openWebsiteButton.setVisibility(View.INVISIBLE);
|
||||
openWebsiteButton.setEnabled(false);
|
||||
ignoreButton.setVisibility(View.INVISIBLE);
|
||||
ignoreButton.setEnabled(false);
|
||||
|
||||
var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
|
||||
dismissButton.setEnabled(false);
|
||||
|
||||
getCountdownRunnable(dismissButton, openWebsiteButton).run();
|
||||
// Start the countdown for showing and enabling buttons.
|
||||
getCountdownRunnable(ignoreButton, openWebsiteButton).run();
|
||||
}
|
||||
});
|
||||
}, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs.
|
||||
}
|
||||
|
||||
private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) {
|
||||
private static Runnable getCountdownRunnable(Button ignoreButton, Button openWebsiteButton) {
|
||||
return new Runnable() {
|
||||
private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON;
|
||||
|
||||
@@ -146,17 +196,15 @@ abstract class Check {
|
||||
Utils.verifyOnMainThread();
|
||||
|
||||
if (secondsRemaining > 0) {
|
||||
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) {
|
||||
openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button"));
|
||||
if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON <= 0) {
|
||||
openWebsiteButton.setVisibility(View.VISIBLE);
|
||||
openWebsiteButton.setEnabled(true);
|
||||
}
|
||||
|
||||
secondsRemaining--;
|
||||
|
||||
Utils.runOnMainThreadDelayed(this, 1000);
|
||||
} else {
|
||||
dismissButton.setText(str("revanced_check_environment_dialog_ignore_button"));
|
||||
dismissButton.setEnabled(true);
|
||||
ignoreButton.setVisibility(View.VISIBLE);
|
||||
ignoreButton.setEnabled(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -52,7 +52,7 @@ public class Route {
|
||||
|
||||
private int countMatches(CharSequence seq, char c) {
|
||||
int count = 0;
|
||||
for (int i = 0; i < seq.length(); i++) {
|
||||
for (int i = 0, length = seq.length(); i < length; i++) {
|
||||
if (seq.charAt(i) == c)
|
||||
count++;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,20 @@ package app.revanced.extension.shared.settings.preference;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.preference.*;
|
||||
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceFragment;
|
||||
import android.preference.PreferenceGroup;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.preference.PreferenceScreen;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.ListPreference;
|
||||
import android.util.Pair;
|
||||
import android.widget.LinearLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -44,7 +52,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
* Set by subclasses if Strings cannot be added as a resource.
|
||||
*/
|
||||
@Nullable
|
||||
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle;
|
||||
protected static String restartDialogButtonText, restartDialogTitle, confirmDialogTitle, restartDialogMessage;
|
||||
|
||||
private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> {
|
||||
try {
|
||||
@@ -76,7 +84,7 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
updatingPreference = true;
|
||||
// Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'.
|
||||
// Updating here can can cause a recursive call back into this same method.
|
||||
// Updating here can cause a recursive call back into this same method.
|
||||
updatePreference(pref, setting, true, settingImportInProgress);
|
||||
// Update any other preference availability that may now be different.
|
||||
updateUIAvailability();
|
||||
@@ -116,11 +124,14 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
|
||||
showingUserDialogMessage = true;
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(confirmDialogTitle)
|
||||
.setMessage(Objects.requireNonNull(setting.userDialogMessage).toString())
|
||||
.setPositiveButton(android.R.string.ok, (dialog, id) -> {
|
||||
// User confirmed, save to the Setting.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
confirmDialogTitle, // Title.
|
||||
Objects.requireNonNull(setting.userDialogMessage).toString(), // No message.
|
||||
null, // No EditText.
|
||||
null, // OK button text.
|
||||
() -> {
|
||||
// OK button action. User confirmed, save to the Setting.
|
||||
updatePreference(pref, setting, true, false);
|
||||
|
||||
// Update availability of other preferences that may be changed.
|
||||
@@ -129,23 +140,27 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
if (setting.rebootApp) {
|
||||
showRestartDialog(context);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialog, id) -> {
|
||||
// Restore whatever the setting was before the change.
|
||||
},
|
||||
() -> {
|
||||
// Cancel button action. Restore whatever the setting was before the change.
|
||||
updatePreference(pref, setting, true, true);
|
||||
})
|
||||
.setOnDismissListener(dialog -> {
|
||||
showingUserDialogMessage = false;
|
||||
})
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
},
|
||||
null, // No Neutral button.
|
||||
null, // No Neutral button action.
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
dialogPair.first.setOnDismissListener(d -> showingUserDialogMessage = false);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all Preferences values and their availability using the current values in {@link Setting}.
|
||||
*/
|
||||
protected void updateUIToSettingValues() {
|
||||
updatePreferenceScreen(getPreferenceScreen(), true,true);
|
||||
updatePreferenceScreen(getPreferenceScreen(), true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -280,17 +295,27 @@ public abstract class AbstractPreferenceFragment extends PreferenceFragment {
|
||||
if (restartDialogTitle == null) {
|
||||
restartDialogTitle = str("revanced_settings_restart_title");
|
||||
}
|
||||
if (restartDialogMessage == null) {
|
||||
restartDialogMessage = str("revanced_settings_restart_dialog_message");
|
||||
}
|
||||
if (restartDialogButtonText == null) {
|
||||
restartDialogButtonText = str("revanced_settings_restart");
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setMessage(restartDialogTitle)
|
||||
.setPositiveButton(restartDialogButtonText, (dialog, id)
|
||||
-> Utils.restartApp(context))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(context,
|
||||
restartDialogTitle, // Title.
|
||||
restartDialogMessage, // Message.
|
||||
null, // No EditText.
|
||||
restartDialogButtonText, // OK button text.
|
||||
() -> Utils.restartApp(context), // OK button action.
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
null, // No Neutral button text.
|
||||
null, // No Neutral button action.
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
}
|
||||
|
||||
@SuppressLint("ResourceType")
|
||||
|
||||
@@ -2,8 +2,9 @@ package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
@@ -18,14 +19,12 @@ import android.text.TextWatcher;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.*;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
@@ -182,7 +181,7 @@ public class ColorPickerPreference extends EditTextPreference {
|
||||
* @throws IllegalArgumentException If the color string is invalid.
|
||||
*/
|
||||
@Override
|
||||
public final void setText(String colorString) {
|
||||
public final void setText(String colorString) {
|
||||
try {
|
||||
Logger.printDebug(() -> "setText: " + colorString);
|
||||
super.setText(colorString);
|
||||
@@ -216,86 +215,6 @@ public class ColorPickerPreference extends EditTextPreference {
|
||||
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a layout with a color preview and EditText for hex color input.
|
||||
*
|
||||
* @param context The context for creating the layout.
|
||||
* @return A LinearLayout containing the color preview and EditText.
|
||||
*/
|
||||
private LinearLayout createDialogLayout(Context context) {
|
||||
LinearLayout layout = new LinearLayout(context);
|
||||
layout.setOrientation(LinearLayout.VERTICAL);
|
||||
layout.setPadding(70, 0, 70, 0);
|
||||
|
||||
// Inflate color picker.
|
||||
View colorPicker = LayoutInflater.from(context).inflate(
|
||||
getResourceIdentifier("revanced_color_picker", "layout"), null);
|
||||
dialogColorPickerView = colorPicker.findViewById(
|
||||
getResourceIdentifier("color_picker_view", "id"));
|
||||
dialogColorPickerView.setColor(currentColor);
|
||||
layout.addView(colorPicker);
|
||||
|
||||
// Horizontal layout for preview and EditText.
|
||||
LinearLayout inputLayout = new LinearLayout(context);
|
||||
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
inputLayout.setPadding(0, 20, 0, 0);
|
||||
|
||||
dialogColorPreview = new TextView(context);
|
||||
inputLayout.addView(dialogColorPreview);
|
||||
updateColorPreview();
|
||||
|
||||
EditText editText = getEditText();
|
||||
ViewParent parent = editText.getParent();
|
||||
if (parent instanceof ViewGroup parentViewGroup) {
|
||||
parentViewGroup.removeView(editText);
|
||||
}
|
||||
editText.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
String currentColorString = getColorString(currentColor);
|
||||
editText.setText(currentColorString);
|
||||
editText.setSelection(currentColorString.length());
|
||||
editText.setTypeface(Typeface.MONOSPACE);
|
||||
colorTextWatcher = createColorTextWatcher(dialogColorPickerView);
|
||||
editText.addTextChangedListener(colorTextWatcher);
|
||||
inputLayout.addView(editText);
|
||||
|
||||
// Add a dummy view to take up remaining horizontal space,
|
||||
// otherwise it will show an oversize underlined text view.
|
||||
View paddingView = new View(context);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
0,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
1f
|
||||
);
|
||||
paddingView.setLayoutParams(params);
|
||||
inputLayout.addView(paddingView);
|
||||
|
||||
layout.addView(inputLayout);
|
||||
|
||||
// Set up color picker listener with debouncing.
|
||||
// Add listener last to prevent callbacks from set calls above.
|
||||
dialogColorPickerView.setOnColorChangedListener(color -> {
|
||||
// Check if it actually changed, since this callback
|
||||
// can be caused by updates in afterTextChanged().
|
||||
if (currentColor == color) {
|
||||
return;
|
||||
}
|
||||
|
||||
String updatedColorString = getColorString(color);
|
||||
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
|
||||
currentColor = color;
|
||||
editText.setText(updatedColorString);
|
||||
editText.setSelection(updatedColorString.length());
|
||||
|
||||
updateColorPreview();
|
||||
updateWidgetColorDot();
|
||||
});
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the color preview TextView with a colored dot.
|
||||
*/
|
||||
@@ -360,65 +279,153 @@ public class ColorPickerPreference extends EditTextPreference {
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the dialog builder with a custom view and reset button.
|
||||
*
|
||||
* @param builder The AlertDialog.Builder to configure.
|
||||
* Creates a Dialog with a color preview and EditText for hex color input.
|
||||
*/
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
LinearLayout dialogLayout = createDialogLayout(builder.getContext());
|
||||
builder.setView(dialogLayout);
|
||||
final int originalColor = currentColor;
|
||||
|
||||
builder.setNeutralButton(str("revanced_settings_reset_color"), null);
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
try {
|
||||
String colorString = getEditText().getText().toString();
|
||||
|
||||
if (colorString.length() != COLOR_STRING_LENGTH) {
|
||||
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||
setText(getColorString(originalColor));
|
||||
return;
|
||||
}
|
||||
|
||||
setText(colorString);
|
||||
} catch (Exception ex) {
|
||||
// Should never happen due to a bad color string,
|
||||
// since the text is validated and fixed while the user types.
|
||||
Logger.printException(() -> "setPositiveButton failure", ex);
|
||||
}
|
||||
});
|
||||
|
||||
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
try {
|
||||
// Restore the original color.
|
||||
setText(getColorString(originalColor));
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setNegativeButton failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showDialog(Bundle state) {
|
||||
super.showDialog(state);
|
||||
Context context = getContext();
|
||||
|
||||
AlertDialog dialog = (AlertDialog) getDialog();
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
// Inflate color picker view.
|
||||
View colorPicker = LayoutInflater.from(context).inflate(
|
||||
getResourceIdentifier("revanced_color_picker", "layout"), null);
|
||||
dialogColorPickerView = colorPicker.findViewById(
|
||||
getResourceIdentifier("revanced_color_picker_view", "id"));
|
||||
dialogColorPickerView.setColor(currentColor);
|
||||
|
||||
// Do not close dialog when reset is pressed.
|
||||
Button button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
button.setOnClickListener(view -> {
|
||||
try {
|
||||
final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
|
||||
// Setting view color causes listener callback into this class.
|
||||
dialogColorPickerView.setColor(defaultColor);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setOnClickListener failure", ex);
|
||||
// Horizontal layout for preview and EditText.
|
||||
LinearLayout inputLayout = new LinearLayout(context);
|
||||
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||
|
||||
dialogColorPreview = new TextView(context);
|
||||
LinearLayout.LayoutParams previewParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
);
|
||||
previewParams.setMargins(dipToPixels(15), 0, dipToPixels(10), 0); // text dot has its own indents so 15, instead 16.
|
||||
dialogColorPreview.setLayoutParams(previewParams);
|
||||
inputLayout.addView(dialogColorPreview);
|
||||
updateColorPreview();
|
||||
|
||||
EditText editText = getEditText();
|
||||
ViewParent parent = editText.getParent();
|
||||
if (parent instanceof ViewGroup parentViewGroup) {
|
||||
parentViewGroup.removeView(editText);
|
||||
}
|
||||
editText.setLayoutParams(new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
));
|
||||
String currentColorString = getColorString(currentColor);
|
||||
editText.setText(currentColorString);
|
||||
editText.setSelection(currentColorString.length());
|
||||
editText.setTypeface(Typeface.MONOSPACE);
|
||||
colorTextWatcher = createColorTextWatcher(dialogColorPickerView);
|
||||
editText.addTextChangedListener(colorTextWatcher);
|
||||
inputLayout.addView(editText);
|
||||
|
||||
// Add a dummy view to take up remaining horizontal space,
|
||||
// otherwise it will show an oversize underlined text view.
|
||||
View paddingView = new View(context);
|
||||
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
0,
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
1f
|
||||
);
|
||||
paddingView.setLayoutParams(params);
|
||||
inputLayout.addView(paddingView);
|
||||
|
||||
// Create content container for color picker and input layout.
|
||||
LinearLayout contentContainer = new LinearLayout(context);
|
||||
contentContainer.setOrientation(LinearLayout.VERTICAL);
|
||||
contentContainer.addView(colorPicker);
|
||||
contentContainer.addView(inputLayout);
|
||||
|
||||
// Create ScrollView to wrap the content container.
|
||||
ScrollView contentScrollView = new ScrollView(context);
|
||||
contentScrollView.setVerticalScrollBarEnabled(false); // Disable vertical scrollbar.
|
||||
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); // Disable overscroll effect.
|
||||
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
0,
|
||||
1.0f
|
||||
);
|
||||
contentScrollView.setLayoutParams(scrollViewParams);
|
||||
contentScrollView.addView(contentContainer);
|
||||
|
||||
// Create custom dialog.
|
||||
final int originalColor = currentColor & 0x00FFFFFF;
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
getTitle() != null ? getTitle().toString() : str("revanced_settings_color_picker_title"), // Title.
|
||||
null, // No message.
|
||||
null, // No EditText.
|
||||
null, // OK button text.
|
||||
() -> {
|
||||
// OK button action.
|
||||
try {
|
||||
String colorString = editText.getText().toString();
|
||||
if (colorString.length() != COLOR_STRING_LENGTH) {
|
||||
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||
setText(getColorString(originalColor));
|
||||
return;
|
||||
}
|
||||
setText(colorString);
|
||||
} catch (Exception ex) {
|
||||
// Should never happen due to a bad color string,
|
||||
// since the text is validated and fixed while the user types.
|
||||
Logger.printException(() -> "OK button failure", ex);
|
||||
}
|
||||
},
|
||||
() -> {
|
||||
// Cancel button action.
|
||||
try {
|
||||
// Restore the original color.
|
||||
setText(getColorString(originalColor));
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Cancel button failure", ex);
|
||||
}
|
||||
},
|
||||
str("revanced_settings_reset_color"), // Neutral button text.
|
||||
() -> {
|
||||
// Neutral button action.
|
||||
try {
|
||||
final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
|
||||
// Setting view color causes listener callback into this class.
|
||||
dialogColorPickerView.setColor(defaultColor);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Reset button failure", ex);
|
||||
}
|
||||
},
|
||||
false // Do not dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Add the ScrollView to the dialog's main layout.
|
||||
LinearLayout dialogMainLayout = dialogPair.second;
|
||||
dialogMainLayout.addView(contentScrollView, dialogMainLayout.getChildCount() - 1);
|
||||
|
||||
// Set up color picker listener with debouncing.
|
||||
// Add listener last to prevent callbacks from set calls above.
|
||||
dialogColorPickerView.setOnColorChangedListener(color -> {
|
||||
// Check if it actually changed, since this callback
|
||||
// can be caused by updates in afterTextChanged().
|
||||
if (currentColor == color) {
|
||||
return;
|
||||
}
|
||||
|
||||
String updatedColorString = getColorString(color);
|
||||
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
|
||||
currentColor = color;
|
||||
editText.setText(updatedColorString);
|
||||
editText.setSelection(updatedColorString.length());
|
||||
|
||||
updateColorPreview();
|
||||
updateWidgetColorDot();
|
||||
});
|
||||
|
||||
// Configure and show the dialog.
|
||||
Dialog dialog = dialogPair.first;
|
||||
dialog.setCanceledOnTouchOutside(false);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -29,8 +29,8 @@ import app.revanced.extension.shared.Utils;
|
||||
* <p>
|
||||
* This view displays two main components for color selection:
|
||||
* <ul>
|
||||
* <li><b>Hue Bar:</b> A vertical bar on the right that allows the user to select the hue component of the color.
|
||||
* <li><b>Saturation-Value Selector:</b> A rectangular area that allows the user to select the saturation and value (brightness)
|
||||
* <li><b>Hue Bar:</b> A horizontal bar at the bottom that allows the user to select the hue component of the color.
|
||||
* <li><b>Saturation-Value Selector:</b> A rectangular area above the hue bar that allows the user to select the saturation and value (brightness)
|
||||
* components of the color based on the selected hue.
|
||||
* </ul>
|
||||
*
|
||||
@@ -63,12 +63,12 @@ public class ColorPickerView extends View {
|
||||
|
||||
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
|
||||
private static final float VIEW_PADDING = dipToPixels(16);
|
||||
private static final float HUE_BAR_WIDTH = dipToPixels(12);
|
||||
private static final float HUE_BAR_HEIGHT = dipToPixels(12);
|
||||
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
|
||||
private static final float SELECTOR_RADIUS = dipToPixels(12);
|
||||
private static final float SELECTOR_STROKE_WIDTH = 8;
|
||||
/**
|
||||
* Hue fill radius. Use slightly smaller radius for the selector handle fill,
|
||||
* Hue fill radius. Use slightly smaller radius for the selector handle fill,
|
||||
* otherwise the anti-aliasing causes the fill color to bleed past the selector outline.
|
||||
*/
|
||||
private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2;
|
||||
@@ -144,17 +144,17 @@ public class ColorPickerView extends View {
|
||||
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
||||
|
||||
final int minWidth = Utils.dipToPixels(250);
|
||||
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO);
|
||||
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
|
||||
|
||||
int width = resolveSize(minWidth, widthMeasureSpec);
|
||||
int height = resolveSize(minHeight, heightMeasureSpec);
|
||||
|
||||
// Ensure minimum dimensions for usability
|
||||
// Ensure minimum dimensions for usability.
|
||||
width = Math.max(width, minWidth);
|
||||
height = Math.max(height, minHeight);
|
||||
|
||||
// Adjust height to maintain desired aspect ratio if possible
|
||||
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO);
|
||||
// Adjust height to maintain desired aspect ratio if possible.
|
||||
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
|
||||
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
|
||||
height = desiredHeight;
|
||||
}
|
||||
@@ -171,22 +171,22 @@ public class ColorPickerView extends View {
|
||||
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
||||
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
||||
|
||||
// Calculate bounds with hue bar on the right
|
||||
// Calculate bounds with hue bar at the bottom.
|
||||
final float effectiveWidth = width - (2 * VIEW_PADDING);
|
||||
final float selectorWidth = effectiveWidth - HUE_BAR_WIDTH - MARGIN_BETWEEN_AREAS;
|
||||
final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS;
|
||||
|
||||
// Adjust rectangles to account for padding and density-independent dimensions
|
||||
// Adjust rectangles to account for padding and density-independent dimensions.
|
||||
saturationValueRect.set(
|
||||
VIEW_PADDING,
|
||||
VIEW_PADDING,
|
||||
VIEW_PADDING + selectorWidth,
|
||||
height - VIEW_PADDING
|
||||
VIEW_PADDING + effectiveWidth,
|
||||
VIEW_PADDING + effectiveHeight
|
||||
);
|
||||
|
||||
hueRect.set(
|
||||
width - VIEW_PADDING - HUE_BAR_WIDTH,
|
||||
VIEW_PADDING,
|
||||
width - VIEW_PADDING,
|
||||
height - VIEW_PADDING - HUE_BAR_HEIGHT,
|
||||
VIEW_PADDING + effectiveWidth,
|
||||
height - VIEW_PADDING
|
||||
);
|
||||
|
||||
@@ -201,7 +201,7 @@ public class ColorPickerView extends View {
|
||||
private void updateHueShader() {
|
||||
LinearGradient hueShader = new LinearGradient(
|
||||
hueRect.left, hueRect.top,
|
||||
hueRect.left, hueRect.bottom,
|
||||
hueRect.right, hueRect.top,
|
||||
HUE_COLORS,
|
||||
null,
|
||||
Shader.TileMode.CLAMP
|
||||
@@ -263,8 +263,8 @@ public class ColorPickerView extends View {
|
||||
// Draw the hue bar.
|
||||
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
|
||||
|
||||
final float hueSelectorX = hueRect.centerX();
|
||||
final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height();
|
||||
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
||||
final float hueSelectorY = hueRect.centerY();
|
||||
|
||||
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||
@@ -316,17 +316,17 @@ public class ColorPickerView extends View {
|
||||
|
||||
// Define touch expansion for the hue bar.
|
||||
RectF expandedHueRect = new RectF(
|
||||
hueRect.left - TOUCH_EXPANSION,
|
||||
hueRect.top,
|
||||
hueRect.right + TOUCH_EXPANSION,
|
||||
hueRect.bottom
|
||||
hueRect.left,
|
||||
hueRect.top - TOUCH_EXPANSION,
|
||||
hueRect.right,
|
||||
hueRect.bottom + TOUCH_EXPANSION
|
||||
);
|
||||
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// Calculate current handle positions.
|
||||
final float hueSelectorX = hueRect.centerX();
|
||||
final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height();
|
||||
final float hueSelectorX = hueRect.left + (hue / 360f) * hueRect.width();
|
||||
final float hueSelectorY = hueRect.centerY();
|
||||
|
||||
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||
@@ -348,14 +348,14 @@ public class ColorPickerView extends View {
|
||||
// Check if the touch started on a handle or within the expanded hue bar area.
|
||||
if (hueHitRect.contains(x, y)) {
|
||||
isDraggingHue = true;
|
||||
updateHueFromTouch(y);
|
||||
updateHueFromTouch(x);
|
||||
} else if (satValHitRect.contains(x, y)) {
|
||||
isDraggingSaturation = true;
|
||||
updateSaturationValueFromTouch(x, y);
|
||||
} else if (expandedHueRect.contains(x, y)) {
|
||||
// Handle touch within the expanded hue bar area.
|
||||
isDraggingHue = true;
|
||||
updateHueFromTouch(y);
|
||||
updateHueFromTouch(x);
|
||||
} else if (saturationValueRect.contains(x, y)) {
|
||||
isDraggingSaturation = true;
|
||||
updateSaturationValueFromTouch(x, y);
|
||||
@@ -365,7 +365,7 @@ public class ColorPickerView extends View {
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// Continue updating values even if touch moves outside the view.
|
||||
if (isDraggingHue) {
|
||||
updateHueFromTouch(y);
|
||||
updateHueFromTouch(x);
|
||||
} else if (isDraggingSaturation) {
|
||||
updateSaturationValueFromTouch(x, y);
|
||||
}
|
||||
@@ -387,12 +387,12 @@ public class ColorPickerView extends View {
|
||||
/**
|
||||
* Updates the hue value based on touch position, clamping to valid range.
|
||||
*
|
||||
* @param y The y-coordinate of the touch position.
|
||||
* @param x The x-coordinate of the touch position.
|
||||
*/
|
||||
private void updateHueFromTouch(float y) {
|
||||
// Clamp y to the hue rectangle bounds.
|
||||
final float clampedY = Utils.clamp(y, hueRect.top, hueRect.bottom);
|
||||
final float updatedHue = ((clampedY - hueRect.top) / hueRect.height()) * 360f;
|
||||
private void updateHueFromTouch(float x) {
|
||||
// Clamp x to the hue rectangle bounds.
|
||||
final float clampedX = Utils.clamp(x, hueRect.left, hueRect.right);
|
||||
final float updatedHue = ((clampedX - hueRect.left) / hueRect.width()) * 360f;
|
||||
if (hue == updatedHue) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.preference.ListPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.*;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
/**
|
||||
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class CustomDialogListPreference extends ListPreference {
|
||||
|
||||
/**
|
||||
* Custom ArrayAdapter to handle checkmark visibility.
|
||||
*/
|
||||
private static class ListPreferenceArrayAdapter extends ArrayAdapter<CharSequence> {
|
||||
private static class SubViewDataContainer {
|
||||
ImageView checkIcon;
|
||||
View placeholder;
|
||||
TextView itemText;
|
||||
}
|
||||
|
||||
final int layoutResourceId;
|
||||
final CharSequence[] entryValues;
|
||||
String selectedValue;
|
||||
|
||||
public ListPreferenceArrayAdapter(Context context, int resource, CharSequence[] entries,
|
||||
CharSequence[] entryValues, String selectedValue) {
|
||||
super(context, resource, entries);
|
||||
this.layoutResourceId = resource;
|
||||
this.entryValues = entryValues;
|
||||
this.selectedValue = selectedValue;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(int position, View convertView, @NonNull ViewGroup parent) {
|
||||
View view = convertView;
|
||||
SubViewDataContainer holder;
|
||||
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
view = inflater.inflate(layoutResourceId, parent, false);
|
||||
holder = new SubViewDataContainer();
|
||||
holder.checkIcon = view.findViewById(Utils.getResourceIdentifier(
|
||||
"revanced_check_icon", "id"));
|
||||
holder.placeholder = view.findViewById(Utils.getResourceIdentifier(
|
||||
"revanced_check_icon_placeholder", "id"));
|
||||
holder.itemText = view.findViewById(Utils.getResourceIdentifier(
|
||||
"revanced_item_text", "id"));
|
||||
view.setTag(holder);
|
||||
} else {
|
||||
holder = (SubViewDataContainer) view.getTag();
|
||||
}
|
||||
|
||||
// Set text.
|
||||
holder.itemText.setText(getItem(position));
|
||||
holder.itemText.setTextColor(Utils.getAppForegroundColor());
|
||||
|
||||
// Show or hide checkmark and placeholder.
|
||||
String currentValue = entryValues[position].toString();
|
||||
boolean isSelected = currentValue.equals(selectedValue);
|
||||
holder.checkIcon.setVisibility(isSelected ? View.VISIBLE : View.GONE);
|
||||
holder.checkIcon.setColorFilter(Utils.getAppForegroundColor());
|
||||
holder.placeholder.setVisibility(isSelected ? View.GONE : View.VISIBLE);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setSelectedValue(String value) {
|
||||
this.selectedValue = value;
|
||||
}
|
||||
}
|
||||
|
||||
public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
|
||||
public CustomDialogListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public CustomDialogListPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public CustomDialogListPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showDialog(Bundle state) {
|
||||
Context context = getContext();
|
||||
|
||||
// Create ListView.
|
||||
ListView listView = new ListView(context);
|
||||
listView.setId(android.R.id.list);
|
||||
listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
|
||||
|
||||
// Create custom adapter for the ListView.
|
||||
ListPreferenceArrayAdapter adapter = new ListPreferenceArrayAdapter(
|
||||
context,
|
||||
Utils.getResourceIdentifier("revanced_custom_list_item_checked", "layout"),
|
||||
getEntries(),
|
||||
getEntryValues(),
|
||||
getValue()
|
||||
);
|
||||
listView.setAdapter(adapter);
|
||||
|
||||
// Set checked item.
|
||||
String currentValue = getValue();
|
||||
if (currentValue != null) {
|
||||
CharSequence[] entryValues = getEntryValues();
|
||||
for (int i = 0, length = entryValues.length; i < length; i++) {
|
||||
if (currentValue.equals(entryValues[i].toString())) {
|
||||
listView.setItemChecked(i, true);
|
||||
listView.setSelection(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create the custom dialog without OK button.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
getTitle() != null ? getTitle().toString() : "",
|
||||
null,
|
||||
null,
|
||||
null, // No OK button text.
|
||||
null, // No OK button action.
|
||||
() -> {}, // Cancel button action (just dismiss).
|
||||
null,
|
||||
null,
|
||||
true
|
||||
);
|
||||
|
||||
// Add the ListView to the main layout.
|
||||
LinearLayout mainLayout = dialogPair.second;
|
||||
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
0,
|
||||
1.0f
|
||||
);
|
||||
mainLayout.addView(listView, mainLayout.getChildCount() - 1, listViewParams);
|
||||
|
||||
// Handle item click to select value and dismiss dialog.
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
String selectedValue = getEntryValues()[position].toString();
|
||||
if (callChangeListener(selectedValue)) {
|
||||
setValue(selectedValue);
|
||||
adapter.setSelectedValue(selectedValue);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
dialogPair.first.dismiss();
|
||||
});
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,30 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.text.InputType;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import app.revanced.extension.shared.settings.Setting;
|
||||
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener {
|
||||
@@ -54,7 +65,8 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
try {
|
||||
// Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened.
|
||||
// Must set text before showing dialog,
|
||||
// otherwise text is non-selectable if this preference is later reopened.
|
||||
existingSettings = Setting.exportToJson(getContext());
|
||||
getEditText().setText(existingSettings);
|
||||
} catch (Exception ex) {
|
||||
@@ -64,18 +76,32 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
protected void showDialog(Bundle state) {
|
||||
try {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
Context context = getContext();
|
||||
EditText editText = getEditText();
|
||||
|
||||
// Show the user the settings in JSON format.
|
||||
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
|
||||
Utils.setClipboard(getEditText().getText());
|
||||
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
|
||||
importSettings(builder.getContext(), getEditText().getText().toString());
|
||||
});
|
||||
// Create a custom dialog with the EditText.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
str("revanced_pref_import_export_title"), // Title.
|
||||
null, // No message (EditText replaces it).
|
||||
editText, // Pass the EditText.
|
||||
str("revanced_settings_import"), // OK button text.
|
||||
() -> importSettings(context, editText.getText().toString()), // OK button action.
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
str("revanced_settings_import_copy"), // Neutral button (Copy) text.
|
||||
() -> {
|
||||
// Neutral button (Copy) action. Show the user the settings in JSON format.
|
||||
Utils.setClipboard(editText.getText());
|
||||
},
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +114,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
||||
|
||||
final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings);
|
||||
if (rebootNeeded) {
|
||||
AbstractPreferenceFragment.showRestartDialog(getContext());
|
||||
AbstractPreferenceFragment.showRestartDialog(context);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "importSettings failure", ex);
|
||||
@@ -96,5 +122,4 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
||||
AbstractPreferenceFragment.settingImportInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
@@ -8,16 +9,19 @@ import android.app.Dialog;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -48,28 +52,6 @@ public class ReVancedAboutPreference extends Preference {
|
||||
return text.replace("-", "‑"); // #8209 = non breaking hyphen.
|
||||
}
|
||||
|
||||
private static String getColorHexString(int color) {
|
||||
return String.format("#%06X", (0x00FFFFFF & color));
|
||||
}
|
||||
|
||||
protected boolean isDarkModeEnabled() {
|
||||
return Utils.isDarkModeEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses can override this and provide a themed color.
|
||||
*/
|
||||
protected int getLightColor() {
|
||||
return Color.WHITE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses can override this and provide a themed color.
|
||||
*/
|
||||
protected int getDarkColor() {
|
||||
return Color.BLACK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apps that do not support bundling resources must override this.
|
||||
*
|
||||
@@ -86,9 +68,8 @@ public class ReVancedAboutPreference extends Preference {
|
||||
builder.append("<html>");
|
||||
builder.append("<body style=\"text-align: center; padding: 10px;\">");
|
||||
|
||||
final boolean isDarkMode = isDarkModeEnabled();
|
||||
String backgroundColorHex = getColorHexString(isDarkMode ? getDarkColor() : getLightColor());
|
||||
String foregroundColorHex = getColorHexString(isDarkMode ? getLightColor() : getDarkColor());
|
||||
String foregroundColorHex = Utils.getColorHexString(Utils.getAppForegroundColor());
|
||||
String backgroundColorHex = Utils.getColorHexString(Utils.getDialogBackgroundColor());
|
||||
// Apply light/dark mode colors.
|
||||
builder.append(String.format(
|
||||
"<style> body { background-color: %s; color: %s; } a { color: %s; } </style>",
|
||||
@@ -220,14 +201,38 @@ class WebViewDialog extends Dialog {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||
requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
|
||||
|
||||
// Create main layout.
|
||||
LinearLayout mainLayout = new LinearLayout(getContext());
|
||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
final int padding = dipToPixels(10);
|
||||
mainLayout.setPadding(padding, padding, padding, padding);
|
||||
// Set rounded rectangle background.
|
||||
ShapeDrawable mainBackground = new ShapeDrawable(new RoundRectShape(
|
||||
Utils.createCornerRadii(28), null, null));
|
||||
mainBackground.getPaint().setColor(Utils.getDialogBackgroundColor());
|
||||
mainLayout.setBackground(mainBackground);
|
||||
|
||||
// Create WebView.
|
||||
WebView webView = new WebView(getContext());
|
||||
webView.setVerticalScrollBarEnabled(false); // Disable the vertical scrollbar.
|
||||
webView.setOverScrollMode(View.OVER_SCROLL_NEVER);
|
||||
webView.getSettings().setJavaScriptEnabled(true);
|
||||
webView.setWebViewClient(new OpenLinksExternallyWebClient());
|
||||
webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null);
|
||||
|
||||
setContentView(webView);
|
||||
// Add WebView to layout.
|
||||
mainLayout.addView(webView);
|
||||
|
||||
setContentView(mainLayout);
|
||||
|
||||
// Set dialog window attributes
|
||||
Window window = getWindow();
|
||||
if (window != null) {
|
||||
Utils.setDialogWindowParameters(window);
|
||||
}
|
||||
}
|
||||
|
||||
private class OpenLinksExternallyWebClient extends WebViewClient {
|
||||
@@ -315,7 +320,7 @@ class AboutLinksRoutes {
|
||||
// Do not show an exception toast if the server is down
|
||||
final int responseCode = connection.getResponseCode();
|
||||
if (responseCode != 200) {
|
||||
Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode);
|
||||
Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode);
|
||||
return NO_CONNECTION_STATIC_LINKS;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.graphics.drawable.shapes.RectShape;
|
||||
import android.graphics.drawable.shapes.RoundRectShape;
|
||||
import android.graphics.drawable.ShapeDrawable;
|
||||
import android.graphics.Paint.Style;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -44,41 +58,61 @@ public class ResettableEditTextPreference extends EditTextPreference {
|
||||
this.setting = setting;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
super.onPrepareDialogBuilder(builder);
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
|
||||
if (setting == null) {
|
||||
String key = getKey();
|
||||
if (key != null) {
|
||||
setting = Setting.getSettingFromPath(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (setting != null) {
|
||||
builder.setNeutralButton(str("revanced_settings_reset"), null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showDialog(Bundle state) {
|
||||
super.showDialog(state);
|
||||
try {
|
||||
Context context = getContext();
|
||||
EditText editText = getEditText();
|
||||
|
||||
// Override the button click listener to prevent dismissing the dialog.
|
||||
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
if (button == null) {
|
||||
return;
|
||||
}
|
||||
button.setOnClickListener(v -> {
|
||||
try {
|
||||
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
|
||||
EditText editText = getEditText();
|
||||
editText.setText(defaultStringValue);
|
||||
editText.setSelection(defaultStringValue.length()); // move cursor to end of text
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "reset failure", ex);
|
||||
// Resolve setting if not already set.
|
||||
if (setting == null) {
|
||||
String key = getKey();
|
||||
if (key != null) {
|
||||
setting = Setting.getSettingFromPath(key);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Set initial EditText value to the current persisted value or empty string.
|
||||
String initialValue = getText() != null ? getText() : "";
|
||||
editText.setText(initialValue);
|
||||
editText.setSelection(initialValue.length()); // Move cursor to end.
|
||||
|
||||
// Create custom dialog.
|
||||
String neutralButtonText = (setting != null) ? str("revanced_settings_reset") : null;
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
getTitle() != null ? getTitle().toString() : "", // Title.
|
||||
null, // Message is replaced by EditText.
|
||||
editText, // Pass the EditText.
|
||||
null, // OK button text.
|
||||
() -> {
|
||||
// OK button action. Persist the EditText value when OK is clicked.
|
||||
String newValue = editText.getText().toString();
|
||||
if (callChangeListener(newValue)) {
|
||||
setText(newValue);
|
||||
}
|
||||
},
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
neutralButtonText, // Neutral button text (Reset).
|
||||
() -> {
|
||||
// Neutral button action.
|
||||
if (setting != null) {
|
||||
try {
|
||||
String defaultStringValue = Objects.requireNonNull(setting).defaultValue.toString();
|
||||
editText.setText(defaultStringValue);
|
||||
editText.setSelection(defaultStringValue.length()); // Move cursor to end of text.
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "reset failure", ex);
|
||||
}
|
||||
}
|
||||
},
|
||||
false // Do not dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package app.revanced.extension.shared.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.ListPreference;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
|
||||
@@ -24,12 +23,14 @@ import app.revanced.extension.shared.Utils;
|
||||
* it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public class SortedListPreference extends ListPreference {
|
||||
public class SortedListPreference extends CustomDialogListPreference {
|
||||
|
||||
/**
|
||||
* Sorts the current list entries.
|
||||
*
|
||||
* @param firstEntriesToPreserve The number of entries to preserve in their original position.
|
||||
* @param firstEntriesToPreserve The number of entries to preserve in their original position,
|
||||
* or a negative value to not sort and leave entries
|
||||
* as they current are.
|
||||
*/
|
||||
public void sortEntryAndValues(int firstEntriesToPreserve) {
|
||||
CharSequence[] entries = getEntries();
|
||||
@@ -44,6 +45,10 @@ public class SortedListPreference extends ListPreference {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
if (firstEntriesToPreserve < 0) {
|
||||
return; // Nothing to do.
|
||||
}
|
||||
|
||||
List<Pair<CharSequence, CharSequence>> firstEntries = new ArrayList<>(firstEntriesToPreserve);
|
||||
|
||||
// Android does not have a triple class like Kotlin, So instead use a nested pair.
|
||||
@@ -85,10 +90,6 @@ public class SortedListPreference extends ListPreference {
|
||||
super.setEntryValues(sortedEntryValues);
|
||||
}
|
||||
|
||||
protected int getFirstEntriesToPreserve() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
|
||||
@@ -112,4 +113,12 @@ public class SortedListPreference extends ListPreference {
|
||||
|
||||
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The number of first entries to leave exactly where they are, and do not sort them.
|
||||
* A negative value indicates do not sort any entries.
|
||||
*/
|
||||
protected int getFirstEntriesToPreserve() {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,82 @@
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
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;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "returnNullIfIsCreateButton failure", ex);
|
||||
}
|
||||
|
||||
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,85 @@
|
||||
package app.revanced.extension.spotify.shared;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
public final class ComponentFilters {
|
||||
|
||||
public interface ComponentFilter {
|
||||
@NonNull
|
||||
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;
|
||||
@Nullable
|
||||
private String stringfiedResourceId;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@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;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@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,7 +16,7 @@ public class SpoofSimPatch {
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.initializationException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null);
|
||||
Logger.printException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
package app.revanced.extension.youtube;
|
||||
|
||||
import static app.revanced.extension.shared.Utils.clamp;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Build;
|
||||
import android.text.style.ReplacementSpan;
|
||||
import android.text.TextPaint;
|
||||
import android.view.Window;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
|
||||
public class ThemeHelper {
|
||||
@Nullable
|
||||
private static Integer darkThemeColor, lightThemeColor;
|
||||
private static int themeValue;
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static void setTheme(Enum<?> value) {
|
||||
final int newOrdinalValue = value.ordinal();
|
||||
if (themeValue != newOrdinalValue) {
|
||||
themeValue = newOrdinalValue;
|
||||
Logger.printDebug(() -> "Theme value: " + newOrdinalValue);
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isDarkTheme() {
|
||||
return themeValue == 1;
|
||||
}
|
||||
|
||||
public static void setActivityTheme(Activity activity) {
|
||||
final var theme = isDarkTheme()
|
||||
? "Theme.YouTube.Settings.Dark"
|
||||
: "Theme.YouTube.Settings";
|
||||
activity.setTheme(Utils.getResourceIdentifier(theme, "style"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
private static String darkThemeResourceName() {
|
||||
// Value is changed by Theme patch, if included.
|
||||
return "@color/yt_black3";
|
||||
}
|
||||
|
||||
private static int getThemeColor(String resourceName, int defaultColor) {
|
||||
try {
|
||||
return Utils.getColorFromString(resourceName);
|
||||
} catch (Exception ex) {
|
||||
// User entered an invalid custom theme color.
|
||||
// Normally this should never be reached, and no localized strings are needed.
|
||||
Utils.showToastLong("Invalid custom theme color: " + resourceName);
|
||||
return defaultColor;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The dark theme color as specified by the Theme patch (if included),
|
||||
* or the dark mode background color unpatched YT uses.
|
||||
*/
|
||||
public static int getDarkThemeColor() {
|
||||
if (darkThemeColor == null) {
|
||||
darkThemeColor = getThemeColor(darkThemeResourceName(), Color.BLACK);
|
||||
}
|
||||
return darkThemeColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
private static String lightThemeResourceName() {
|
||||
// Value is changed by Theme patch, if included.
|
||||
return "@color/yt_white1";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The light theme color as specified by the Theme patch (if included),
|
||||
* or the non dark mode background color unpatched YT uses.
|
||||
*/
|
||||
public static int getLightThemeColor() {
|
||||
if (lightThemeColor == null) {
|
||||
lightThemeColor = getThemeColor(lightThemeResourceName(), Color.WHITE);
|
||||
}
|
||||
return lightThemeColor;
|
||||
}
|
||||
|
||||
public static int getBackgroundColor() {
|
||||
return isDarkTheme() ? getDarkThemeColor() : getLightThemeColor();
|
||||
}
|
||||
|
||||
public static int getForegroundColor() {
|
||||
return isDarkTheme() ? getLightThemeColor() : getDarkThemeColor();
|
||||
}
|
||||
|
||||
public static int getToolbarBackgroundColor() {
|
||||
final String colorName = isDarkTheme()
|
||||
? "yt_black3"
|
||||
: "yt_white1";
|
||||
|
||||
return Utils.getColorFromString(colorName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system navigation bar color for the activity.
|
||||
* Applies the background color obtained from {@link #getBackgroundColor()} to the navigation bar.
|
||||
* For Android 10 (API 29) and above, enforces navigation bar contrast to ensure visibility.
|
||||
*/
|
||||
public static void setNavigationBarColor(@Nullable Window window) {
|
||||
if (window == null) {
|
||||
Logger.printDebug(() -> "Cannot set navigation bar color, window is null");
|
||||
return;
|
||||
}
|
||||
|
||||
window.setNavigationBarColor(getBackgroundColor());
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.setNavigationBarContrastEnforced(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjusts the brightness of a color by lightening or darkening it based on the given factor.
|
||||
* <p>
|
||||
* If the factor is greater than 1, the color is lightened by interpolating toward white (#FFFFFF).
|
||||
* If the factor is less than or equal to 1, the color is darkened by scaling its RGB components toward black (#000000).
|
||||
* The alpha channel remains unchanged.
|
||||
*
|
||||
* @param color The input color to adjust, in ARGB format.
|
||||
* @param factor The adjustment factor. Use values > 1.0f to lighten (e.g., 1.11f for slight lightening)
|
||||
* or values <= 1.0f to darken (e.g., 0.95f for slight darkening).
|
||||
* @return The adjusted color in ARGB format.
|
||||
*/
|
||||
public static int adjustColorBrightness(int color, float factor) {
|
||||
final int alpha = Color.alpha(color);
|
||||
int red = Color.red(color);
|
||||
int green = Color.green(color);
|
||||
int blue = Color.blue(color);
|
||||
|
||||
if (factor > 1.0f) {
|
||||
// Lighten: Interpolate toward white (255)
|
||||
final float t = 1.0f - (1.0f / factor); // Interpolation parameter
|
||||
red = Math.round(red + (255 - red) * t);
|
||||
green = Math.round(green + (255 - green) * t);
|
||||
blue = Math.round(blue + (255 - blue) * t);
|
||||
} else {
|
||||
// Darken or no change: Scale toward black
|
||||
red = (int) (red * factor);
|
||||
green = (int) (green * factor);
|
||||
blue = (int) (blue * factor);
|
||||
}
|
||||
|
||||
// Ensure values are within [0, 255]
|
||||
red = clamp(red, 0, 255);
|
||||
green = clamp(green, 0, 255);
|
||||
blue = clamp(blue, 0, 255);
|
||||
|
||||
return Color.argb(alpha, red, green, blue);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -3,7 +3,10 @@ package app.revanced.extension.youtube.patches;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.text.Html;
|
||||
import android.util.Pair;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
@@ -63,18 +66,28 @@ public class CheckWatchHistoryDomainNameResolutionPatch {
|
||||
}
|
||||
|
||||
Utils.runOnMainThread(() -> {
|
||||
var alert = new android.app.AlertDialog.Builder(context)
|
||||
.setTitle(str("revanced_check_watch_history_domain_name_dialog_title"))
|
||||
.setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")))
|
||||
.setIconAttribute(android.R.attr.alertDialogIcon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
}).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> {
|
||||
Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false);
|
||||
dialog.dismiss();
|
||||
}).create();
|
||||
try {
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
str("revanced_check_watch_history_domain_name_dialog_title"), // Title.
|
||||
Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message")), // Message (HTML).
|
||||
null, // No EditText.
|
||||
null, // OK button text.
|
||||
() -> {}, // OK button action (just dismiss).
|
||||
() -> {}, // Cancel button action (just dismiss).
|
||||
str("revanced_check_watch_history_domain_name_dialog_ignore"), // Neutral button text.
|
||||
() -> Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false), // Neutral button action (Ignore).
|
||||
true // Dismiss dialog on Neutral button click.
|
||||
);
|
||||
|
||||
Utils.showDialog(context, alert, false, null);
|
||||
// Show the dialog.
|
||||
Dialog dialog = dialogPair.first;
|
||||
|
||||
Utils.showDialog(context, dialog, false, null);
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "checkDnsResolver dialog creation failure", ex);
|
||||
}
|
||||
});
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "checkDnsResolver failure", ex);
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -7,11 +7,17 @@ public class VersionCheckPatch {
|
||||
return Utils.getAppVersionName().compareTo(version) >= 0;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public static final boolean IS_19_17_OR_GREATER = isVersionOrGreater("19.17.00");
|
||||
@Deprecated
|
||||
public static final boolean IS_19_20_OR_GREATER = isVersionOrGreater("19.20.00");
|
||||
@Deprecated
|
||||
public static final boolean IS_19_21_OR_GREATER = isVersionOrGreater("19.21.00");
|
||||
@Deprecated
|
||||
public static final boolean IS_19_26_OR_GREATER = isVersionOrGreater("19.26.00");
|
||||
@Deprecated
|
||||
public static final boolean IS_19_29_OR_GREATER = isVersionOrGreater("19.29.00");
|
||||
@Deprecated
|
||||
public static final boolean IS_19_34_OR_GREATER = isVersionOrGreater("19.34.00");
|
||||
public static final boolean IS_19_46_OR_GREATER = isVersionOrGreater("19.46.00");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ package app.revanced.extension.youtube.patches.announcements;
|
||||
|
||||
import static android.text.Html.FROM_HTML_MODE_COMPACT;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENTS;
|
||||
import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.json.JSONArray;
|
||||
@@ -120,25 +123,38 @@ public final class AnnouncementsPatch {
|
||||
final Level finalLevel = level;
|
||||
|
||||
Utils.runOnMainThread(() -> {
|
||||
// Show the announcement.
|
||||
var alert = new AlertDialog.Builder(context)
|
||||
.setTitle(finalTitle)
|
||||
.setMessage(finalMessage)
|
||||
.setIcon(finalLevel.icon)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
Settings.ANNOUNCEMENT_LAST_ID.save(finalId);
|
||||
dialog.dismiss();
|
||||
}).setNegativeButton(str("revanced_announcements_dialog_dismiss"), (dialog, which) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setCancelable(false)
|
||||
.create();
|
||||
// Create the custom dialog and show the announcement.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
finalTitle, // Title.
|
||||
finalMessage, // Message.
|
||||
null, // No EditText.
|
||||
null, // OK button text.
|
||||
() -> Settings.ANNOUNCEMENT_LAST_ID.save(finalId), // OK button action.
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
str("revanced_announcements_dialog_dismiss"), // Neutral button text.
|
||||
() -> {}, // Neutral button action (dismiss only).
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
Utils.showDialog(context, alert, false, (AlertDialog dialog) -> {
|
||||
// Make links clickable.
|
||||
((TextView) dialog.findViewById(android.R.id.message))
|
||||
.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
});
|
||||
Dialog dialog = dialogPair.first;
|
||||
LinearLayout mainLayout = dialogPair.second;
|
||||
|
||||
// Set the icon for the title TextView
|
||||
for (int i = 0, childCould = mainLayout.getChildCount(); i < childCould; i++) {
|
||||
View child = mainLayout.getChildAt(i);
|
||||
if (child instanceof TextView childTextView && finalTitle.equals(childTextView.getText().toString())) {
|
||||
childTextView.setCompoundDrawablesWithIntrinsicBounds(
|
||||
finalLevel.icon, 0, 0, 0);
|
||||
childTextView.setCompoundDrawablePadding(dipToPixels(8));
|
||||
}
|
||||
}
|
||||
|
||||
// Set dialog as non-cancelable.
|
||||
dialog.setCancelable(false);
|
||||
|
||||
// Show the dialog.
|
||||
Utils.showDialog(context, dialog);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
final var message = "Failed to get announcement";
|
||||
|
||||
@@ -121,12 +121,14 @@ public final class AdsFilter extends Filter {
|
||||
|
||||
playerShoppingShelf = new StringFilterGroup(
|
||||
Settings.HIDE_PLAYER_STORE_SHELF,
|
||||
"expandable_list.eml",
|
||||
"horizontal_shelf.eml"
|
||||
);
|
||||
|
||||
playerShoppingShelfBuffer = new ByteArrayFilterGroup(
|
||||
null,
|
||||
"shopping_item_card_list.eml"
|
||||
"shopping_link_item",
|
||||
"shopping_item_card_list"
|
||||
);
|
||||
|
||||
channelProfile = new StringFilterGroup(
|
||||
|
||||
@@ -6,7 +6,7 @@ import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQual
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
* Abuse LithoFilter for {@link AdvancedVideoQualityMenuPatch}.
|
||||
* LithoFilter for {@link AdvancedVideoQualityMenuPatch}.
|
||||
*/
|
||||
public final class AdvancedVideoQualityMenuFilter extends Filter {
|
||||
// Must be volatile or synchronized, as litho filtering runs off main thread
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -74,6 +74,27 @@ public final class LithoFilterPatch {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Litho layout fixed thread pool size override.
|
||||
* <p>
|
||||
* Unpatched YouTube uses a layout fixed thread pool between 1 and 3 threads:
|
||||
* <pre>
|
||||
* 1 thread - > Device has less than 6 cores
|
||||
* 2 threads -> Device has over 6 cores and less than 6GB of memory
|
||||
* 3 threads -> Device has over 6 cores and more than 6GB of memory
|
||||
* </pre>
|
||||
*
|
||||
* Using more than 1 thread causes layout issues such as the You tab watch/playlist shelf
|
||||
* that is sometimes incorrectly hidden (ReVanced is not hiding it), and seems to
|
||||
* fix a race issue if using the active navigation tab status with litho filtering.
|
||||
*/
|
||||
private static final int LITHO_LAYOUT_THREAD_POOL_SIZE = 1;
|
||||
|
||||
/**
|
||||
* Placeholder for actual filters.
|
||||
*/
|
||||
private static final class DummyFilter extends Filter { }
|
||||
|
||||
private static final Filter[] filters = new Filter[] {
|
||||
new DummyFilter() // Replaced by patch.
|
||||
};
|
||||
@@ -213,9 +234,28 @@ public final class LithoFilterPatch {
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder for actual filters.
|
||||
*/
|
||||
final class DummyFilter extends Filter { }
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getExecutorCorePoolSize(int originalCorePoolSize) {
|
||||
if (originalCorePoolSize != LITHO_LAYOUT_THREAD_POOL_SIZE) {
|
||||
Logger.printDebug(() -> "Overriding core thread pool size from: " + originalCorePoolSize
|
||||
+ " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
|
||||
}
|
||||
|
||||
return LITHO_LAYOUT_THREAD_POOL_SIZE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*/
|
||||
public static int getExecutorMaxThreads(int originalMaxThreads) {
|
||||
if (originalMaxThreads != LITHO_LAYOUT_THREAD_POOL_SIZE) {
|
||||
Logger.printDebug(() -> "Overriding max thread pool size from: " + originalMaxThreads
|
||||
+ " to: " + LITHO_LAYOUT_THREAD_POOL_SIZE);
|
||||
}
|
||||
|
||||
return LITHO_LAYOUT_THREAD_POOL_SIZE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -40,8 +40,10 @@ public final class ShortsFilter extends Filter {
|
||||
|
||||
private static WeakReference<PivotBar> pivotBarRef = new WeakReference<>(null);
|
||||
|
||||
private final StringFilterGroup shortsCompactFeedVideoPath;
|
||||
private final StringFilterGroup shortsCompactFeedVideo;
|
||||
private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer;
|
||||
private final StringFilterGroup useSoundButton;
|
||||
private final ByteArrayFilterGroup useSoundButtonBuffer;
|
||||
|
||||
private final StringFilterGroup subscribeButton;
|
||||
private final StringFilterGroup joinButton;
|
||||
@@ -49,11 +51,11 @@ public final class ShortsFilter extends Filter {
|
||||
private final StringFilterGroup shelfHeader;
|
||||
|
||||
private final StringFilterGroup suggestedAction;
|
||||
private final ByteArrayFilterGroupList suggestedActionsGroupList = new ByteArrayFilterGroupList();
|
||||
private final ByteArrayFilterGroupList suggestedActionsBuffer = new ByteArrayFilterGroupList();
|
||||
|
||||
private final StringFilterGroup shortsActionBar;
|
||||
private final StringFilterGroup actionButton;
|
||||
private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList();
|
||||
private final StringFilterGroup videoActionButton;
|
||||
private final ByteArrayFilterGroupList videoActionButtonBuffer = new ByteArrayFilterGroupList();
|
||||
|
||||
public ShortsFilter() {
|
||||
//
|
||||
@@ -82,7 +84,7 @@ public final class ShortsFilter extends Filter {
|
||||
// Path components.
|
||||
//
|
||||
|
||||
shortsCompactFeedVideoPath = new StringFilterGroup(null,
|
||||
shortsCompactFeedVideo = new StringFilterGroup(null,
|
||||
// Shorts that appear in the feed/search when the device is using tablet layout.
|
||||
"compact_video.eml",
|
||||
// 'video_lockup_with_attachment.eml' is shown instead of 'compact_video.eml' for some users
|
||||
@@ -143,12 +145,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 +172,24 @@ 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(
|
||||
useSoundButton = new StringFilterGroup(
|
||||
Settings.HIDE_SHORTS_USE_SOUND_BUTTON,
|
||||
"floating_action_button.eml",
|
||||
REEL_METAPANEL_PATH
|
||||
);
|
||||
|
||||
useSoundButtonBuffer = new ByteArrayFilterGroup(
|
||||
null,
|
||||
// Can be simply 'button.eml' or 'shorts_video_action_button.eml'
|
||||
"yt_outline_camera_"
|
||||
);
|
||||
|
||||
videoActionButton = new StringFilterGroup(
|
||||
null,
|
||||
// Can be simply 'button.eml', 'shorts_video_action_button.eml' or 'reel_action_button.eml'
|
||||
"button.eml"
|
||||
);
|
||||
|
||||
@@ -183,34 +199,43 @@ public final class ShortsFilter extends Filter {
|
||||
);
|
||||
|
||||
addPathCallbacks(
|
||||
shortsCompactFeedVideoPath, joinButton, subscribeButton, paidPromotionButton,
|
||||
shortsCompactFeedVideo, joinButton, subscribeButton, paidPromotionButton,
|
||||
shortsActionBar, suggestedAction, pausedOverlayButtons, channelBar,
|
||||
fullVideoLinkLabel, videoTitle, reelSoundMetadata, soundButton, infoPanel,
|
||||
fullVideoLinkLabel, videoTitle, useSoundButton, reelSoundMetadata, soundButton, infoPanel,
|
||||
stickers, likeFountain, likeButton, dislikeButton
|
||||
);
|
||||
|
||||
//
|
||||
// All other action buttons.
|
||||
//
|
||||
videoActionButtonGroupList.addAll(
|
||||
videoActionButtonBuffer.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"
|
||||
)
|
||||
);
|
||||
|
||||
//
|
||||
// Suggested actions.
|
||||
//
|
||||
suggestedActionsGroupList.addAll(
|
||||
suggestedActionsBuffer.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_"
|
||||
@@ -230,10 +255,7 @@ public final class ShortsFilter extends Filter {
|
||||
"yt_outline_bookmark_",
|
||||
// 'Save sound' button. It seems this has been removed and only 'Save music' is used.
|
||||
// Still hide this in case it's still present.
|
||||
"yt_outline_list_add_",
|
||||
// 'Use this sound' button. It seems this has been removed and only 'Save music' is used.
|
||||
// Still hide this in case it's still present.
|
||||
"yt_outline_camera_"
|
||||
"yt_outline_list_add_"
|
||||
),
|
||||
new ByteArrayFilterGroup(
|
||||
Settings.HIDE_SHORTS_SEARCH_SUGGESTIONS,
|
||||
@@ -255,6 +277,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_"
|
||||
@@ -263,7 +289,7 @@ public final class ShortsFilter extends Filter {
|
||||
}
|
||||
|
||||
private boolean isEverySuggestedActionFilterEnabled() {
|
||||
for (ByteArrayFilterGroup group : suggestedActionsGroupList) {
|
||||
for (ByteArrayFilterGroup group : suggestedActionsBuffer) {
|
||||
if (!group.isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
@@ -281,15 +307,19 @@ public final class ShortsFilter extends Filter {
|
||||
return path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH);
|
||||
}
|
||||
|
||||
if (matchedGroup == shortsCompactFeedVideoPath) {
|
||||
if (matchedGroup == useSoundButton) {
|
||||
return useSoundButtonBuffer.check(protobufBufferArray).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == shortsCompactFeedVideo) {
|
||||
return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered();
|
||||
}
|
||||
|
||||
// Video action buttons (comment, share, remix) have the same path.
|
||||
// Like and dislike are separate path filters and don't require buffer searching.
|
||||
if (matchedGroup == shortsActionBar) {
|
||||
return actionButton.check(path).isFiltered()
|
||||
&& videoActionButtonGroupList.check(protobufBufferArray).isFiltered();
|
||||
return videoActionButton.check(path).isFiltered()
|
||||
&& videoActionButtonBuffer.check(protobufBufferArray).isFiltered();
|
||||
}
|
||||
|
||||
if (matchedGroup == suggestedAction) {
|
||||
@@ -300,7 +330,7 @@ public final class ShortsFilter extends Filter {
|
||||
return true;
|
||||
}
|
||||
|
||||
return suggestedActionsGroupList.check(protobufBufferArray).isFiltered();
|
||||
return suggestedActionsBuffer.check(protobufBufferArray).isFiltered();
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -13,8 +13,6 @@ import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
* This patch contains the logic to always open the advanced video quality menu.
|
||||
* Two methods are required, because the quality menu is a RecyclerView in the new YouTube version
|
||||
* and a ListView in the old one.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public final class AdvancedVideoQualityMenuPatch {
|
||||
@@ -76,7 +74,7 @@ public final class AdvancedVideoQualityMenuPatch {
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Used if spoofing to an old app version, and also used for the Shorts video quality flyout.
|
||||
* Shorts video quality flyout.
|
||||
*/
|
||||
public static void showAdvancedVideoQualityMenu(ListView listView) {
|
||||
if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return;
|
||||
@@ -90,14 +88,12 @@ public final class AdvancedVideoQualityMenuPatch {
|
||||
final var indexOfAdvancedQualityMenuItem = 4;
|
||||
if (listView.indexOfChild(child) != indexOfAdvancedQualityMenuItem) return;
|
||||
|
||||
Logger.printDebug(() -> "Found advanced menu item in old type of quality menu");
|
||||
|
||||
listView.setSoundEffectsEnabled(false);
|
||||
final var qualityItemMenuPosition = 4;
|
||||
listView.performItemClick(null, qualityItemMenuPosition, 0);
|
||||
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showOldVideoQualityMenu failure", ex);
|
||||
Logger.printException(() -> "showAdvancedVideoQualityMenu failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,10 +64,11 @@ public class RememberVideoQualityPatch {
|
||||
else videoQualityWifi.save(defaultQuality);
|
||||
networkTypeMessage = str("revanced_remember_video_quality_wifi");
|
||||
}
|
||||
Utils.showToastShort(str(
|
||||
useShortsPreference ? "revanced_remember_video_quality_toast_shorts" : "revanced_remember_video_quality_toast",
|
||||
networkTypeMessage, (defaultQuality + "p")
|
||||
));
|
||||
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
|
||||
Utils.showToastShort(str(
|
||||
useShortsPreference ? "revanced_remember_video_quality_toast_shorts" : "revanced_remember_video_quality_toast",
|
||||
networkTypeMessage, (defaultQuality + "p")
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,24 +1,58 @@
|
||||
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.Gravity;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
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.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 +60,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 +73,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 +102,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 +118,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 +170,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 +215,475 @@ 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);
|
||||
|
||||
// Enable dismissing the dialog when tapping outside.
|
||||
dialog.setCanceledOnTouchOutside(true);
|
||||
|
||||
// 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(
|
||||
Utils.createCornerRadii(12), null, null);
|
||||
ShapeDrawable background = new ShapeDrawable(roundRectShape);
|
||||
background.getPaint().setColor(Utils.getDialogBackgroundColor());
|
||||
mainLayout.setBackground(background);
|
||||
|
||||
// Add handle bar at the top.
|
||||
View handleBar = new View(context);
|
||||
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
|
||||
Utils.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(Utils.getAppForegroundColor());
|
||||
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(
|
||||
Utils.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(
|
||||
Utils.getAppForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar.
|
||||
speedSlider.getThumb().setColorFilter(
|
||||
Utils.getAppForegroundColor(), 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(
|
||||
Utils.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(Utils.getAppForegroundColor());
|
||||
speedButton.setTextSize(12);
|
||||
speedButton.setAllCaps(false);
|
||||
speedButton.setGravity(Gravity.CENTER);
|
||||
|
||||
ShapeDrawable buttonBackground = new ShapeDrawable(new RoundRectShape(
|
||||
Utils.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(Utils.getAppForegroundColor());
|
||||
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.
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
// Set touch listener on mainLayout to enable drag-to-dismiss.
|
||||
//noinspection ClickableViewAccessibility
|
||||
mainLayout.setOnTouchListener(new View.OnTouchListener() {
|
||||
/** Threshold for dismissing the dialog. */
|
||||
final float dismissThreshold = dipToPixels(100); // Distance to drag to dismiss.
|
||||
/** Store initial Y position of touch. */
|
||||
float touchY;
|
||||
/** Track current translation. */
|
||||
float translationY;
|
||||
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
switch (event.getAction()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
// Capture initial Y position of touch.
|
||||
touchY = event.getRawY();
|
||||
translationY = mainLayout.getTranslationY();
|
||||
return true;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
// Calculate drag distance and apply translation downwards only.
|
||||
final float deltaY = event.getRawY() - touchY;
|
||||
// Only allow downward drag (positive deltaY).
|
||||
if (deltaY >= 0) {
|
||||
mainLayout.setTranslationY(translationY + deltaY);
|
||||
}
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
// Check if dialog should be dismissed based on drag distance.
|
||||
if (mainLayout.getTranslationY() > dismissThreshold) {
|
||||
// Animate dialog off-screen and dismiss.
|
||||
//noinspection ExtractMethodRecommender
|
||||
final float remainingDistance = context.getResources().getDisplayMetrics().heightPixels
|
||||
- mainLayout.getTop();
|
||||
TranslateAnimation slideOut = new TranslateAnimation(
|
||||
0, 0, mainLayout.getTranslationY(), remainingDistance);
|
||||
slideOut.setDuration(fadeDurationFast);
|
||||
slideOut.setAnimationListener(new Animation.AnimationListener() {
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {}
|
||||
});
|
||||
mainLayout.startAnimation(slideOut);
|
||||
} else {
|
||||
// Animate back to original position if not dragged far enough.
|
||||
TranslateAnimation slideBack = new TranslateAnimation(
|
||||
0, 0, mainLayout.getTranslationY(), 0);
|
||||
slideBack.setDuration(fadeDurationFast);
|
||||
mainLayout.startAnimation(slideBack);
|
||||
mainLayout.setTranslationY(0);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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");
|
||||
});
|
||||
|
||||
dialog.show(); // Display the dialog.
|
||||
}
|
||||
|
||||
/**
|
||||
* @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. Must use double precision otherwise rounding error can occur.
|
||||
final double roundedSpeed = Math.round(speed / 0.05) * 0.05;
|
||||
return Utils.clamp((float) 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 = Utils.getDialogBackgroundColor();
|
||||
float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
|
||||
float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
|
||||
return Utils.isDarkModeEnabled()
|
||||
? Utils.adjustColorBrightness(baseColor, darkThemeFactor) // Lighten for dark theme.
|
||||
: Utils.adjustColorBrightness(baseColor, lightThemeFactor); // Darken for light theme.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(Utils.getAppForegroundColor());
|
||||
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,8 @@ public final class RememberPlaybackSpeedPatch {
|
||||
}
|
||||
Settings.PLAYBACK_SPEED_DEFAULT.save(finalPlaybackSpeed);
|
||||
|
||||
Utils.showToastLong(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
|
||||
if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get())
|
||||
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,48 @@
|
||||
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
|
||||
@@ -37,7 +74,7 @@ public class ThemePatch {
|
||||
* @return The new or original color value
|
||||
*/
|
||||
public static int getValue(int originalValue) {
|
||||
if (ThemeHelper.isDarkTheme()) {
|
||||
if (Utils.isDarkModeEnabled()) {
|
||||
if (anyEquals(originalValue, DARK_VALUES)) return BLACK_COLOR;
|
||||
} else {
|
||||
if (anyEquals(originalValue, WHITE_VALUES)) return WHITE_COLOR;
|
||||
@@ -58,4 +95,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,15 @@ import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.youtube.ThemeHelper;
|
||||
import app.revanced.extension.youtube.returnyoutubedislike.requests.RYDVoteData;
|
||||
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
@@ -177,7 +181,7 @@ public class ReturnYouTubeDislike {
|
||||
* Ideally, this would be the actual color YT uses at runtime.
|
||||
*/
|
||||
private static int getSeparatorColor() {
|
||||
return ThemeHelper.isDarkTheme()
|
||||
return Utils.isDarkModeEnabled()
|
||||
? 0x33FFFFFF
|
||||
: 0xFFD9D9D9;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import app.revanced.extension.shared.Logger;
|
||||
import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.AppLanguage;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.youtube.ThemeHelper;
|
||||
import app.revanced.extension.youtube.patches.VersionCheckPatch;
|
||||
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
|
||||
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
|
||||
@@ -27,6 +26,8 @@ import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFrag
|
||||
@SuppressWarnings("unused")
|
||||
public class LicenseActivityHook {
|
||||
|
||||
private static int currentThemeValueOrdinal = -1; // Must initially be a non-valid enum ordinal value.
|
||||
|
||||
private static ViewGroup.LayoutParams toolbarLayoutParams;
|
||||
|
||||
public static void setToolbarLayoutParams(Toolbar toolbar) {
|
||||
@@ -78,8 +79,8 @@ public class LicenseActivityHook {
|
||||
*/
|
||||
public static void initialize(Activity licenseActivity) {
|
||||
try {
|
||||
ThemeHelper.setActivityTheme(licenseActivity);
|
||||
ThemeHelper.setNavigationBarColor(licenseActivity.getWindow());
|
||||
setActivityTheme(licenseActivity);
|
||||
ReVancedPreferenceFragment.setNavigationBarColor(licenseActivity.getWindow());
|
||||
licenseActivity.setContentView(getResourceIdentifier(
|
||||
"revanced_settings_with_toolbar", "layout"));
|
||||
|
||||
@@ -114,7 +115,7 @@ public class LicenseActivityHook {
|
||||
toolBarParent.removeView(dummyToolbar);
|
||||
|
||||
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
|
||||
toolbar.setBackgroundColor(ThemeHelper.getToolbarBackgroundColor());
|
||||
toolbar.setBackgroundColor(getToolbarBackgroundColor());
|
||||
toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable());
|
||||
toolbar.setTitle(getResourceIdentifier("revanced_settings_title", "string"));
|
||||
|
||||
@@ -124,7 +125,7 @@ public class LicenseActivityHook {
|
||||
TextView toolbarTextView = Utils.getChildView(toolbar, false,
|
||||
view -> view instanceof TextView);
|
||||
if (toolbarTextView != null) {
|
||||
toolbarTextView.setTextColor(ThemeHelper.getForegroundColor());
|
||||
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
||||
}
|
||||
setToolbarLayoutParams(toolbar);
|
||||
|
||||
@@ -135,4 +136,34 @@ public class LicenseActivityHook {
|
||||
|
||||
toolBarParent.addView(toolbar, 0);
|
||||
}
|
||||
|
||||
public static void setActivityTheme(Activity activity) {
|
||||
final var theme = Utils.isDarkModeEnabled()
|
||||
? "Theme.YouTube.Settings.Dark"
|
||||
: "Theme.YouTube.Settings";
|
||||
activity.setTheme(getResourceIdentifier(theme, "style"));
|
||||
}
|
||||
|
||||
public static int getToolbarBackgroundColor() {
|
||||
final String colorName = Utils.isDarkModeEnabled()
|
||||
? "yt_black3"
|
||||
: "yt_white1";
|
||||
|
||||
return Utils.getColorFromString(colorName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Injection point.
|
||||
*
|
||||
* Updates dark/light mode since YT settings can force light/dark mode
|
||||
* which can differ from the global device settings.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static void updateLightDarkModeStatus(Enum<?> value) {
|
||||
final int themeOrdinal = value.ordinal();
|
||||
if (currentThemeValueOrdinal != themeOrdinal) {
|
||||
currentThemeValueOrdinal = themeOrdinal;
|
||||
Utils.setIsDarkModeEnabled(themeOrdinal == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,10 @@ import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.GradientDrawable;
|
||||
import android.util.Pair;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
@@ -18,6 +19,7 @@ import android.widget.SearchView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -31,7 +33,6 @@ import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.AppLanguage;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.StringSetting;
|
||||
import app.revanced.extension.youtube.ThemeHelper;
|
||||
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
|
||||
|
||||
/**
|
||||
@@ -58,11 +59,7 @@ public class SearchViewController {
|
||||
GradientDrawable background = new GradientDrawable();
|
||||
background.setShape(GradientDrawable.RECTANGLE);
|
||||
background.setCornerRadius(28 * context.getResources().getDisplayMetrics().density); // 28dp corner radius.
|
||||
int baseColor = ThemeHelper.getBackgroundColor();
|
||||
int adjustedColor = ThemeHelper.isDarkTheme()
|
||||
? ThemeHelper.adjustColorBrightness(baseColor, 1.11f) // Lighten for dark theme.
|
||||
: ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
|
||||
background.setColor(adjustedColor);
|
||||
background.setColor(getSearchViewBackground());
|
||||
return background;
|
||||
}
|
||||
|
||||
@@ -72,10 +69,17 @@ public class SearchViewController {
|
||||
private static GradientDrawable createSuggestionBackgroundDrawable(Context context) {
|
||||
GradientDrawable background = new GradientDrawable();
|
||||
background.setShape(GradientDrawable.RECTANGLE);
|
||||
background.setCornerRadius(8 * context.getResources().getDisplayMetrics().density); // 8dp corner radius.
|
||||
background.setColor(getSearchViewBackground());
|
||||
return background;
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
public static int getSearchViewBackground() {
|
||||
return Utils.isDarkModeEnabled()
|
||||
? Utils.adjustColorBrightness(Utils.getDialogBackgroundColor(), 1.11f)
|
||||
: Utils.adjustColorBrightness(Utils.getThemeLightColor(), 0.95f);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds search view components to the activity.
|
||||
*/
|
||||
@@ -171,10 +175,6 @@ public class SearchViewController {
|
||||
final int actionSearchId = getResourceIdentifier("action_search", "id");
|
||||
toolbar.inflateMenu(getResourceIdentifier("revanced_search_menu", "menu"));
|
||||
MenuItem searchItem = toolbar.getMenu().findItem(actionSearchId);
|
||||
searchItem.setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme()
|
||||
? "yt_outline_search_white_24"
|
||||
: "yt_outline_search_black_24",
|
||||
"drawable")).setTooltipText(null);
|
||||
|
||||
// Set menu item click listener.
|
||||
toolbar.setOnMenuItemClickListener(item -> {
|
||||
@@ -316,12 +316,7 @@ public class SearchViewController {
|
||||
private void closeSearch() {
|
||||
isSearchActive = false;
|
||||
toolbar.getMenu().findItem(getResourceIdentifier(
|
||||
"action_search", "id"))
|
||||
.setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme()
|
||||
? "yt_outline_search_white_24"
|
||||
: "yt_outline_search_black_24",
|
||||
"drawable")
|
||||
).setVisible(true);
|
||||
"action_search", "id")).setVisible(true);
|
||||
toolbar.setTitle(originalTitle);
|
||||
searchContainer.setVisibility(View.GONE);
|
||||
searchView.setQuery("", false);
|
||||
@@ -365,13 +360,22 @@ public class SearchViewController {
|
||||
|
||||
// Set long click listener for deletion confirmation.
|
||||
convertView.setOnLongClickListener(v -> {
|
||||
new AlertDialog.Builder(activity)
|
||||
.setTitle(query)
|
||||
.setMessage(str("revanced_settings_search_remove_message"))
|
||||
.setPositiveButton(android.R.string.ok,
|
||||
(dialog, which) -> removeSearchQuery(query))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
activity,
|
||||
query, // Title.
|
||||
str("revanced_settings_search_remove_message"), // Message.
|
||||
null, // No EditText.
|
||||
null, // OK button text.
|
||||
() -> removeSearchQuery(query), // OK button action.
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
null, // No Neutral button text.
|
||||
() -> {}, // Neutral button action (dismiss only).
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
Dialog dialog = dialogPair.first;
|
||||
dialog.setCancelable(true); // Allow dismissal via back button.
|
||||
dialog.show(); // Show the dialog.
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
@@ -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,18 +38,22 @@ 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
|
||||
public static final IntegerSetting VIDEO_QUALITY_DEFAULT_WIFI = new IntegerSetting("revanced_video_quality_default_wifi", -2);
|
||||
public static final IntegerSetting VIDEO_QUALITY_DEFAULT_MOBILE = new IntegerSetting("revanced_video_quality_default_mobile", -2);
|
||||
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", FALSE);
|
||||
public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, false,
|
||||
parent(REMEMBER_VIDEO_QUALITY_LAST_SELECTED));
|
||||
public static final IntegerSetting SHORTS_QUALITY_DEFAULT_WIFI = new IntegerSetting("revanced_shorts_quality_default_wifi", -2, true);
|
||||
public static final IntegerSetting SHORTS_QUALITY_DEFAULT_MOBILE = new IntegerSetting("revanced_shorts_quality_default_mobile", -2, true);
|
||||
public static final BooleanSetting REMEMBER_SHORTS_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_shorts_quality_last_selected", FALSE);
|
||||
@@ -60,10 +62,12 @@ public class Settings extends BaseSettings {
|
||||
// Speed
|
||||
public static final FloatSetting SPEED_TAP_AND_HOLD = new FloatSetting("revanced_speed_tap_and_hold", 2.0f, true);
|
||||
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", FALSE);
|
||||
public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, false,
|
||||
parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED));
|
||||
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 +139,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 +184,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 +231,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 +267,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 +283,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);
|
||||
@@ -284,6 +293,7 @@ public class Settings extends BaseSettings {
|
||||
public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_UPCOMING_BUTTON = new BooleanSetting("revanced_hide_shorts_upcoming_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_USE_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_use_sound_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_USE_TEMPLATE_BUTTON = new BooleanSetting("revanced_hide_shorts_use_template_button", TRUE);
|
||||
public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE);
|
||||
public static final BooleanSetting SHORTS_AUTOPLAY = new BooleanSetting("revanced_shorts_autoplay", FALSE);
|
||||
|
||||
@@ -3,17 +3,18 @@ package app.revanced.extension.youtube.settings.preference;
|
||||
import static app.revanced.extension.shared.StringRef.sf;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.ListPreference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import app.revanced.extension.shared.settings.preference.CustomDialogListPreference;
|
||||
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
||||
import app.revanced.extension.youtube.settings.Settings;
|
||||
|
||||
/**
|
||||
* A custom ListPreference that uses a styled custom dialog with a custom checkmark indicator.
|
||||
* Custom video speeds used by {@link CustomPlaybackSpeedPatch}.
|
||||
*/
|
||||
@SuppressWarnings({"unused", "deprecation"})
|
||||
public final class CustomVideoSpeedListPreference extends ListPreference {
|
||||
public final class CustomVideoSpeedListPreference extends CustomDialogListPreference {
|
||||
|
||||
/**
|
||||
* Initialize a settings preference list with the available playback speeds.
|
||||
@@ -59,4 +60,5 @@ public final class CustomVideoSpeedListPreference extends ListPreference {
|
||||
public CustomVideoSpeedListPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
@@ -40,7 +41,6 @@ import app.revanced.extension.shared.Utils;
|
||||
import app.revanced.extension.shared.settings.BaseSettings;
|
||||
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
|
||||
import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
|
||||
import app.revanced.extension.youtube.ThemeHelper;
|
||||
import app.revanced.extension.youtube.settings.LicenseActivityHook;
|
||||
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
|
||||
|
||||
@@ -71,11 +71,27 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||
|
||||
@SuppressLint("UseCompatLoadingForDrawables")
|
||||
public static Drawable getBackButtonDrawable() {
|
||||
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
|
||||
? "yt_outline_arrow_left_white_24"
|
||||
: "yt_outline_arrow_left_black_24",
|
||||
"drawable");
|
||||
return Utils.getContext().getResources().getDrawable(backButtonResource);
|
||||
final int backButtonResource = getResourceIdentifier("revanced_settings_toolbar_arrow_left", "drawable");
|
||||
Drawable drawable = Utils.getContext().getResources().getDrawable(backButtonResource);
|
||||
drawable.setTint(Utils.getAppForegroundColor());
|
||||
return drawable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the system navigation bar color for the activity.
|
||||
* Applies the background color obtained from {@link Utils#getAppBackgroundColor()} to the navigation bar.
|
||||
* For Android 10 (API 29) and above, enforces navigation bar contrast to ensure visibility.
|
||||
*/
|
||||
public static void setNavigationBarColor(@Nullable Window window) {
|
||||
if (window == null) {
|
||||
Logger.printDebug(() -> "Cannot set navigation bar color, window is null");
|
||||
return;
|
||||
}
|
||||
|
||||
window.setNavigationBarColor(Utils.getAppBackgroundColor());
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.setNavigationBarContrastEnforced(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,9 +217,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||
// Set icon for the placeholder preference.
|
||||
noResultsPreference.setLayoutResource(getResourceIdentifier(
|
||||
"revanced_preference_with_icon_no_search_result", "layout"));
|
||||
noResultsPreference.setIcon(getResourceIdentifier(
|
||||
ThemeHelper.isDarkTheme() ? "yt_outline_search_white_24" : "yt_outline_search_black_24",
|
||||
"drawable"));
|
||||
noResultsPreference.setIcon(getResourceIdentifier("revanced_settings_search_icon", "drawable"));
|
||||
preferenceScreen.addPreference(noResultsPreference);
|
||||
}
|
||||
}
|
||||
@@ -226,7 +240,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||
.getParent();
|
||||
|
||||
// Fix the system navigation bar color for submenus.
|
||||
ThemeHelper.setNavigationBarColor(preferenceScreenDialog.getWindow());
|
||||
setNavigationBarColor(preferenceScreenDialog.getWindow());
|
||||
|
||||
// Fix edge-to-edge screen with Android 15 and YT 19.45+
|
||||
// https://developer.android.com/develop/ui/views/layout/edge-to-edge#system-bars-insets
|
||||
@@ -250,7 +264,7 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||
TextView toolbarTextView = Utils.getChildView(toolbar,
|
||||
true, TextView.class::isInstance);
|
||||
if (toolbarTextView != null) {
|
||||
toolbarTextView.setTextColor(ThemeHelper.getForegroundColor());
|
||||
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
|
||||
}
|
||||
|
||||
LicenseActivityHook.setToolbarLayoutParams(toolbar);
|
||||
@@ -304,10 +318,10 @@ class AbstractPreferenceSearchData<T extends Preference> {
|
||||
return text;
|
||||
}
|
||||
|
||||
final int baseColor = ThemeHelper.getBackgroundColor();
|
||||
final int adjustedColor = ThemeHelper.isDarkTheme()
|
||||
? ThemeHelper.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme.
|
||||
: ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
|
||||
final int baseColor = Utils.getAppBackgroundColor();
|
||||
final int adjustedColor = Utils.isDarkModeEnabled()
|
||||
? Utils.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme.
|
||||
: Utils.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
|
||||
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
|
||||
|
||||
SpannableStringBuilder spannable = new SpannableStringBuilder(text);
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package app.revanced.extension.youtube.settings.preference;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference;
|
||||
import app.revanced.extension.youtube.ThemeHelper;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class ReVancedYouTubeAboutPreference extends ReVancedAboutPreference {
|
||||
|
||||
public int getLightColor() {
|
||||
return ThemeHelper.getLightThemeColor();
|
||||
}
|
||||
|
||||
public int getDarkColor() {
|
||||
return ThemeHelper.getDarkThemeColor();
|
||||
}
|
||||
|
||||
public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
}
|
||||
public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
public ReVancedYouTubeAboutPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ package app.revanced.extension.youtube.sponsorblock;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.util.Pair;
|
||||
import android.util.Patterns;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -56,7 +58,7 @@ public class SponsorBlockSettings {
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < categorySelectionsArray.length(); i++) {
|
||||
for (int i = 0, length = categorySelectionsArray.length(); i < length; i++) {
|
||||
JSONObject categorySelectionObject = categorySelectionsArray.getJSONObject(i);
|
||||
|
||||
String categoryKey = categorySelectionObject.getString("name");
|
||||
@@ -181,13 +183,25 @@ public class SponsorBlockSettings {
|
||||
// If user has a SponsorBlock user id then show a warning.
|
||||
if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateId()
|
||||
&& !Settings.SB_HIDE_EXPORT_WARNING.get()) {
|
||||
new AlertDialog.Builder(dialogContext)
|
||||
.setMessage(str("revanced_sb_settings_revanced_export_user_id_warning"))
|
||||
.setNeutralButton(str("revanced_sb_settings_revanced_export_user_id_warning_dismiss"),
|
||||
(dialog, which) -> Settings.SB_HIDE_EXPORT_WARNING.save(true))
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
dialogContext,
|
||||
null, // No title.
|
||||
str("revanced_sb_settings_revanced_export_user_id_warning"), // Message.
|
||||
null, // No EditText.
|
||||
null, // OK button text.
|
||||
() -> {}, // OK button action (dismiss only).
|
||||
null, // No cancel button action.
|
||||
str("revanced_sb_settings_revanced_export_user_id_warning_dismiss"), // Neutral button text.
|
||||
() -> Settings.SB_HIDE_EXPORT_WARNING.save(true), // Neutral button action.
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Set dialog as non-cancelable.
|
||||
dialogPair.first.setCancelable(false);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ package app.revanced.extension.youtube.sponsorblock.objects;
|
||||
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
|
||||
import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Bundle;
|
||||
@@ -15,13 +15,10 @@ import android.preference.ListPreference;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.GridLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.*;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
|
||||
@@ -52,6 +49,7 @@ public class SegmentCategoryListPreference extends ListPreference {
|
||||
private EditText dialogColorEditText;
|
||||
private EditText dialogOpacityEditText;
|
||||
private ColorPickerView dialogColorPickerView;
|
||||
private Dialog dialog;
|
||||
|
||||
public SegmentCategoryListPreference(Context context, SegmentCategory category) {
|
||||
super(context);
|
||||
@@ -75,30 +73,45 @@ public class SegmentCategoryListPreference extends ListPreference {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
protected void showDialog(Bundle state) {
|
||||
try {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
|
||||
Context context = getContext();
|
||||
categoryColor = category.getColorNoOpacity();
|
||||
categoryOpacity = category.getOpacity();
|
||||
selectedDialogEntryIndex = findIndexOfValue(getValue());
|
||||
|
||||
Context context = builder.getContext();
|
||||
LinearLayout mainLayout = new LinearLayout(context);
|
||||
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
mainLayout.setPadding(70, 0, 70, 0);
|
||||
// Create the main layout for the dialog content.
|
||||
LinearLayout contentLayout = new LinearLayout(context);
|
||||
contentLayout.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
// Add behavior selection radio buttons.
|
||||
RadioGroup radioGroup = new RadioGroup(context);
|
||||
radioGroup.setOrientation(RadioGroup.VERTICAL);
|
||||
CharSequence[] entries = getEntries();
|
||||
for (int i = 0; i < entries.length; i++) {
|
||||
RadioButton radioButton = new RadioButton(context);
|
||||
radioButton.setText(entries[i]);
|
||||
radioButton.setId(i);
|
||||
radioButton.setChecked(i == selectedDialogEntryIndex);
|
||||
radioGroup.addView(radioButton);
|
||||
}
|
||||
radioGroup.setOnCheckedChangeListener((group, checkedId) -> selectedDialogEntryIndex = checkedId);
|
||||
radioGroup.setPadding(dipToPixels(10), 0, 0, 0);
|
||||
contentLayout.addView(radioGroup);
|
||||
|
||||
// Inflate the color picker view.
|
||||
View colorPickerContainer = LayoutInflater.from(context)
|
||||
.inflate(getResourceIdentifier("revanced_color_picker", "layout"), null);
|
||||
dialogColorPickerView = colorPickerContainer.findViewById(
|
||||
getResourceIdentifier("color_picker_view", "id"));
|
||||
getResourceIdentifier("revanced_color_picker_view", "id"));
|
||||
dialogColorPickerView.setColor(categoryColor);
|
||||
mainLayout.addView(colorPickerContainer);
|
||||
contentLayout.addView(colorPickerContainer);
|
||||
|
||||
// Grid layout for color and opacity inputs.
|
||||
GridLayout gridLayout = new GridLayout(context);
|
||||
gridLayout.setColumnCount(3);
|
||||
gridLayout.setRowCount(2);
|
||||
gridLayout.setPadding(dipToPixels(16), 0, 0, 0);
|
||||
|
||||
GridLayout.LayoutParams gridParams = new GridLayout.LayoutParams();
|
||||
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
||||
@@ -111,7 +124,7 @@ public class SegmentCategoryListPreference extends ListPreference {
|
||||
gridParams = new GridLayout.LayoutParams();
|
||||
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
||||
gridParams.columnSpec = GridLayout.spec(1); // Second column.
|
||||
gridParams.setMargins(0, 0, 10, 0);
|
||||
gridParams.setMargins(0, 0, dipToPixels(10), 0);
|
||||
dialogColorDotView = new TextView(context);
|
||||
dialogColorDotView.setLayoutParams(gridParams);
|
||||
gridLayout.addView(dialogColorDotView);
|
||||
@@ -162,8 +175,7 @@ public class SegmentCategoryListPreference extends ListPreference {
|
||||
}
|
||||
}
|
||||
});
|
||||
dialogColorEditText.setLayoutParams(gridParams);
|
||||
gridLayout.addView(dialogColorEditText);
|
||||
gridLayout.addView(dialogColorEditText, gridParams);
|
||||
|
||||
gridParams = new GridLayout.LayoutParams();
|
||||
gridParams.rowSpec = GridLayout.spec(1); // Second row.
|
||||
@@ -226,11 +238,70 @@ public class SegmentCategoryListPreference extends ListPreference {
|
||||
}
|
||||
}
|
||||
});
|
||||
dialogOpacityEditText.setLayoutParams(gridParams);
|
||||
gridLayout.addView(dialogOpacityEditText);
|
||||
gridLayout.addView(dialogOpacityEditText, gridParams);
|
||||
updateOpacityText();
|
||||
|
||||
mainLayout.addView(gridLayout);
|
||||
contentLayout.addView(gridLayout);
|
||||
|
||||
// Create ScrollView to wrap the content layout.
|
||||
ScrollView contentScrollView = new ScrollView(context);
|
||||
contentScrollView.setVerticalScrollBarEnabled(false); // Disable vertical scrollbar.
|
||||
contentScrollView.setOverScrollMode(View.OVER_SCROLL_NEVER); // Disable overscroll effect.
|
||||
LinearLayout.LayoutParams scrollViewParams = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
0,
|
||||
1.0f
|
||||
);
|
||||
contentScrollView.setLayoutParams(scrollViewParams);
|
||||
contentScrollView.addView(contentLayout);
|
||||
|
||||
// Create the custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
category.title.toString(), // Title.
|
||||
null, // No message (replaced by contentLayout).
|
||||
null, // No EditText.
|
||||
null, // OK button text.
|
||||
() -> {
|
||||
// OK button action.
|
||||
if (selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
|
||||
String value = getEntryValues()[selectedDialogEntryIndex].toString();
|
||||
if (callChangeListener(value)) {
|
||||
setValue(value);
|
||||
category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
|
||||
SegmentCategory.updateEnabledCategories();
|
||||
}
|
||||
|
||||
try {
|
||||
category.setColor(dialogColorEditText.getText().toString());
|
||||
category.setOpacity(categoryOpacity);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||
}
|
||||
|
||||
updateUI();
|
||||
}
|
||||
},
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
str("revanced_settings_reset_color"), // Neutral button text.
|
||||
() -> {
|
||||
// Neutral button action (Reset).
|
||||
try {
|
||||
// Setting view color causes callback to update the UI.
|
||||
dialogColorPickerView.setColor(category.getColorNoOpacityDefault());
|
||||
|
||||
categoryOpacity = category.getOpacityDefault();
|
||||
updateOpacityText();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "resetButton onClick failure", ex);
|
||||
}
|
||||
},
|
||||
false // Do not dismiss dialog on Neutral button click.
|
||||
);
|
||||
|
||||
// Add the ScrollView to the dialog's main layout.
|
||||
LinearLayout dialogMainLayout = dialogPair.second;
|
||||
dialogMainLayout.addView(contentScrollView, dialogMainLayout.getChildCount() - 1);
|
||||
|
||||
// Set up color picker listener.
|
||||
// Do last to prevent listener callbacks while setting up view.
|
||||
@@ -247,69 +318,25 @@ public class SegmentCategoryListPreference extends ListPreference {
|
||||
dialogColorEditText.setSelection(hexColor.length());
|
||||
});
|
||||
|
||||
builder.setView(mainLayout);
|
||||
builder.setTitle(category.title.toString());
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
onClick(dialog, DialogInterface.BUTTON_POSITIVE);
|
||||
});
|
||||
builder.setNeutralButton(str("revanced_settings_reset_color"), null);
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
selectedDialogEntryIndex = findIndexOfValue(getValue());
|
||||
builder.setSingleChoiceItems(getEntries(), selectedDialogEntryIndex,
|
||||
(dialog, which) -> selectedDialogEntryIndex = which);
|
||||
// Show the dialog.
|
||||
dialog = dialogPair.first;
|
||||
dialog.show();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onPrepareDialogBuilder failure", ex);
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void showDialog(Bundle state) {
|
||||
super.showDialog(state);
|
||||
|
||||
// Do not close dialog when reset is pressed.
|
||||
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||
button.setOnClickListener(view -> {
|
||||
try {
|
||||
// Setting view color causes callback to update the UI.
|
||||
dialogColorPickerView.setColor(category.getColorNoOpacityDefault());
|
||||
|
||||
categoryOpacity = category.getOpacityDefault();
|
||||
updateOpacityText();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "setOnClickListener failure", ex);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDialogClosed(boolean positiveResult) {
|
||||
try {
|
||||
if (positiveResult && selectedDialogEntryIndex >= 0 && getEntryValues() != null) {
|
||||
String value = getEntryValues()[selectedDialogEntryIndex].toString();
|
||||
if (callChangeListener(value)) {
|
||||
setValue(value);
|
||||
category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value)));
|
||||
SegmentCategory.updateEnabledCategories();
|
||||
}
|
||||
// Nullify dialog references.
|
||||
dialogColorDotView = null;
|
||||
dialogColorEditText = null;
|
||||
dialogOpacityEditText = null;
|
||||
dialogColorPickerView = null;
|
||||
|
||||
try {
|
||||
category.setColor(dialogColorEditText.getText().toString());
|
||||
category.setOpacity(categoryOpacity);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||
}
|
||||
|
||||
updateUI();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "onDialogClosed failure", ex);
|
||||
} finally {
|
||||
dialogColorDotView = null;
|
||||
dialogColorEditText = null;
|
||||
dialogOpacityEditText = null;
|
||||
dialogColorPickerView = null;
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
dialog = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,19 +3,25 @@ package app.revanced.extension.youtube.sponsorblock.ui;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.preference.*;
|
||||
import android.os.Bundle;
|
||||
import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.preference.PreferenceGroup;
|
||||
import android.preference.SwitchPreference;
|
||||
import android.text.Html;
|
||||
import android.text.InputType;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -279,14 +285,26 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
|
||||
Boolean newValue = (Boolean) o;
|
||||
if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
|
||||
new AlertDialog.Builder(preference1.getContext())
|
||||
.setTitle(str("revanced_sb_guidelines_popup_title"))
|
||||
.setMessage(str("revanced_sb_guidelines_popup_content"))
|
||||
.setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null)
|
||||
.setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines())
|
||||
.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true))
|
||||
.setCancelable(false)
|
||||
.show();
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
preference1.getContext(),
|
||||
str("revanced_sb_guidelines_popup_title"), // Title.
|
||||
str("revanced_sb_guidelines_popup_content"), // Message.
|
||||
null, // No EditText.
|
||||
str("revanced_sb_guidelines_popup_open"), // OK button text.
|
||||
() -> openGuidelines(), // OK button action.
|
||||
null, // Cancel button action.
|
||||
str("revanced_sb_guidelines_popup_already_read"), // Neutral button text.
|
||||
() -> {}, // Neutral button action (dismiss only).
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Set dialog as non-cancelable.
|
||||
dialogPair.first.setCancelable(false);
|
||||
|
||||
dialogPair.first.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true));
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
}
|
||||
Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
|
||||
updateUI();
|
||||
@@ -372,16 +390,52 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
generalCategory.addPreference(minSegmentDuration);
|
||||
|
||||
privateUserId = new EditTextPreference(context) {
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
@Override
|
||||
protected void showDialog(Bundle state) {
|
||||
try {
|
||||
Context context = getContext();
|
||||
EditText editText = getEditText();
|
||||
|
||||
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
||||
try {
|
||||
Utils.setClipboard(getEditText().getText());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Copy settings failure", ex);
|
||||
}
|
||||
});
|
||||
// Set initial EditText value to the current persisted value or empty string.
|
||||
String initialValue = getText() != null ? getText() : "";
|
||||
editText.setText(initialValue);
|
||||
editText.setSelection(initialValue.length()); // Move cursor to end.
|
||||
|
||||
// Create custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
getTitle() != null ? getTitle().toString() : "", // Title.
|
||||
null, // Message is replaced by EditText.
|
||||
editText, // Pass the EditText.
|
||||
null, // OK button text.
|
||||
() -> {
|
||||
// OK button action. Persist the EditText value when OK is clicked.
|
||||
String newValue = editText.getText().toString();
|
||||
if (callChangeListener(newValue)) {
|
||||
setText(newValue);
|
||||
}
|
||||
},
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
str("revanced_sb_settings_copy"), // Neutral button text (Copy).
|
||||
() -> {
|
||||
// Neutral button action (Copy).
|
||||
try {
|
||||
Utils.setClipboard(getEditText().getText());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Copy settings failure", ex);
|
||||
}
|
||||
},
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Set dialog as cancelable.
|
||||
dialogPair.first.setCancelable(true);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
privateUserId.setTitle(str("revanced_sb_general_uuid"));
|
||||
@@ -407,51 +461,90 @@ public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||
editText.setText(Settings.SB_API_URL.get());
|
||||
|
||||
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
|
||||
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) {
|
||||
Settings.SB_API_URL.resetToDefault();
|
||||
Utils.showToastLong(str("revanced_sb_api_url_reset"));
|
||||
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
|
||||
String serverAddress = editText.getText().toString();
|
||||
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
|
||||
Utils.showToastLong(str("revanced_sb_api_url_invalid"));
|
||||
} else if (!serverAddress.equals(Settings.SB_API_URL.get())) {
|
||||
Settings.SB_API_URL.save(serverAddress);
|
||||
Utils.showToastLong(str("revanced_sb_api_url_changed"));
|
||||
}
|
||||
}
|
||||
};
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(apiUrl.getTitle())
|
||||
.setView(editText)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(str("revanced_settings_reset"), urlChangeListener)
|
||||
.setPositiveButton(android.R.string.ok, urlChangeListener)
|
||||
.show();
|
||||
// Create a custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
str("revanced_sb_general_api_url"), // Title.
|
||||
null, // No message, EditText replaces it.
|
||||
editText, // Pass the EditText.
|
||||
null, // OK button text.
|
||||
() -> {
|
||||
// OK button action.
|
||||
String serverAddress = editText.getText().toString();
|
||||
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
|
||||
Utils.showToastLong(str("revanced_sb_api_url_invalid"));
|
||||
} else if (!serverAddress.equals(Settings.SB_API_URL.get())) {
|
||||
Settings.SB_API_URL.save(serverAddress);
|
||||
Utils.showToastLong(str("revanced_sb_api_url_changed"));
|
||||
}
|
||||
},
|
||||
() -> {}, // Cancel button action (dismiss dialog).
|
||||
str("revanced_settings_reset"), // Neutral (Reset) button text.
|
||||
() -> {
|
||||
// Neutral button action.
|
||||
Settings.SB_API_URL.resetToDefault();
|
||||
Utils.showToastLong(str("revanced_sb_api_url_reset"));
|
||||
},
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
return true;
|
||||
});
|
||||
generalCategory.addPreference(apiUrl);
|
||||
|
||||
importExport = new EditTextPreference(context) {
|
||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||
Utils.setEditTextDialogTheme(builder);
|
||||
@Override
|
||||
protected void showDialog(Bundle state) {
|
||||
try {
|
||||
Context context = getContext();
|
||||
EditText editText = getEditText();
|
||||
|
||||
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
||||
try {
|
||||
Utils.setClipboard(getEditText().getText());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Copy settings failure", ex);
|
||||
}
|
||||
});
|
||||
// Create a custom dialog.
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
context,
|
||||
str("revanced_sb_settings_ie"), // Title.
|
||||
null, // No message, EditText replaces it.
|
||||
editText, // Pass the EditText.
|
||||
str("revanced_settings_import"), // OK button text.
|
||||
() -> {
|
||||
// OK button action. Trigger OnPreferenceChangeListener.
|
||||
String newValue = editText.getText().toString();
|
||||
if (getOnPreferenceChangeListener() != null) {
|
||||
getOnPreferenceChangeListener().onPreferenceChange(this, newValue);
|
||||
}
|
||||
},
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
str("revanced_sb_settings_copy"), // Neutral button text (Copy).
|
||||
() -> {
|
||||
// Neutral button action (Copy).
|
||||
try {
|
||||
Utils.setClipboard(editText.getText());
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "Copy settings failure", ex);
|
||||
}
|
||||
},
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
} catch (Exception ex) {
|
||||
Logger.printException(() -> "showDialog failure", ex);
|
||||
}
|
||||
}
|
||||
};
|
||||
importExport.setTitle(str("revanced_sb_settings_ie"));
|
||||
// Summary is set in updateUI()
|
||||
importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT
|
||||
// Summary is set in updateUI().
|
||||
EditText editText = importExport.getEditText();
|
||||
editText.setInputType(InputType.TYPE_CLASS_TEXT
|
||||
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
importExport.getEditText().setAutofillHints((String) null);
|
||||
importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
|
||||
editText.setAutofillHints((String) null);
|
||||
editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
|
||||
|
||||
// Set preference listeners.
|
||||
importExport.setOnPreferenceClickListener(preference1 -> {
|
||||
importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings());
|
||||
return true;
|
||||
|
||||
@@ -3,7 +3,7 @@ package app.revanced.extension.youtube.sponsorblock.ui;
|
||||
import static android.text.Html.fromHtml;
|
||||
import static app.revanced.extension.shared.StringRef.str;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
@@ -11,6 +11,8 @@ import android.preference.EditTextPreference;
|
||||
import android.preference.Preference;
|
||||
import android.preference.PreferenceCategory;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Pair;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -194,14 +196,26 @@ public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
||||
updateStatsSelfSaved.run();
|
||||
|
||||
preference.setOnPreferenceClickListener(preference1 -> {
|
||||
new AlertDialog.Builder(preference1.getContext())
|
||||
.setTitle(str("revanced_sb_stats_self_saved_reset_title"))
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
Pair<Dialog, LinearLayout> dialogPair = Utils.createCustomDialog(
|
||||
preference.getContext(),
|
||||
str("revanced_sb_stats_self_saved_reset_title"), // Title.
|
||||
null, // No message.
|
||||
null, // No EditText.
|
||||
null, // OK button text.
|
||||
() -> {
|
||||
// OK button action.
|
||||
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault();
|
||||
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault();
|
||||
updateStatsSelfSaved.run();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null).show();
|
||||
},
|
||||
() -> {}, // Cancel button action (dismiss only).
|
||||
null, // No neutral button.
|
||||
null, // No neutral button action.
|
||||
true // Dismiss dialog when onNeutralClick.
|
||||
);
|
||||
|
||||
// Show the dialog.
|
||||
dialogPair.first.show();
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
@@ -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,26 @@ 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);
|
||||
} 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.26.0-dev.1
|
||||
version = 5.29.0-dev.4
|
||||
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,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;
|
||||
}
|
||||
@@ -156,6 +160,10 @@ public final class app/revanced/patches/cieid/restrictions/root/BypassRootChecks
|
||||
public static final fun getBypassRootChecksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/crunchyroll/ads/HideAdsPatchKt {
|
||||
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/duolingo/ad/DisableAdsPatchKt {
|
||||
public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -192,6 +200,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;
|
||||
}
|
||||
@@ -284,6 +296,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;
|
||||
}
|
||||
@@ -575,6 +591,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;
|
||||
}
|
||||
@@ -593,6 +613,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 {
|
||||
@@ -677,6 +698,7 @@ public final class app/revanced/patches/shared/misc/pairip/license/DisableLicens
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/shared/misc/settings/SettingsPatchKt {
|
||||
public static final fun overrideThemeColors (Ljava/lang/String;Ljava/lang/String;)V
|
||||
public static final fun settingsPatch (Ljava/util/List;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
public static final fun settingsPatch (Lkotlin/Pair;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
public static synthetic fun settingsPatch$default (Ljava/util/List;Ljava/util/Set;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch;
|
||||
@@ -872,6 +894,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;
|
||||
}
|
||||
@@ -900,6 +926,10 @@ public final class app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginP
|
||||
public static final fun getFixFacebookLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/spotify/misc/lyrics/ChangeLyricsProviderPatchKt {
|
||||
public static final fun getChangeLyricsProviderPatch ()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;
|
||||
}
|
||||
@@ -932,6 +962,10 @@ public final class app/revanced/patches/swissid/integritycheck/RemoveGooglePlayI
|
||||
public static final fun getRemoveGooglePlayIntegrityCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/threads/HideAdsPatchKt {
|
||||
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
|
||||
public final class app/revanced/patches/ticktick/misc/themeunlock/UnlockThemePatchKt {
|
||||
public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||
}
|
||||
@@ -1653,6 +1687,7 @@ public final class app/revanced/util/BytecodeUtilsKt {
|
||||
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;F)V
|
||||
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
|
||||
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;J)V
|
||||
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/String;)V
|
||||
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V
|
||||
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V
|
||||
public static synthetic fun returnEarly$default (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ZILjava/lang/Object;)V
|
||||
@@ -1662,6 +1697,7 @@ public final class app/revanced/util/BytecodeUtilsKt {
|
||||
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;F)V
|
||||
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
|
||||
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;J)V
|
||||
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/String;)V
|
||||
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V
|
||||
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V
|
||||
public static final fun transformMethods (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)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,7 @@
|
||||
package app.revanced.patches.crunchyroll.ads
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
internal val videoUrlReadyToStringFingerprint = fingerprint {
|
||||
strings("VideoUrlReady(url=", ", enableAds=")
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package app.revanced.patches.crunchyroll.ads
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.instructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstruction
|
||||
import app.revanced.util.removeFlags
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
|
||||
|
||||
@Suppress("unused")
|
||||
val hideAdsPatch = bytecodePatch(
|
||||
name = "Hide Ads"
|
||||
) {
|
||||
compatibleWith("com.crunchyroll.crunchyroid")
|
||||
|
||||
execute {
|
||||
// Get obfuscated "enableAds" field from toString method.
|
||||
val enableAdsField = videoUrlReadyToStringFingerprint.let {
|
||||
val strIndex = videoUrlReadyToStringFingerprint.stringMatches!!.last().index
|
||||
val fieldIndex = it.method.indexOfFirstInstruction(strIndex, Opcode.IGET_BOOLEAN)
|
||||
it.method.getInstruction<ReferenceInstruction>(fieldIndex).getReference<FieldReference>()!!
|
||||
}
|
||||
|
||||
// Remove final access flag on field.
|
||||
videoUrlReadyToStringFingerprint.classDef.fields
|
||||
.first { it.name == enableAdsField.name }
|
||||
.removeFlags(AccessFlags.FINAL)
|
||||
|
||||
// Override enableAds field in non-default constructor.
|
||||
val constructor = videoUrlReadyToStringFingerprint.classDef.methods.first {
|
||||
AccessFlags.CONSTRUCTOR.isSet(it.accessFlags) && it.parameters.isNotEmpty()
|
||||
}
|
||||
constructor.addInstructions(
|
||||
constructor.instructions.count() - 1,
|
||||
"""
|
||||
move-object/from16 v0, p0
|
||||
const/4 v1, 0x0
|
||||
iput-boolean v1, v0, $enableAdsField
|
||||
""")
|
||||
}
|
||||
}
|
||||
@@ -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,7 +1,7 @@
|
||||
package app.revanced.patches.idaustria.detection.signature
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Suppress("unused")
|
||||
val spoofSignaturePatch = bytecodePatch(
|
||||
@@ -24,12 +24,6 @@ val spoofSignaturePatch = bytecodePatch(
|
||||
"77ef1be61b2c01ebdabddcbf53cc4b6fd9a3c445606ee77b3758162c80ad8f8137b3c6864e92db904807dcb2be9d7717dd21" +
|
||||
"bf42c121d620ddfb7914f7a95c713d9e1c1b7bdb4a03d618e40cf7e9e235c0b5687e03b7ab3,publicExponent=10001}"
|
||||
|
||||
spoofSignatureFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "$expectedSignature"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
spoofSignatureFingerprint.method.returnEarly(expectedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
package app.revanced.patches.instagram.ads
|
||||
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.meta.ads.adInjectorFingerprint
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Deprecated("Patch was moved to different package: app.revanced.patches.meta.ads.hideAdsPatch")
|
||||
@Suppress("unused")
|
||||
val hideAdsPatch = bytecodePatch {
|
||||
dependsOn(app.revanced.patches.meta.ads.hideAdsPatch)
|
||||
val hideAdsPatch = bytecodePatch(
|
||||
name = "Hide ads",
|
||||
) {
|
||||
compatibleWith("com.instagram.android")
|
||||
|
||||
execute {
|
||||
adInjectorFingerprint.method.returnEarly(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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,20 @@ 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;")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal val metaAIKillSwitchCheckFingerprint = fingerprint {
|
||||
strings("SearchAiagentImplementationsKillSwitch")
|
||||
opcodes(Opcode.CONST_WIDE)
|
||||
}
|
||||
|
||||
internal val extensionMethodFingerprint = fingerprint {
|
||||
strings("REPLACED_BY_PATCH")
|
||||
custom { method, classDef ->
|
||||
method.name == EXTENSION_METHOD_NAME && classDef.type == EXTENSION_CLASS_DESCRIPTOR
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ package app.revanced.patches.messenger.metaai
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.messenger.misc.extension.sharedExtensionPatch
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
|
||||
|
||||
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/messenger/metaai/RemoveMetaAIPatch;"
|
||||
internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/messenger/metaai/RemoveMetaAIPatch;"
|
||||
internal const val EXTENSION_METHOD_NAME = "overrideBooleanFlag"
|
||||
|
||||
@Suppress("unused")
|
||||
val removeMetaAIPatch = bytecodePatch(
|
||||
@@ -25,10 +28,25 @@ 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->$EXTENSION_METHOD_NAME(JZ)Z
|
||||
move-result v$returnRegister
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
// Extract the common starting digits of Meta AI flag IDs from a flag found in code.
|
||||
val relevantDigits = with(metaAIKillSwitchCheckFingerprint) {
|
||||
method.getInstruction<WideLiteralInstruction>(patternMatch!!.startIndex).wideLiteral
|
||||
}.toString().substring(0, 7)
|
||||
|
||||
// Replace placeholder in the extension method.
|
||||
with(extensionMethodFingerprint) {
|
||||
method.replaceInstruction(
|
||||
stringMatches!!.first().index,
|
||||
"""
|
||||
const-string v1, "$relevantDigits"
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", messengerApplicationOnCreateHook)
|
||||
|
||||
@@ -2,6 +2,8 @@ package app.revanced.patches.messenger.misc.extension
|
||||
|
||||
import app.revanced.patches.shared.misc.extension.extensionHook
|
||||
|
||||
internal val mainActivityOnCreateHook = extensionHook {
|
||||
strings("MainActivity_onCreate_begin")
|
||||
internal val messengerApplicationOnCreateHook = extensionHook {
|
||||
custom { method, classDef ->
|
||||
method.name == "onCreate" && classDef.endsWith("/MessengerApplication;")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,18 +3,9 @@ package app.revanced.patches.meta.ads
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Deprecated("Instead use the Instagram or Threads specific hide ads patch")
|
||||
@Suppress("unused")
|
||||
val hideAdsPatch = bytecodePatch(
|
||||
name = "Hide ads",
|
||||
) {
|
||||
/**
|
||||
* Patch is identical for both Instagram and Threads app.
|
||||
*/
|
||||
compatibleWith(
|
||||
"com.instagram.android",
|
||||
"com.instagram.barcelona",
|
||||
)
|
||||
|
||||
val hideAdsPatch = bytecodePatch {
|
||||
execute {
|
||||
adInjectorFingerprint.method.returnEarly(false)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.revanced.patches.nunl.firebase
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Suppress("unused")
|
||||
val spoofCertificatePatch = bytecodePatch(
|
||||
@@ -12,13 +12,7 @@ val spoofCertificatePatch = bytecodePatch(
|
||||
|
||||
execute {
|
||||
getFingerprintHashForPackageFingerprints.forEach { fingerprint ->
|
||||
fingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "eae41fc018df2731a9b6ae1ac327da44a288667b"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
fingerprint.method.returnEarly("eae41fc018df2731a9b6ae1ac327da44a288667b")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
package app.revanced.patches.pandora.ads
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.pandora.shared.constructUserDataFingerprint
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Suppress("unused")
|
||||
val disableAudioAdsPatch = bytecodePatch(
|
||||
@@ -15,16 +10,7 @@ val disableAudioAdsPatch = bytecodePatch(
|
||||
compatibleWith("com.pandora.android")
|
||||
|
||||
execute {
|
||||
constructUserDataFingerprint.method.apply {
|
||||
// First match is "hasAudioAds".
|
||||
val hasAudioAdsStringIndex = constructUserDataFingerprint.stringMatches!!.first().index
|
||||
val moveResultIndex = indexOfFirstInstructionOrThrow(hasAudioAdsStringIndex, Opcode.MOVE_RESULT)
|
||||
val hasAudioAdsRegister = getInstruction<OneRegisterInstruction>(moveResultIndex).registerA
|
||||
|
||||
addInstruction(
|
||||
moveResultIndex + 1,
|
||||
"const/4 v$hasAudioAdsRegister, 0"
|
||||
)
|
||||
}
|
||||
getIsAdSupportedFingerprint.method.returnEarly(false)
|
||||
requestAudioAdFingerprint.method.returnEarly()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package app.revanced.patches.pandora.ads
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
internal val getIsAdSupportedFingerprint = fingerprint {
|
||||
custom { method, classDef ->
|
||||
method.name == "getIsAdSupported" && classDef.endsWith("UserData;")
|
||||
}
|
||||
}
|
||||
|
||||
internal val requestAudioAdFingerprint = fingerprint {
|
||||
custom { method, classDef ->
|
||||
method.name == "requestAudioAdFromAdSDK" && classDef.endsWith("ContentServiceOpsImpl;")
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,7 @@
|
||||
package app.revanced.patches.pandora.misc
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.pandora.shared.constructUserDataFingerprint
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import com.android.tools.smali.dexlib2.Opcode
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Suppress("unused")
|
||||
val enableUnlimitedSkipsPatch = bytecodePatch(
|
||||
@@ -15,17 +10,6 @@ val enableUnlimitedSkipsPatch = bytecodePatch(
|
||||
compatibleWith("com.pandora.android")
|
||||
|
||||
execute {
|
||||
constructUserDataFingerprint.method.apply {
|
||||
// Last match is "skipLimitBehavior".
|
||||
val skipLimitBehaviorStringIndex = constructUserDataFingerprint.stringMatches!!.last().index
|
||||
val moveResultObjectIndex =
|
||||
indexOfFirstInstructionOrThrow(skipLimitBehaviorStringIndex, Opcode.MOVE_RESULT_OBJECT)
|
||||
val skipLimitBehaviorRegister = getInstruction<OneRegisterInstruction>(moveResultObjectIndex).registerA
|
||||
|
||||
addInstruction(
|
||||
moveResultObjectIndex + 1,
|
||||
"const-string v$skipLimitBehaviorRegister, \"unlimited\""
|
||||
)
|
||||
}
|
||||
skipLimitBehaviorFingerprint.method.returnEarly("unlimited")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package app.revanced.patches.pandora.misc
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
internal val skipLimitBehaviorFingerprint = fingerprint {
|
||||
custom { method, classDef ->
|
||||
method.name == "getSkipLimitBehavior" && classDef.endsWith("UserData;")
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package app.revanced.patches.pandora.shared
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
|
||||
internal val constructUserDataFingerprint = fingerprint {
|
||||
strings("hasAudioAds", "skipLimitBehavior")
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package app.revanced.patches.photomath.detection.deviceid
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patches.photomath.detection.signature.signatureDetectionPatch
|
||||
import app.revanced.util.returnEarly
|
||||
import kotlin.random.Random
|
||||
|
||||
@Suppress("unused")
|
||||
@@ -15,12 +15,6 @@ val getDeviceIdPatch = bytecodePatch(
|
||||
compatibleWith("com.microblink.photomath")
|
||||
|
||||
execute {
|
||||
getDeviceIdFingerprint.method.replaceInstructions(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "${Random.nextLong().toString(16)}"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
getDeviceIdFingerprint.method.returnEarly(Random.nextLong().toString(16))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package app.revanced.patches.piccomafr.misc
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.patch.stringOption
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
@Suppress("unused")
|
||||
val spoofAndroidDeviceIdPatch = bytecodePatch(
|
||||
@@ -39,12 +40,6 @@ val spoofAndroidDeviceIdPatch = bytecodePatch(
|
||||
) { it!!.matches("[A-Fa-f0-9]{16}".toRegex()) }
|
||||
|
||||
execute {
|
||||
getAndroidIdFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "$androidDeviceId"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
getAndroidIdFingerprint.method.returnEarly(androidDeviceId!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package app.revanced.patches.reddit.customclients.boostforreddit.api
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patches.reddit.customclients.spoofClientPatch
|
||||
import app.revanced.util.returnEarly
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
|
||||
val spoofClientPatch = spoofClientPatch(redirectUri = "http://rubenmayayo.com") { clientIdOption ->
|
||||
@@ -14,13 +14,7 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "http://rubenmayayo.com")
|
||||
execute {
|
||||
// region Patch client id.
|
||||
|
||||
getClientIdFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "$clientId"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
getClientIdFingerprint.method.returnEarly(clientId!!)
|
||||
|
||||
// endregion
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
package app.revanced.patches.reddit.customclients.joeyforreddit.api
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions
|
||||
import app.revanced.patches.reddit.customclients.joeyforreddit.detection.piracy.disablePiracyDetectionPatch
|
||||
import app.revanced.patches.reddit.customclients.spoofClientPatch
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
val spoofClientPatch = spoofClientPatch(redirectUri = "https://127.0.0.1:65023/authorize_callback") { clientIdOption ->
|
||||
dependsOn(disablePiracyDetectionPatch)
|
||||
@@ -19,13 +18,7 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "https://127.0.0.1:65023/a
|
||||
execute {
|
||||
// region Patch client id.
|
||||
|
||||
getClientIdFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "$clientId"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
getClientIdFingerprint.method.returnEarly(clientId!!)
|
||||
|
||||
// endregion
|
||||
|
||||
@@ -35,13 +28,7 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "https://127.0.0.1:65023/a
|
||||
val randomName = (0..100000).random()
|
||||
val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)"
|
||||
|
||||
authUtilityUserAgentFingerprint.method.replaceInstructions(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "$userAgent"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
authUtilityUserAgentFingerprint.method.returnEarly(userAgent)
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ package app.revanced.patches.reddit.customclients.redditisfun.api
|
||||
|
||||
import app.revanced.patcher.Fingerprint
|
||||
import app.revanced.patcher.Match
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patches.reddit.customclients.spoofClientPatch
|
||||
import app.revanced.util.getReference
|
||||
import app.revanced.util.indexOfFirstInstructionOrThrow
|
||||
import app.revanced.util.returnEarly
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
||||
|
||||
@@ -54,13 +54,7 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "redditisfun://auth") { cl
|
||||
val randomName = (0..100000).random()
|
||||
val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)"
|
||||
|
||||
getUserAgentFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "$userAgent"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
getUserAgentFingerprint.method.returnEarly(userAgent)
|
||||
|
||||
// endregion
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package app.revanced.patches.reddit.customclients.slide.api
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patches.reddit.customclients.spoofClientPatch
|
||||
import app.revanced.util.returnEarly
|
||||
|
||||
val spoofClientPatch = spoofClientPatch(redirectUri = "http://www.ccrama.me") { clientIdOption ->
|
||||
compatibleWith("me.ccrama.redditslide")
|
||||
@@ -9,12 +9,6 @@ val spoofClientPatch = spoofClientPatch(redirectUri = "http://www.ccrama.me") {
|
||||
val clientId by clientIdOption
|
||||
|
||||
execute {
|
||||
getClientIdFingerprint.method.addInstructions(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "$clientId"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
getClientIdFingerprint.method.returnEarly(clientId!!)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
package app.revanced.patches.reddit.customclients.sync.syncforreddit.api
|
||||
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
||||
import app.revanced.patches.reddit.customclients.spoofClientPatch
|
||||
import app.revanced.patches.reddit.customclients.sync.detection.piracy.disablePiracyDetectionPatch
|
||||
import app.revanced.util.returnEarly
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
|
||||
import com.android.tools.smali.dexlib2.iface.reference.StringReference
|
||||
import java.util.*
|
||||
import java.util.Base64
|
||||
|
||||
val spoofClientPatch = spoofClientPatch(
|
||||
redirectUri = "http://redditsync/auth",
|
||||
@@ -28,13 +28,8 @@ val spoofClientPatch = spoofClientPatch(
|
||||
|
||||
getBearerTokenFingerprint.match(getAuthorizationStringFingerprint.originalClassDef).method.apply {
|
||||
val auth = Base64.getEncoder().encodeToString("$clientId:".toByteArray(Charsets.UTF_8))
|
||||
addInstructions(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "Basic $auth"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
returnEarly("Basic $auth")
|
||||
|
||||
val occurrenceIndex =
|
||||
getAuthorizationStringFingerprint.stringMatches!!.first().index
|
||||
|
||||
@@ -63,23 +58,19 @@ val spoofClientPatch = spoofClientPatch(
|
||||
val randomName = (0..100000).random()
|
||||
val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)"
|
||||
|
||||
getUserAgentFingerprint.method.replaceInstruction(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "$userAgent"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
getUserAgentFingerprint.method.returnEarly(userAgent)
|
||||
|
||||
// endregion
|
||||
|
||||
// region Patch Imgur API URL.
|
||||
|
||||
val apiUrlIndex = imgurImageAPIFingerprint.stringMatches!!.first().index
|
||||
imgurImageAPIFingerprint.method.replaceInstruction(
|
||||
apiUrlIndex,
|
||||
"const-string v1, \"https://api.imgur.com/3/image\"",
|
||||
)
|
||||
imgurImageAPIFingerprint.let {
|
||||
val apiUrlIndex = it.stringMatches!!.first().index
|
||||
it.method.replaceInstruction(
|
||||
apiUrlIndex,
|
||||
"const-string v1, \"https://api.imgur.com/3/image\"",
|
||||
)
|
||||
}
|
||||
|
||||
// endregion
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -84,7 +84,7 @@ fun checkEnvironmentPatch(
|
||||
|
||||
fun invokeCheck() = mainActivityOnCreateFingerprint.method.addInstruction(
|
||||
0,
|
||||
"invoke-static/range { p0 .. p0 },$EXTENSION_CLASS_DESCRIPTOR->check(Landroid/app/Activity;)V",
|
||||
"invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->check(Landroid/app/Activity;)V",
|
||||
)
|
||||
|
||||
setPatchInfo()
|
||||
|
||||
@@ -3,11 +3,11 @@ package app.revanced.patches.shared.misc.extension
|
||||
import app.revanced.patcher.Fingerprint
|
||||
import app.revanced.patcher.FingerprintBuilder
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
|
||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.patcher.patch.BytecodePatchContext
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.util.returnEarly
|
||||
import com.android.tools.smali.dexlib2.iface.Method
|
||||
import java.net.URLDecoder
|
||||
import java.util.jar.JarFile
|
||||
@@ -80,14 +80,7 @@ fun sharedExtensionPatch(
|
||||
}
|
||||
|
||||
val manifestValue = getPatchesManifestEntry("Version")
|
||||
|
||||
addInstructions(
|
||||
0,
|
||||
"""
|
||||
const-string v0, "$manifestValue"
|
||||
return-object v0
|
||||
""",
|
||||
)
|
||||
returnEarly(manifestValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,23 @@
|
||||
package app.revanced.patches.shared.misc.settings
|
||||
|
||||
import app.revanced.patcher.fingerprint
|
||||
import app.revanced.patches.shared.misc.extension.EXTENSION_CLASS_DESCRIPTOR
|
||||
import com.android.tools.smali.dexlib2.AccessFlags
|
||||
|
||||
internal val themeLightColorResourceNameFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC)
|
||||
returns("Ljava/lang/String;")
|
||||
parameters()
|
||||
custom { method, classDef ->
|
||||
method.name == "getThemeLightColorResourceName" && classDef.type == EXTENSION_CLASS_DESCRIPTOR
|
||||
}
|
||||
}
|
||||
|
||||
internal val themeDarkColorResourceNameFingerprint = fingerprint {
|
||||
accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC)
|
||||
returns("Ljava/lang/String;")
|
||||
parameters()
|
||||
custom { method, classDef ->
|
||||
method.name == "getThemeDarkColorResourceName" && classDef.type == EXTENSION_CLASS_DESCRIPTOR
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package app.revanced.patches.shared.misc.settings
|
||||
|
||||
import app.revanced.patcher.patch.PatchException
|
||||
import app.revanced.patcher.patch.bytecodePatch
|
||||
import app.revanced.patcher.patch.resourcePatch
|
||||
import app.revanced.patches.all.misc.resources.addResource
|
||||
import app.revanced.patches.all.misc.resources.addResources
|
||||
@@ -13,6 +14,7 @@ import app.revanced.util.ResourceGroup
|
||||
import app.revanced.util.copyResources
|
||||
import app.revanced.util.getNode
|
||||
import app.revanced.util.insertFirst
|
||||
import app.revanced.util.returnEarly
|
||||
import org.w3c.dom.Node
|
||||
|
||||
// TODO: Delete this on next major version bump.
|
||||
@@ -22,6 +24,30 @@ fun settingsPatch (
|
||||
preferences: Set<BasePreference>,
|
||||
) = settingsPatch(listOf(rootPreference), preferences)
|
||||
|
||||
private var themeForegroundColor : String? = null
|
||||
private var themeBackgroundColor : String? = null
|
||||
|
||||
/**
|
||||
* Sets the default theme colors used in various ReVanced specific settings menus.
|
||||
* By default these colors are white and black, but instead can be set to the
|
||||
* same color the target app uses for it's own settings.
|
||||
*/
|
||||
fun overrideThemeColors(foregroundColor: String, backgroundColor: String) {
|
||||
themeForegroundColor = foregroundColor
|
||||
themeBackgroundColor = backgroundColor
|
||||
}
|
||||
|
||||
private val settingsColorPatch = bytecodePatch {
|
||||
finalize {
|
||||
if (themeForegroundColor != null) {
|
||||
themeLightColorResourceNameFingerprint.method.returnEarly(themeForegroundColor!!)
|
||||
}
|
||||
if (themeBackgroundColor != null) {
|
||||
themeDarkColorResourceNameFingerprint.method.returnEarly(themeBackgroundColor!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A resource patch that adds settings to a settings fragment.
|
||||
*
|
||||
@@ -33,12 +59,28 @@ fun settingsPatch (
|
||||
rootPreferences: List<Pair<BasePreference, String>>? = null,
|
||||
preferences: Set<BasePreference>,
|
||||
) = resourcePatch {
|
||||
dependsOn(addResourcesPatch)
|
||||
dependsOn(addResourcesPatch, settingsColorPatch)
|
||||
|
||||
execute {
|
||||
copyResources(
|
||||
"settings",
|
||||
ResourceGroup("xml", "revanced_prefs.xml", "revanced_prefs_icons.xml"),
|
||||
ResourceGroup("drawable",
|
||||
// CustomListPreference resources.
|
||||
"revanced_ic_dialog_alert.xml",
|
||||
"revanced_settings_arrow_time.xml",
|
||||
"revanced_settings_circle_background.xml",
|
||||
"revanced_settings_cursor.xml",
|
||||
"revanced_settings_custom_checkmark.xml",
|
||||
"revanced_settings_search_icon.xml",
|
||||
"revanced_settings_toolbar_arrow_left.xml",
|
||||
),
|
||||
ResourceGroup("layout",
|
||||
"revanced_custom_list_item_checked.xml",
|
||||
// Color picker.
|
||||
"revanced_color_dot_widget.xml",
|
||||
"revanced_color_picker.xml",
|
||||
)
|
||||
)
|
||||
|
||||
addResources("shared", "misc.settings.settingsResourcePatch")
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.w3c.dom.Document
|
||||
* @param summaryKey The preference summary key.
|
||||
* @param icon The preference icon resource name.
|
||||
* @param layout Layout declaration.
|
||||
* @param tag The preference tag.
|
||||
* @param tag The preference class type.
|
||||
* @param entriesKey The entries array key.
|
||||
* @param entryValuesKey The entry values array key.
|
||||
*/
|
||||
@@ -20,10 +20,12 @@ import org.w3c.dom.Document
|
||||
class ListPreference(
|
||||
key: String? = null,
|
||||
titleKey: String = "${key}_title",
|
||||
summaryKey: String? = "${key}_summary",
|
||||
/** Summary key is ignored and will be removed soon */
|
||||
//@Deprecated
|
||||
summaryKey: String? = null,
|
||||
icon: String? = null,
|
||||
layout: String? = null,
|
||||
tag: String = "ListPreference",
|
||||
tag: String = "app.revanced.extension.shared.settings.preference.CustomDialogListPreference",
|
||||
val entriesKey: String? = "${key}_entries",
|
||||
val entryValuesKey: String? = "${key}_entry_values"
|
||||
) : BasePreference(key, titleKey, summaryKey, icon, layout, tag) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user