Compare commits

...

181 Commits

Author SHA1 Message Date
semantic-release-bot
2d8f5641f9 chore: Release v5.29.0-dev.3 [skip ci]
# [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 ([0dbd058](0dbd058099))
2025-06-23 09:18:27 +00:00
LisoUseInAIKyrios
0dbd058099 fix(YouTube): Fix refactoring app startup exception 2025-06-23 13:15:43 +04:00
semantic-release-bot
c1a8fd0766 chore: Release v5.29.0-dev.2 [skip ci]
# [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)) ([d338989](d338989cb4))
2025-06-23 08:47:04 +00:00
hoodles
d338989cb4 feat(Crunchyroll): Add Hide ads patch (#5201) 2025-06-23 12:44:07 +04:00
github-actions[bot]
b94daacf01 chore: Sync translations (#5236) 2025-06-23 12:43:45 +04:00
semantic-release-bot
c764c4f197 chore: Release v5.29.0-dev.1 [skip ci]
# [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)) ([ccd1691](ccd169121a))

### Features

* **YouTube:** Add an option to disable toasts when changing default playback speed or quality ([#5230](https://github.com/ReVanced/revanced-patches/issues/5230)) ([6b719df](6b719dfcd7))
2025-06-23 08:24:13 +00:00
MarcaD
6b719dfcd7 feat(YouTube): Add an option to disable toasts when changing default playback speed or quality (#5230) 2025-06-23 12:20:37 +04:00
LisoUseInAIKyrios
ccd169121a fix(YouTube): Always use single threaded layout to resolve layout bugs in unpatched YouTube (#5226) 2025-06-23 12:19:07 +04:00
semantic-release-bot
dcfbd8bf93 chore: Release v5.28.1-dev.2 [skip ci]
## [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)) ([b656976](b65697603d))
2025-06-23 07:19:38 +00:00
ILoveOpenSourceApplications
b65697603d fix(YouTube - Hide ads): Hide new type of product ad in video description (#5225) 2025-06-23 11:17:08 +04:00
semantic-release-bot
25da5cca8b chore: Release v5.28.1-dev.1 [skip ci]
## [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)) ([2b62fc2](2b62fc2224))
2025-06-22 11:23:52 +00:00
MarcaD
2b62fc2224 fix: Add scrollable content to modern style settings dialogs (#5211) 2025-06-22 15:21:00 +04:00
semantic-release-bot
a9e9456b6b chore: Release v5.28.0 [skip ci]
# [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 ([1cea6bf](1cea6bfdff))
* **Messenger - Remove Meta AI:** Improve patch logic ([#5153](https://github.com/ReVanced/revanced-patches/issues/5153)) ([a8d2a1e](a8d2a1e028))
* **Pandora - Disable ads:** Support latest app target ([#5185](https://github.com/ReVanced/revanced-patches/issues/5185)) ([90868ff](90868ff025))
* **Spotify:** Fix `Hide Create button` and `Sanitize sharing links` for older but supported app targets ([#5159](https://github.com/ReVanced/revanced-patches/issues/5159)) ([5540136](55401368b8))
* **Threads - Hide ads:** Constrain patch to the last working app target ([#5189](https://github.com/ReVanced/revanced-patches/issues/5189)) ([e138501](e138501657))
* **YouTube:** Remove old app targets that are no longer supported by YouTube ([#5192](https://github.com/ReVanced/revanced-patches/issues/5192)) ([e790cfb](e790cfbf59))

### Features

* **Spotify:** Add `Change lyrics provider` patch ([#4937](https://github.com/ReVanced/revanced-patches/issues/4937)) ([7bbaca7](7bbaca77ad))
* Use modern style settings dialogs ([#5109](https://github.com/ReVanced/revanced-patches/issues/5109)) ([a426e2a](a426e2af50))
2025-06-20 08:17:57 +00:00
LisoUseInAIKyrios
b01523e97d chore: Merge branch dev to main (#5160) 2025-06-20 12:14:29 +04:00
github-actions[bot]
b8afb4e821 chore: Sync translations (#5210) 2025-06-20 12:13:39 +04:00
github-actions[bot]
0d2198faed chore: Sync translations (#5207) 2025-06-20 01:33:20 +04:00
semantic-release-bot
5c7c407b82 chore: Release v5.28.0-dev.8 [skip ci]
# [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)) ([a8d2a1e](a8d2a1e028))
2025-06-19 06:10:06 +00:00
Dawid Krajcarz
a8d2a1e028 fix(Messenger - Remove Meta AI): Improve patch logic (#5153) 2025-06-19 10:06:46 +04:00
semantic-release-bot
d31624cae8 chore: Release v5.28.0-dev.7 [skip ci]
# [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)) ([e790cfb](e790cfbf59))
2025-06-18 10:38:43 +00:00
LisoUseInAIKyrios
e790cfbf59 fix(YouTube): Remove old app targets that are no longer supported by YouTube (#5192) 2025-06-18 12:35:43 +02:00
LisoUseInAIKyrios
a54d408d3e chore: Fix string typos, fix missing long/wide returnEarly/returnLate 2025-06-18 11:06:48 +02:00
semantic-release-bot
5d3769e921 chore: Release v5.28.0-dev.6 [skip ci]
# [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)) ([e138501](e138501657))
2025-06-17 21:07:00 +00:00
LisoUseInAIKyrios
e138501657 fix(Threads - Hide ads): Constrain patch to the last working app target (#5189) 2025-06-17 23:03:35 +02:00
semantic-release-bot
a9235d6b62 chore: Release v5.28.0-dev.5 [skip ci]
# [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)) ([90868ff](90868ff025))
2025-06-17 06:47:11 +00:00
hoodles
90868ff025 fix(Pandora - Disable ads): Support latest app target (#5185) 2025-06-17 08:44:19 +02:00
github-actions[bot]
345ec5c430 chore: Sync translations (#5188) 2025-06-17 08:43:55 +02:00
semantic-release-bot
c8b95d475c chore: Release v5.28.0-dev.4 [skip ci]
# [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)) ([a426e2a](a426e2af50))
2025-06-13 07:33:27 +00:00
github-actions[bot]
0a93f44a5e chore: Sync translations (#5167) 2025-06-13 09:30:30 +02:00
MarcaD
a426e2af50 feat: Use modern style settings dialogs (#5109)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-06-13 09:29:13 +02:00
semantic-release-bot
9e30c34e74 chore: Release v5.28.0-dev.3 [skip ci]
# [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)) ([5540136](55401368b8))
2025-06-11 20:23:04 +00:00
Nuckyz
55401368b8 fix(Spotify): Fix Hide Create button and Sanitize sharing links for older but supported app targets (#5159) 2025-06-11 17:20:19 -03:00
LisoUseInAIKyrios
c0c56fef23 chore: Fix debug logging if context is not set 2025-06-11 19:57:37 +02:00
semantic-release-bot
69df47602f chore: Release v5.28.0-dev.2 [skip ci]
# [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 ([1cea6bf](1cea6bfdff))
2025-06-11 17:44:07 +00:00
LisoUseInAIKyrios
1cea6bfdff fix(Google Photos): Resolve startup crash if MicroG GmsCore does not already have granted permissions 2025-06-11 19:40:37 +02:00
semantic-release-bot
e2a9552f91 chore: Release v5.28.0-dev.1 [skip ci]
# [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)) ([7bbaca7](7bbaca77ad))
2025-06-11 08:28:44 +00:00
brosssh
7bbaca77ad feat(Spotify): Add Change lyrics provider patch (#4937) 2025-06-11 10:25:58 +02:00
semantic-release-bot
246f3efe55 chore: Release v5.27.0 [skip ci]
# [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)) ([c0448de](c0448dece4))
* **Spotify:** `Hide Create button` patch failing in edge cases ([#5131](https://github.com/ReVanced/revanced-patches/issues/5131)) ([7a432e5](7a432e5741))
* **Spotify:** Prevent hiding all navigation bar buttons ([#5122](https://github.com/ReVanced/revanced-patches/issues/5122)) ([65fc6b4](65fc6b43f5))
* **YouTube - Hide layout components:** Remove broken option 'Hide comments emoji picker' ([#5121](https://github.com/ReVanced/revanced-patches/issues/5121)) ([4b8499f](4b8499ff2c))
* **YouTube - Hide Shorts components:** Disable A/B player flags that prevents hiding buttons ([9d10ab6](9d10ab6c00))
* **YouTube - Video quality:** Remove non-functional Shorts 144p default quality ([6aff8e8](6aff8e8ca4))

### Features

* Add `Hide app icon` patch ([#4977](https://github.com/ReVanced/revanced-patches/issues/4977)) ([6127f48](6127f48a9e))
* **Google Photos:** Add `Enable DCIM folders backup control` patch ([#5138](https://github.com/ReVanced/revanced-patches/issues/5138)) ([af827e2](af827e2f1a))
* **Messenger:** Add `Hide Facebook button` patch ([#5057](https://github.com/ReVanced/revanced-patches/issues/5057)) ([ed0d807](ed0d807d70))
* **YouTube - Hide player overlay buttons:** Add in app setting for "Hide player control buttons background" ([#5147](https://github.com/ReVanced/revanced-patches/issues/5147)) ([adfac8a](adfac8a1f2))
* **YouTube - Hide Shorts components:** Add hide 'New posts' button ([f8e31c8](f8e31c820a))
* **YouTube - Theme:** Add option for black and white splash screen animation ([#5119](https://github.com/ReVanced/revanced-patches/issues/5119)) ([6d5380d](6d5380d44d))
2025-06-09 18:37:59 +00:00
LisoUseInAIKyrios
0fca3e8fb1 chore: Merge branch dev to main (#5115) 2025-06-09 20:35:14 +02:00
semantic-release-bot
c09255eaed chore: Release v5.27.0-dev.9 [skip ci]
# [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)) ([ed0d807](ed0d807d70))
2025-06-09 18:18:19 +00:00
github-actions[bot]
e78d6240ea chore: Sync translations (#5151) 2025-06-09 20:15:28 +02:00
Dawid Krajcarz
ed0d807d70 feat(Messenger): Add Hide Facebook button patch (#5057) 2025-06-09 20:13:05 +02:00
semantic-release-bot
1c39004350 chore: Release v5.27.0-dev.8 [skip ci]
# [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)) ([6127f48](6127f48a9e))
2025-06-09 14:11:00 +00:00
ILoveOpenSourceApplications
6127f48a9e feat: Add Hide app icon patch (#4977) 2025-06-09 11:07:43 -03:00
semantic-release-bot
ad416f4aa7 chore: Release v5.27.0-dev.7 [skip ci]
# [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)) ([adfac8a](adfac8a1f2))
2025-06-08 18:07:53 +00:00
Nuckyz
adfac8a1f2 feat(YouTube - Hide player overlay buttons): Add in app setting for "Hide player control buttons background" (#5147) 2025-06-08 15:04:58 -03:00
semantic-release-bot
498488d45b chore: Release v5.27.0-dev.6 [skip ci]
# [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 ([f8e31c8](f8e31c820a))
2025-06-08 12:06:28 +00:00
LisoUseInAIKyrios
f8e31c820a feat(YouTube - Hide Shorts components): Add hide 'New posts' button 2025-06-08 14:03:00 +02:00
semantic-release-bot
826a391591 chore: Release v5.27.0-dev.5 [skip ci]
# [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)) ([af827e2](af827e2f1a))
2025-06-08 07:17:04 +00:00
Nuckyz
af827e2f1a feat(Google Photos): Add Enable DCIM folders backup control patch (#5138) 2025-06-08 09:13:53 +02:00
semantic-release-bot
97cd31509e chore: Release v5.27.0-dev.4 [skip ci]
# [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)) ([c0448de](c0448dece4))
2025-06-06 21:22:31 +00:00
hoodles
c0448dece4 fix(Bandcamp - Remove play limits): Support latest app version (#5124) 2025-06-06 23:20:00 +02:00
semantic-release-bot
f00a95c0d8 chore: Release v5.27.0-dev.3 [skip ci]
# [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)) ([7a432e5](7a432e5741))
2025-06-06 21:14:18 +00:00
Nuckyz
7a432e5741 fix(Spotify): Hide Create button patch failing in edge cases (#5131) 2025-06-06 18:11:32 -03:00
semantic-release-bot
966a78bd81 chore: Release v5.27.0-dev.2 [skip ci]
# [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 ([6aff8e8](6aff8e8ca4))
2025-06-06 07:06:49 +00:00
LisoUseInAIKyrios
6aff8e8ca4 fix(YouTube - Video quality): Remove non-functional Shorts 144p default quality 2025-06-06 09:04:07 +02:00
github-actions[bot]
11aa463fa6 chore: Sync translations (#5125) 2025-06-06 09:03:38 +02:00
semantic-release-bot
bf1b639a2f chore: Release v5.27.0-dev.1 [skip ci]
# [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)) ([6d5380d](6d5380d44d))
2025-06-05 20:08:58 +00:00
LisoUseInAIKyrios
6d5380d44d feat(YouTube - Theme): Add option for black and white splash screen animation (#5119) 2025-06-05 22:06:12 +02:00
github-actions[bot]
7e1547b5b9 chore: Sync translations (#5123) 2025-06-05 22:05:56 +02:00
semantic-release-bot
c790b45cc5 chore: Release v5.26.1-dev.3 [skip ci]
## [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)) ([65fc6b4](65fc6b43f5))
2025-06-05 19:57:52 +00:00
Nuckyz
65fc6b43f5 fix(Spotify): Prevent hiding all navigation bar buttons (#5122) 2025-06-05 16:55:33 -03:00
semantic-release-bot
2257dd90aa chore: Release v5.26.1-dev.2 [skip ci]
## [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)) ([4b8499f](4b8499ff2c))
2025-06-05 19:12:08 +00:00
LisoUseInAIKyrios
4b8499ff2c fix(YouTube - Hide layout components): Remove broken option 'Hide comments emoji picker' (#5121) 2025-06-05 21:09:25 +02:00
Nuckyz
bde3fda972 refactor(Spotify): Add extensions debug logging (#5110) 2025-06-05 16:04:02 -03:00
semantic-release-bot
e2e07b5cb2 chore: Release v5.26.1-dev.1 [skip ci]
## [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 ([9d10ab6](9d10ab6c00))
2025-06-05 09:24:29 +00:00
LisoUseInAIKyrios
9d10ab6c00 fix(YouTube - Hide Shorts components): Disable A/B player flags that prevents hiding buttons 2025-06-05 11:21:05 +02:00
github-actions[bot]
d7644152fd chore: Sync translations (#5116) 2025-06-05 11:19:38 +02:00
LisoUseInAIKyrios
9be21f4824 refactor(YouTube - Hide Shorts components): Rename 'Hide comment panel' to 'Hide preview comment' 2025-06-05 11:08:24 +02:00
semantic-release-bot
a2eae0bf04 chore: Release v5.26.0 [skip ci]
# [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)) ([bc45433](bc45433dcb))
* **YouTube - Hide Shorts components:** Disable A/B player that prevents hiding buttons ([#5104](https://github.com/ReVanced/revanced-patches/issues/5104)) ([1d0c568](1d0c56819b))
* **YouTube:** Support A/B Shorts layout for RYD and component hiding ([#5081](https://github.com/ReVanced/revanced-patches/issues/5081)) ([ff903ba](ff903ba9ac))

### Features

* **Proton Mail:** Add `Remove free accounts limit` patch ([#4970](https://github.com/ReVanced/revanced-patches/issues/4970)) ([49ae0df](49ae0df224))
* **Spotify:** Add `Hide Create button` patch ([#5062](https://github.com/ReVanced/revanced-patches/issues/5062)) ([ce5385b](ce5385b28e))
* **Sync for Reddit:** Add `Fix post thumbnails` patch ([7a53580](7a53580380))
* **YouTube - Hide Shorts components:** Add option to hide comment panel ([#5102](https://github.com/ReVanced/revanced-patches/issues/5102)) ([e435b33](e435b33593))
* **YouTube - Playback Speed:** Use modern custom speed dialog ([#5069](https://github.com/ReVanced/revanced-patches/issues/5069)) ([a320e35](a320e35c32))
2025-06-04 14:05:56 +00:00
LisoUseInAIKyrios
679354b5b3 chore: Merge branch dev to main (#5079) 2025-06-04 16:02:42 +02:00
semantic-release-bot
91dec21033 chore: Release v5.26.0-dev.8 [skip ci]
# [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)) ([1d0c568](1d0c56819b))
2025-06-04 11:48:26 +00:00
LisoUseInAIKyrios
1d0c56819b fix(YouTube - Hide Shorts components): Disable A/B player that prevents hiding buttons (#5104) 2025-06-04 13:45:48 +02:00
github-actions[bot]
4410816c22 chore: Sync translations (#5106) 2025-06-04 13:44:35 +02:00
semantic-release-bot
7e4e48bc9f chore: Release v5.26.0-dev.7 [skip ci]
# [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)) ([e435b33](e435b33593))
2025-06-04 07:09:39 +00:00
LisoUseInAIKyrios
e435b33593 feat(YouTube - Hide Shorts components): Add option to hide comment panel (#5102) 2025-06-04 09:06:27 +02:00
semantic-release-bot
bf288b83ae chore: Release v5.26.0-dev.6 [skip ci]
# [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 ([7a53580](7a53580380))
2025-06-03 19:50:57 +00:00
kolpazar
7a53580380 feat(Sync for Reddit): Add Fix post thumbnails patch 2025-06-03 21:48:24 +02:00
semantic-release-bot
6439efa2a9 chore: Release v5.26.0-dev.5 [skip ci]
# [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)) ([bc45433](bc45433dcb))
2025-06-03 08:04:58 +00:00
Cilly Leang
bc45433dcb fix(Spotify - Custom theme): Apply accent color in more places (#5039)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2025-06-03 10:02:15 +02:00
github-actions[bot]
8871803e83 chore: Sync translations (#5095) 2025-06-03 09:59:31 +02:00
semantic-release-bot
18954a0285 chore: Release v5.26.0-dev.4 [skip ci]
# [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)) ([ce5385b](ce5385b28e))
2025-06-03 07:18:30 +00:00
Nuckyz
ce5385b28e feat(Spotify): Add Hide Create button patch (#5062) 2025-06-03 09:15:52 +02:00
dependabot[bot]
3f4cdf6f83 chore(deps-dev): bump semantic-release from 24.2.1 to 24.2.5 (#5086) 2025-06-01 20:57:25 +02:00
semantic-release-bot
094b4a1ea8 chore: Release v5.26.0-dev.3 [skip ci]
# [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)) ([a320e35](a320e35c32))
2025-06-01 09:15:51 +00:00
MarcaD
a320e35c32 feat(YouTube - Playback Speed): Use modern custom speed dialog (#5069)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-06-01 11:12:56 +02:00
semantic-release-bot
5bf5a2d2db chore: Release v5.26.0-dev.2 [skip ci]
# [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)) ([ff903ba](ff903ba9ac))
2025-06-01 07:01:35 +00:00
alieRN
ff903ba9ac fix(YouTube): Support A/B Shorts layout for RYD and component hiding (#5081) 2025-06-01 08:59:09 +02:00
github-actions[bot]
1079a54dbe chore: Sync translations (#5084) 2025-06-01 08:58:37 +02:00
ILoveOpenSourceApplications
2b0e3b4553 refactor: Make strings consistent (#5075) 2025-05-31 10:25:04 +02:00
semantic-release-bot
0265a7791b chore: Release v5.26.0-dev.1 [skip ci]
# [5.26.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.25.0...v5.26.0-dev.1) (2025-05-30)

### Features

* **Proton Mail:** Add `Remove free accounts limit` patch ([#4970](https://github.com/ReVanced/revanced-patches/issues/4970)) ([49ae0df](49ae0df224))
2025-05-30 20:53:45 +00:00
ByteEVM
49ae0df224 feat(Proton Mail): Add Remove free accounts limit patch (#4970)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2025-05-30 22:51:12 +02:00
semantic-release-bot
e279491724 chore: Release v5.25.0 [skip ci]
# [5.25.0](https://github.com/ReVanced/revanced-patches/compare/v5.24.0...v5.25.0) (2025-05-29)

### Bug Fixes

* **Disable Pairip license check:** Change patch to default off ([798596f](798596fd83))
* **Hide ADB status:** Resolve app crash on startup ([#5029](https://github.com/ReVanced/revanced-patches/issues/5029)) ([2984d73](2984d7362d))
* **Messenger:** Remove outdated `Disable switching emoji to sticker` patch ([#5044](https://github.com/ReVanced/revanced-patches/issues/5044)) ([517368e](517368eda7))
* **Spotify Lite:** Remove obsolete `Enable on demand` patch ([#5046](https://github.com/ReVanced/revanced-patches/issues/5046)) ([b712f38](b712f38017))
* **YouTube - GmsCore support:** Restore patch functionality from prior merge ([b6047fa](b6047fa6b3))
* **YouTube - Hide ads:** Hide new type of general ad ([#5004](https://github.com/ReVanced/revanced-patches/issues/5004)) ([bc0c3c4](bc0c3c452d))
* **YouTube - Open Shorts in regular player:** Do not exit app when pressing back button in regular player ([#5020](https://github.com/ReVanced/revanced-patches/issues/5020)) ([4ab1f0c](4ab1f0cfa9))
* **YouTube:** Better handle incorrect duplicate translations ([8d61ba9](8d61ba90c3))
* **Yuka - Unlock premium:** Remove broken patch that is no longer supported ([#5018](https://github.com/ReVanced/revanced-patches/issues/5018)) ([e286dab](e286dab74e))

### Features

* Add `Disable pairip license check` patch ([#4927](https://github.com/ReVanced/revanced-patches/issues/4927)) ([dea7108](dea7108c45))
* **Messenger:** Add `Remove Meta AI` patch ([#4945](https://github.com/ReVanced/revanced-patches/issues/4945)) ([52b9dc5](52b9dc5c9f))
* **Prime Video:** Add `Rename shared permissions` patch ([#5049](https://github.com/ReVanced/revanced-patches/issues/5049)) ([02373b0](02373b0bd2))
* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([795016a](795016abce))
* **Threads:** Hide Ads ([#5064](https://github.com/ReVanced/revanced-patches/issues/5064)) ([bf1f26d](bf1f26d8bb))
* **YouTube - Enable debugging:** Add settings menu to share debug logs ([#5021](https://github.com/ReVanced/revanced-patches/issues/5021)) ([83c148a](83c148addc))
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([584b00f](584b00fd87))
* **YouTube - Swipe controls:** Add separate color settings for the brightness and volume bars ([#5043](https://github.com/ReVanced/revanced-patches/issues/5043)) ([443b54b](443b54bf09))
* **YouTube:** Add `Disable haptic feedback` patch ([#5033](https://github.com/ReVanced/revanced-patches/issues/5033)) ([5c8ed05](5c8ed05727))
2025-05-29 07:26:20 +00:00
LisoUseInAIKyrios
495260fe2b chore: Merge branch dev to main (#5010) 2025-05-29 09:22:53 +02:00
github-actions[bot]
40f069fff7 chore: Sync translations (#5066) 2025-05-29 09:20:59 +02:00
semantic-release-bot
de263c1061 chore: Release v5.25.0-dev.14 [skip ci]
# [5.25.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.13...v5.25.0-dev.14) (2025-05-29)

### Features

* **Threads:** Hide Ads ([#5064](https://github.com/ReVanced/revanced-patches/issues/5064)) ([bf1f26d](bf1f26d8bb))
2025-05-29 07:17:42 +00:00
scruz
bf1f26d8bb feat(Threads): Hide Ads (#5064) 2025-05-29 09:14:24 +02:00
semantic-release-bot
0ee2ed72d4 chore: Release v5.25.0-dev.13 [skip ci]
# [5.25.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.12...v5.25.0-dev.13) (2025-05-28)

### Features

* **Prime Video:** Add `Rename shared permissions` patch ([#5049](https://github.com/ReVanced/revanced-patches/issues/5049)) ([02373b0](02373b0bd2))
2025-05-28 20:41:45 +00:00
hoodles
02373b0bd2 feat(Prime Video): Add Rename shared permissions patch (#5049) 2025-05-28 22:38:09 +02:00
github-actions[bot]
97c8e2489d chore: Sync translations (#5061) 2025-05-28 22:37:18 +02:00
semantic-release-bot
08b2b2e104 chore: Release v5.25.0-dev.12 [skip ci]
# [5.25.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.11...v5.25.0-dev.12) (2025-05-28)

### Features

* **YouTube - Swipe controls:** Add separate color settings for the brightness and volume bars ([#5043](https://github.com/ReVanced/revanced-patches/issues/5043)) ([443b54b](443b54bf09))
2025-05-28 10:02:34 +00:00
github-actions[bot]
6b386b67d2 chore: Sync translations (#5055) 2025-05-28 11:58:56 +02:00
Nuckyz
f8343ae9f6 refactor(Spotify): Improve protobuf array list mutability patch (#5053)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2025-05-28 11:55:25 +02:00
LisoUseInAIKyrios
3ba791ac7d refactor(YouTube - Enabling debugging): Adjust logger formatting to preserve backwards compatibility (#5054) 2025-05-28 11:55:02 +02:00
MarcaD
443b54bf09 feat(YouTube - Swipe controls): Add separate color settings for the brightness and volume bars (#5043) 2025-05-28 11:54:28 +02:00
semantic-release-bot
53587f190d chore: Release v5.25.0-dev.11 [skip ci]
# [5.25.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.10...v5.25.0-dev.11) (2025-05-27)

### Features

* **YouTube - Enable debugging:** Add settings menu to share debug logs ([#5021](https://github.com/ReVanced/revanced-patches/issues/5021)) ([83c148a](83c148addc))
* **YouTube:** Add `Disable haptic feedback` patch ([#5033](https://github.com/ReVanced/revanced-patches/issues/5033)) ([5c8ed05](5c8ed05727))
2025-05-27 07:55:39 +00:00
MarcaD
83c148addc feat(YouTube - Enable debugging): Add settings menu to share debug logs (#5021)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-05-27 09:52:24 +02:00
MarcaD
5c8ed05727 feat(YouTube): Add Disable haptic feedback patch (#5033) 2025-05-27 09:52:01 +02:00
semantic-release-bot
33833d7a1e chore: Release v5.25.0-dev.10 [skip ci]
# [5.25.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.9...v5.25.0-dev.10) (2025-05-27)

### Bug Fixes

* **Messenger:** Remove outdated `Disable switching emoji to sticker` patch ([#5044](https://github.com/ReVanced/revanced-patches/issues/5044)) ([517368e](517368eda7))
* **Spotify Lite:** Remove obsolete `Enable on demand` patch ([#5046](https://github.com/ReVanced/revanced-patches/issues/5046)) ([b712f38](b712f38017))
2025-05-27 06:51:05 +00:00
LisoUseInAIKyrios
b712f38017 fix(Spotify Lite): Remove obsolete Enable on demand patch (#5046) 2025-05-27 08:47:56 +02:00
LisoUseInAIKyrios
517368eda7 fix(Messenger): Remove outdated Disable switching emoji to sticker patch (#5044) 2025-05-27 08:47:09 +02:00
LisoUseInAIKyrios
2093c0c175 chore: fix api dump 2025-05-26 12:36:16 +02:00
semantic-release-bot
a7cfd80bfe chore: Release v5.25.0-dev.9 [skip ci]
# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26)

### Features

* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([795016a](795016abce))
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([584b00f](584b00fd87))
2025-05-26 09:48:51 +00:00
github-actions[bot]
2990dc6d4e chore: Sync translations (#5038) 2025-05-26 10:39:02 +02:00
semantic-release-bot
c0e52bb6b3 chore: Release v5.25.0-dev.9 [skip ci]
# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26)

### Features

* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([795016a](795016abce))
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([584b00f](584b00fd87))
2025-05-26 07:53:26 +00:00
github-actions[bot]
93fdd6f538 chore: Sync translations (#5037) 2025-05-26 09:49:50 +02:00
semantic-release-bot
decd249f20 chore: Release v5.25.0-dev.9 [skip ci]
# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26)

### Features

* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([795016a](795016abce))
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([584b00f](584b00fd87))
2025-05-26 07:29:23 +00:00
github-actions[bot]
d79cb3eea8 chore: Sync translations (#5036) 2025-05-26 09:25:54 +02:00
MarcaD
584b00fd87 feat(YouTube - Settings): Add a color picker (#4981)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-05-26 09:08:45 +02:00
Nuckyz
795016abce feat(Spotify): Add Fix Facebook login patch (#5023) 2025-05-26 09:08:15 +02:00
semantic-release-bot
dc1dbd50a8 chore: Release v5.25.0-dev.8 [skip ci]
# [5.25.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.7...v5.25.0-dev.8) (2025-05-25)

### Bug Fixes

* **Hide ADB status:** Resolve app crash on startup ([#5029](https://github.com/ReVanced/revanced-patches/issues/5029)) ([2984d73](2984d7362d))
2025-05-25 17:41:52 +00:00
1fexd
2984d7362d fix(Hide ADB status): Resolve app crash on startup (#5029) 2025-05-25 19:38:19 +02:00
semantic-release-bot
627aed4010 chore: Release v5.25.0-dev.7 [skip ci]
# [5.25.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.6...v5.25.0-dev.7) (2025-05-24)

### Bug Fixes

* **YouTube - Open Shorts in regular player:** Do not exit app when pressing back button in regular player ([#5020](https://github.com/ReVanced/revanced-patches/issues/5020)) ([4ab1f0c](4ab1f0cfa9))
2025-05-24 11:16:03 +00:00
LisoUseInAIKyrios
4ab1f0cfa9 fix(YouTube - Open Shorts in regular player): Do not exit app when pressing back button in regular player (#5020) 2025-05-24 13:12:39 +02:00
semantic-release-bot
86e8e61ab2 chore: Release v5.25.0-dev.6 [skip ci]
# [5.25.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.5...v5.25.0-dev.6) (2025-05-23)

### Bug Fixes

* **Yuka - Unlock premium:** Remove broken patch that is no longer supported ([#5018](https://github.com/ReVanced/revanced-patches/issues/5018)) ([e286dab](e286dab74e))
2025-05-23 19:58:47 +00:00
LisoUseInAIKyrios
e286dab74e fix(Yuka - Unlock premium): Remove broken patch that is no longer supported (#5018) 2025-05-23 21:55:04 +02:00
github-actions[bot]
712a82439f chore: Sync translations (#5019) 2025-05-23 21:54:47 +02:00
semantic-release-bot
4449546c85 chore: Release v5.25.0-dev.5 [skip ci]
# [5.25.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.4...v5.25.0-dev.5) (2025-05-22)

### Bug Fixes

* **YouTube:** Better handle incorrect duplicate translations ([8d61ba9](8d61ba90c3))
2025-05-22 19:23:25 +00:00
LisoUseInAIKyrios
8d61ba90c3 fix(YouTube): Better handle incorrect duplicate translations 2025-05-22 21:20:01 +02:00
semantic-release-bot
689be79f71 chore: Release v5.25.0-dev.4 [skip ci]
# [5.25.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.3...v5.25.0-dev.4) (2025-05-22)

### Bug Fixes

* **YouTube - GmsCore support:** Restore patch functionality from prior merge ([b6047fa](b6047fa6b3))
2025-05-22 17:28:53 +00:00
LisoUseInAIKyrios
b6047fa6b3 fix(YouTube - GmsCore support): Restore patch functionality from prior merge 2025-05-22 19:25:15 +02:00
semantic-release-bot
82bbd603ac chore: Release v5.25.0-dev.3 [skip ci]
# [5.25.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.2...v5.25.0-dev.3) (2025-05-22)

### Bug Fixes

* **YouTube - Hide ads:** Hide new type of general ad ([#5004](https://github.com/ReVanced/revanced-patches/issues/5004)) ([bc0c3c4](bc0c3c452d))
2025-05-22 16:38:22 +00:00
ILoveOpenSourceApplications
bc0c3c452d fix(YouTube - Hide ads): Hide new type of general ad (#5004) 2025-05-22 18:35:11 +02:00
Pun Butrach
fe864d8331 ci: Attest release artifacts (#4816)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2025-05-22 14:56:33 +02:00
semantic-release-bot
4f686935c3 chore: Release v5.25.0-dev.2 [skip ci]
# [5.25.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.1...v5.25.0-dev.2) (2025-05-22)

### Bug Fixes

* **Disable Pairip license check:** Change patch to default off ([798596f](798596fd83))
2025-05-22 10:18:30 +00:00
LisoUseInAIKyrios
798596fd83 fix(Disable Pairip license check): Change patch to default off 2025-05-22 12:14:33 +02:00
semantic-release-bot
38b37f182a chore: Release v5.25.0-dev.1 [skip ci]
# [5.25.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.24.0...v5.25.0-dev.1) (2025-05-22)

### Features

* Add `Disable pairip license check` patch ([#4927](https://github.com/ReVanced/revanced-patches/issues/4927)) ([dea7108](dea7108c45))
* **Messenger:** Add `Remove Meta AI` patch ([#4945](https://github.com/ReVanced/revanced-patches/issues/4945)) ([52b9dc5](52b9dc5c9f))
2025-05-22 08:39:21 +00:00
Dawid Krajcarz
52b9dc5c9f feat(Messenger): Add Remove Meta AI patch (#4945) 2025-05-22 10:35:31 +02:00
hoodles
dea7108c45 feat: Add Disable pairip license check patch (#4927)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-05-22 10:35:16 +02:00
github-actions[bot]
24b4579cb9 chore: Sync translations (#5009) 2025-05-22 10:33:36 +02:00
semantic-release-bot
0b52f3d192 chore: Release v5.24.0 [skip ci]
# [5.24.0](https://github.com/ReVanced/revanced-patches/compare/v5.23.0...v5.24.0) (2025-05-19)

### Bug Fixes

* **Spotify - Fix third party launchers widgets:** Add missing compatibility annotation ([e68cd70](e68cd70f66))
* **YouTube - Hide layout components:** Fix `Hide video recommendation labels` ([#4956](https://github.com/ReVanced/revanced-patches/issues/4956)) ([9aec199](9aec1999bb))
* **YouTube - Settings:** Correctly show summary text if search box is closed before searching ([e59c9e9](e59c9e9b3c))
* **YouTube - SponsorBlock:** Fix segment category summary not showing category description ([b2b09a2](b2b09a2025))

### Features

* **GmsCore support:** Open vendor specific DontKillMyApp if available ([#4952](https://github.com/ReVanced/revanced-patches/issues/4952)) ([d2b440d](d2b440d800))
* **NU.nl:** Support version `11.3.0` ([#4925](https://github.com/ReVanced/revanced-patches/issues/4925)) ([887c9f0](887c9f0d75))
* **Spotify:** Add `Fix third party launchers widgets` patch ([#4893](https://github.com/ReVanced/revanced-patches/issues/4893)) ([db68c41](db68c41d5e))
* **YouTube - Hide description components:** Add `Hide Ask` ([#4972](https://github.com/ReVanced/revanced-patches/issues/4972)) ([e582908](e58290839f))
* **YouTube - Hide layout components:** Add `Hide ticket shelf` ([#4969](https://github.com/ReVanced/revanced-patches/issues/4969)) ([4cd0ae9](4cd0ae9b92))
* **YouTube - Hide player components:** Hide related video overlay in fullscreen ([#4938](https://github.com/ReVanced/revanced-patches/issues/4938)) ([f454183](f454183646))
* **YouTube - Settings:** Add ability to search in settings ([#4881](https://github.com/ReVanced/revanced-patches/issues/4881)) ([14a8f4f](14a8f4fb96))
2025-05-19 10:32:36 +00:00
LisoUseInAIKyrios
18c374a81e chore: Merge branch dev to main (#4947) 2025-05-19 14:28:47 +04:00
github-actions[bot]
092303e431 chore: Sync translations (#4995) 2025-05-19 14:25:25 +04:00
semantic-release-bot
6bf5bf9d45 chore: Release v5.24.0-dev.9 [skip ci]
# [5.24.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.8...v5.24.0-dev.9) (2025-05-18)

### Bug Fixes

* **YouTube - SponsorBlock:** Fix segment category summary not showing category description ([b2b09a2](b2b09a2025))
2025-05-18 08:29:10 +00:00
LisoUseInAIKyrios
b2b09a2025 fix(YouTube - SponsorBlock): Fix segment category summary not showing category description 2025-05-18 12:25:43 +04:00
semantic-release-bot
4a3a7f1674 chore: Release v5.24.0-dev.8 [skip ci]
# [5.24.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.7...v5.24.0-dev.8) (2025-05-17)

### Bug Fixes

* **YouTube - Settings:** Correctly show summary text if search box is closed before searching ([e59c9e9](e59c9e9b3c))
2025-05-17 18:00:22 +00:00
LisoUseInAIKyrios
e59c9e9b3c fix(YouTube - Settings): Correctly show summary text if search box is closed before searching 2025-05-17 21:57:03 +04:00
github-actions[bot]
dfb552b01a chore: Sync translations (#4978) 2025-05-17 21:56:48 +04:00
github-actions[bot]
94999c56b1 chore: Sync translations (#4975) 2025-05-17 15:19:14 +04:00
semantic-release-bot
c4fd1f0146 chore: Release v5.24.0-dev.7 [skip ci]
# [5.24.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.6...v5.24.0-dev.7) (2025-05-17)

### Features

* **YouTube - Hide layout components:** Add `Hide ticket shelf` ([#4969](https://github.com/ReVanced/revanced-patches/issues/4969)) ([4cd0ae9](4cd0ae9b92))
2025-05-17 10:51:31 +00:00
ILoveOpenSourceApplications
4cd0ae9b92 feat(YouTube - Hide layout components): Add Hide ticket shelf (#4969) 2025-05-17 14:48:05 +04:00
github-actions[bot]
9548d581c1 chore: Sync translations (#4974) 2025-05-17 14:46:10 +04:00
LisoUseInAIKyrios
a2fe3af6be refactor(YouTube - Litho filter): Simplify debug logging code 2025-05-17 12:26:37 +04:00
semantic-release-bot
6ef6504d41 chore: Release v5.24.0-dev.6 [skip ci]
# [5.24.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.5...v5.24.0-dev.6) (2025-05-17)

### Features

* **YouTube - Hide description components:** Add `Hide Ask` ([#4972](https://github.com/ReVanced/revanced-patches/issues/4972)) ([e582908](e58290839f))
2025-05-17 07:56:03 +00:00
ILoveOpenSourceApplications
e58290839f feat(YouTube - Hide description components): Add Hide Ask (#4972) 2025-05-17 11:52:33 +04:00
github-actions[bot]
e18260bd65 chore: Sync translations (#4973) 2025-05-17 11:46:53 +04:00
semantic-release-bot
b2fcd5a846 chore: Release v5.24.0-dev.5 [skip ci]
# [5.24.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.4...v5.24.0-dev.5) (2025-05-17)

### Bug Fixes

* **Spotify - Fix third party launchers widgets:** Add missing compatibility annotation ([e68cd70](e68cd70f66))

### Features

* **YouTube - Settings:** Add ability to search in settings ([#4881](https://github.com/ReVanced/revanced-patches/issues/4881)) ([14a8f4f](14a8f4fb96))
2025-05-17 07:44:42 +00:00
LisoUseInAIKyrios
e68cd70f66 fix(Spotify - Fix third party launchers widgets): Add missing compatibility annotation 2025-05-17 11:40:37 +04:00
MarcaD
14a8f4fb96 feat(YouTube - Settings): Add ability to search in settings (#4881)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-05-17 11:40:16 +04:00
semantic-release-bot
2593c004f4 chore: Release v5.24.0-dev.4 [skip ci]
# [5.24.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.3...v5.24.0-dev.4) (2025-05-16)

### Features

* **Spotify:** Add `Fix third party launchers widgets` patch ([#4893](https://github.com/ReVanced/revanced-patches/issues/4893)) ([db68c41](db68c41d5e))
2025-05-16 17:18:15 +00:00
Nuckyz
db68c41d5e feat(Spotify): Add Fix third party launchers widgets patch (#4893)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-05-16 19:14:26 +02:00
semantic-release-bot
a4f9cb3cef chore: Release v5.24.0-dev.3 [skip ci]
# [5.24.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.2...v5.24.0-dev.3) (2025-05-14)

### Bug Fixes

* **YouTube - Hide layout components:** Fix `Hide video recommendation labels` ([#4956](https://github.com/ReVanced/revanced-patches/issues/4956)) ([9aec199](9aec1999bb))
2025-05-14 20:01:05 +00:00
ILoveOpenSourceApplications
9aec1999bb fix(YouTube - Hide layout components): Fix Hide video recommendation labels (#4956) 2025-05-14 23:57:47 +04:00
github-actions[bot]
26ecbe646e chore: Sync translations (#4960) 2025-05-14 23:57:11 +04:00
semantic-release-bot
46ba0d8a2e chore: Release v5.24.0-dev.2 [skip ci]
# [5.24.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.1...v5.24.0-dev.2) (2025-05-14)

### Features

* **GmsCore support:** Open vendor specific DontKillMyApp if available ([#4952](https://github.com/ReVanced/revanced-patches/issues/4952)) ([d2b440d](d2b440d800))
* **YouTube - Hide player components:** Hide related video overlay in fullscreen ([#4938](https://github.com/ReVanced/revanced-patches/issues/4938)) ([f454183](f454183646))
2025-05-14 07:07:03 +00:00
MarcaD
f454183646 feat(YouTube - Hide player components): Hide related video overlay in fullscreen (#4938) 2025-05-14 11:03:33 +04:00
LisoUseInAIKyrios
d2b440d800 feat(GmsCore support): Open vendor specific DontKillMyApp if available (#4952) 2025-05-14 11:01:32 +04:00
hoodles
494c5f04a4 fix(Instagram) - Fix hide ads fingerprint and add bypass check signature (#4901)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2025-05-13 10:58:07 +04:00
semantic-release-bot
48d5fdf7e1 chore: Release v5.24.0-dev.1 [skip ci]
# [5.24.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.23.0...v5.24.0-dev.1) (2025-05-12)

### Features

* **NU.nl:** Support version `11.3.0` ([#4925](https://github.com/ReVanced/revanced-patches/issues/4925)) ([887c9f0](887c9f0d75))
2025-05-12 22:40:55 +00:00
Jasper Abbink
887c9f0d75 feat(NU.nl): Support version 11.3.0 (#4925) 2025-05-13 02:37:41 +04:00
github-actions[bot]
7de4c9d41d chore: Sync translations (#4946) 2025-05-13 02:36:53 +04:00
semantic-release-bot
7d3b8d9c42 chore: Release v5.23.0 [skip ci]
# [5.23.0](https://github.com/ReVanced/revanced-patches/compare/v5.22.0...v5.23.0) (2025-05-10)

### Bug Fixes

* Correct incorrect fingerprint ([5f05414](5f0541407c))
* Fix incorrect fingerprints ([#4917](https://github.com/ReVanced/revanced-patches/issues/4917)) ([796c118](796c118fe1))
* **Spotify - Unlock Spotify Premium:** Remove pop up premium ads ([#4842](https://github.com/ReVanced/revanced-patches/issues/4842)) ([5028c1a](5028c1acb3))
* **YouTube:** Improve litho filtering performance ([#4904](https://github.com/ReVanced/revanced-patches/issues/4904)) ([60fdf4c](60fdf4c44c))
* **YouTube:** Simplify litho filtering patch ([#4910](https://github.com/ReVanced/revanced-patches/issues/4910)) ([23fd720](23fd720fa7))

### Features

* **Lightroom:** Constrain patches to last working version ([858c59d](858c59d728))
* **Pandora:** Add `Disable audio ads` and `Unlimited skips` patch ([#4841](https://github.com/ReVanced/revanced-patches/issues/4841)) ([f4f36ff](f4f36ff273))
* **Prime Video:** Add `Skip ads` patch ([#4824](https://github.com/ReVanced/revanced-patches/issues/4824)) ([f8bdf74](f8bdf744ab))
* **Spotify:** Add `Sanitize sharing links` patch ([#4829](https://github.com/ReVanced/revanced-patches/issues/4829)) ([777957e](777957e2d0))
2025-05-10 09:02:41 +00:00
LisoUseInAIKyrios
25e1a965d6 chore: Merge branch dev to main (#4899) 2025-05-10 12:58:55 +04:00
github-actions[bot]
b29c01cee1 chore: Sync translations (#4933) 2025-05-10 12:39:30 +04:00
semantic-release-bot
639850471b chore: Release v5.23.0-dev.7 [skip ci]
# [5.23.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.6...v5.23.0-dev.7) (2025-05-06)

### Bug Fixes

* Fix incorrect fingerprints ([#4917](https://github.com/ReVanced/revanced-patches/issues/4917)) ([796c118](796c118fe1))
2025-05-06 12:13:29 +00:00
Nuckyz
796c118fe1 fix: Fix incorrect fingerprints (#4917) 2025-05-06 16:09:54 +04:00
semantic-release-bot
edf20e397d chore: Release v5.23.0-dev.6 [skip ci]
# [5.23.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.5...v5.23.0-dev.6) (2025-05-06)

### Bug Fixes

* Correct incorrect fingerprint ([5f05414](5f0541407c))
2025-05-06 10:50:54 +00:00
oSumAtrIX
5f0541407c fix: Correct incorrect fingerprint 2025-05-06 12:47:06 +02:00
semantic-release-bot
56b7ba9ba7 chore: Release v5.23.0-dev.5 [skip ci]
# [5.23.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.4...v5.23.0-dev.5) (2025-05-06)

### Bug Fixes

* **Spotify - Unlock Spotify Premium:** Remove pop up premium ads ([#4842](https://github.com/ReVanced/revanced-patches/issues/4842)) ([5028c1a](5028c1acb3))

### Features

* **Pandora:** Add `Disable audio ads` and `Unlimited skips` patch ([#4841](https://github.com/ReVanced/revanced-patches/issues/4841)) ([f4f36ff](f4f36ff273))
* **Prime Video:** Add `Skip ads` patch ([#4824](https://github.com/ReVanced/revanced-patches/issues/4824)) ([f8bdf74](f8bdf744ab))
2025-05-06 07:44:30 +00:00
hoodles
f8bdf744ab feat(Prime Video): Add Skip ads patch (#4824) 2025-05-06 11:40:45 +04:00
hoodles
f4f36ff273 feat(Pandora): Add Disable audio ads and Unlimited skips patch (#4841)
Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
2025-05-06 11:39:07 +04:00
Nuckyz
5028c1acb3 fix(Spotify - Unlock Spotify Premium): Remove pop up premium ads (#4842) 2025-05-06 11:35:47 +04:00
401 changed files with 14943 additions and 7102 deletions

View File

@@ -19,11 +19,11 @@ jobs:
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
uses: burrunan/gradle-cache-action@v3
- name: Build
env:

View File

@@ -13,24 +13,23 @@ jobs:
permissions:
contents: write
packages: write
id-token: write
attestations: write
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Make sure the release step uses its own credentials:
# https://github.com/cycjimmy/semantic-release-action#private-packages
persist-credentials: false
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"
distribution: 'temurin'
java-version: '17'
- name: Cache Gradle
uses: burrunan/gradle-cache-action@v1
uses: burrunan/gradle-cache-action@v3
- name: Build
env:
@@ -40,7 +39,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
node-version: 'lts/*'
cache: 'npm'
- name: Install dependencies
@@ -54,6 +53,14 @@ jobs:
fingerprint: ${{ vars.GPG_FINGERPRINT }}
- name: Release
uses: cycjimmy/semantic-release-action@v4
id: release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: npm exec semantic-release
- name: Attest
if: steps.release.outputs.new_release_published == 'true'
uses: actions/attest-build-provenance@v2
with:
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
subject-path: patches/build/libs/patches-*.rvp

View File

@@ -22,7 +22,7 @@
{
"assets": [
"CHANGELOG.md",
"gradle.properties",
"gradle.properties"
],
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
@@ -33,16 +33,16 @@
"assets": [
{
"path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
},
}
],
successComment: false
"successComment": false
}
],
[
"@saithodev/semantic-release-backmerge",
{
backmergeBranches: [{"from": "main", "to": "dev"}],
clearWorkspace: true
"backmergeBranches": [{"from": "main", "to": "dev"}],
"clearWorkspace": true
}
]
]

View File

@@ -1,3 +1,579 @@
# [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)
### Features
* **Proton Mail:** Add `Remove free accounts limit` patch ([#4970](https://github.com/ReVanced/revanced-patches/issues/4970)) ([b0440ad](https://github.com/ReVanced/revanced-patches/commit/b0440ad6af0e190e516974ce896dcc54c8d2e122))
# [5.25.0](https://github.com/ReVanced/revanced-patches/compare/v5.24.0...v5.25.0) (2025-05-29)
### Bug Fixes
* **Disable Pairip license check:** Change patch to default off ([74b6a94](https://github.com/ReVanced/revanced-patches/commit/74b6a94577ac3f73b04bd0cce98fb7011a6607fd))
* **Hide ADB status:** Resolve app crash on startup ([#5029](https://github.com/ReVanced/revanced-patches/issues/5029)) ([1abebd5](https://github.com/ReVanced/revanced-patches/commit/1abebd5f3b73250c6638d2d8a274b92ea8268924))
* **Messenger:** Remove outdated `Disable switching emoji to sticker` patch ([#5044](https://github.com/ReVanced/revanced-patches/issues/5044)) ([7b182ca](https://github.com/ReVanced/revanced-patches/commit/7b182cab825ee3a4a3ca528c744c9d2a351c7cf8))
* **Spotify Lite:** Remove obsolete `Enable on demand` patch ([#5046](https://github.com/ReVanced/revanced-patches/issues/5046)) ([4886d47](https://github.com/ReVanced/revanced-patches/commit/4886d47506c94b03c1f190ecc4947d3d91df6a47))
* **YouTube - GmsCore support:** Restore patch functionality from prior merge ([7686bbe](https://github.com/ReVanced/revanced-patches/commit/7686bbe975644e1e582fa52f166879da5694ed93))
* **YouTube - Hide ads:** Hide new type of general ad ([#5004](https://github.com/ReVanced/revanced-patches/issues/5004)) ([37e59d2](https://github.com/ReVanced/revanced-patches/commit/37e59d2771528c631dc13e73dac095fec95c6485))
* **YouTube - Open Shorts in regular player:** Do not exit app when pressing back button in regular player ([#5020](https://github.com/ReVanced/revanced-patches/issues/5020)) ([3384f8d](https://github.com/ReVanced/revanced-patches/commit/3384f8dd0ff2a345f2e387f4ed1570079a83ccb6))
* **YouTube:** Better handle incorrect duplicate translations ([20abac5](https://github.com/ReVanced/revanced-patches/commit/20abac52121fbecb65d87d0982f3380e1cf4e20e))
* **Yuka - Unlock premium:** Remove broken patch that is no longer supported ([#5018](https://github.com/ReVanced/revanced-patches/issues/5018)) ([fac6e59](https://github.com/ReVanced/revanced-patches/commit/fac6e59d281e21e57abdcfc899cd1aeb18e5c2b8))
### Features
* Add `Disable pairip license check` patch ([#4927](https://github.com/ReVanced/revanced-patches/issues/4927)) ([42d2c27](https://github.com/ReVanced/revanced-patches/commit/42d2c277982ef63e6ad42d85e46f13c3ab50243c))
* **Messenger:** Add `Remove Meta AI` patch ([#4945](https://github.com/ReVanced/revanced-patches/issues/4945)) ([012dff7](https://github.com/ReVanced/revanced-patches/commit/012dff7b6511b9e519ccac96f6713cf1a1b327b4))
* **Prime Video:** Add `Rename shared permissions` patch ([#5049](https://github.com/ReVanced/revanced-patches/issues/5049)) ([80f1fc6](https://github.com/ReVanced/revanced-patches/commit/80f1fc629e30e391bd5877f07dbdf4b6613bd1cf))
* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5))
* **Threads:** Hide Ads ([#5064](https://github.com/ReVanced/revanced-patches/issues/5064)) ([3c4cecb](https://github.com/ReVanced/revanced-patches/commit/3c4cecb966c2f99bfde99552686dda19ade5f67e))
* **YouTube - Enable debugging:** Add settings menu to share debug logs ([#5021](https://github.com/ReVanced/revanced-patches/issues/5021)) ([1ec4a88](https://github.com/ReVanced/revanced-patches/commit/1ec4a88464a2a2810c02cf072950b618d183779a))
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16))
* **YouTube - Swipe controls:** Add separate color settings for the brightness and volume bars ([#5043](https://github.com/ReVanced/revanced-patches/issues/5043)) ([80f50e8](https://github.com/ReVanced/revanced-patches/commit/80f50e8c50ca6a8366b7fd7b01459fb16fa1074a))
* **YouTube:** Add `Disable haptic feedback` patch ([#5033](https://github.com/ReVanced/revanced-patches/issues/5033)) ([bbe7974](https://github.com/ReVanced/revanced-patches/commit/bbe79744a513c96f9016476e8435f999e94c45d7))
# [5.25.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.13...v5.25.0-dev.14) (2025-05-29)
### Features
* **Threads:** Hide Ads ([#5064](https://github.com/ReVanced/revanced-patches/issues/5064)) ([3c4cecb](https://github.com/ReVanced/revanced-patches/commit/3c4cecb966c2f99bfde99552686dda19ade5f67e))
# [5.25.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.12...v5.25.0-dev.13) (2025-05-28)
### Features
* **Prime Video:** Add `Rename shared permissions` patch ([#5049](https://github.com/ReVanced/revanced-patches/issues/5049)) ([80f1fc6](https://github.com/ReVanced/revanced-patches/commit/80f1fc629e30e391bd5877f07dbdf4b6613bd1cf))
# [5.25.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.11...v5.25.0-dev.12) (2025-05-28)
### Features
* **YouTube - Swipe controls:** Add separate color settings for the brightness and volume bars ([#5043](https://github.com/ReVanced/revanced-patches/issues/5043)) ([80f50e8](https://github.com/ReVanced/revanced-patches/commit/80f50e8c50ca6a8366b7fd7b01459fb16fa1074a))
# [5.25.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.10...v5.25.0-dev.11) (2025-05-27)
### Features
* **YouTube - Enable debugging:** Add settings menu to share debug logs ([#5021](https://github.com/ReVanced/revanced-patches/issues/5021)) ([1ec4a88](https://github.com/ReVanced/revanced-patches/commit/1ec4a88464a2a2810c02cf072950b618d183779a))
* **YouTube:** Add `Disable haptic feedback` patch ([#5033](https://github.com/ReVanced/revanced-patches/issues/5033)) ([bbe7974](https://github.com/ReVanced/revanced-patches/commit/bbe79744a513c96f9016476e8435f999e94c45d7))
# [5.25.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.9...v5.25.0-dev.10) (2025-05-27)
### Bug Fixes
* **Messenger:** Remove outdated `Disable switching emoji to sticker` patch ([#5044](https://github.com/ReVanced/revanced-patches/issues/5044)) ([7b182ca](https://github.com/ReVanced/revanced-patches/commit/7b182cab825ee3a4a3ca528c744c9d2a351c7cf8))
* **Spotify Lite:** Remove obsolete `Enable on demand` patch ([#5046](https://github.com/ReVanced/revanced-patches/issues/5046)) ([4886d47](https://github.com/ReVanced/revanced-patches/commit/4886d47506c94b03c1f190ecc4947d3d91df6a47))
# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26)
### Features
* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5))
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16))
# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26)
### Features
* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5))
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16))
# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26)
### Features
* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5))
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16))
# [5.25.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.7...v5.25.0-dev.8) (2025-05-25)
### Bug Fixes
* **Hide ADB status:** Resolve app crash on startup ([#5029](https://github.com/ReVanced/revanced-patches/issues/5029)) ([1abebd5](https://github.com/ReVanced/revanced-patches/commit/1abebd5f3b73250c6638d2d8a274b92ea8268924))
# [5.25.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.6...v5.25.0-dev.7) (2025-05-24)
### Bug Fixes
* **YouTube - Open Shorts in regular player:** Do not exit app when pressing back button in regular player ([#5020](https://github.com/ReVanced/revanced-patches/issues/5020)) ([3384f8d](https://github.com/ReVanced/revanced-patches/commit/3384f8dd0ff2a345f2e387f4ed1570079a83ccb6))
# [5.25.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.5...v5.25.0-dev.6) (2025-05-23)
### Bug Fixes
* **Yuka - Unlock premium:** Remove broken patch that is no longer supported ([#5018](https://github.com/ReVanced/revanced-patches/issues/5018)) ([fac6e59](https://github.com/ReVanced/revanced-patches/commit/fac6e59d281e21e57abdcfc899cd1aeb18e5c2b8))
# [5.25.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.4...v5.25.0-dev.5) (2025-05-22)
### Bug Fixes
* **YouTube:** Better handle incorrect duplicate translations ([20abac5](https://github.com/ReVanced/revanced-patches/commit/20abac52121fbecb65d87d0982f3380e1cf4e20e))
# [5.25.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.3...v5.25.0-dev.4) (2025-05-22)
### Bug Fixes
* **YouTube - GmsCore support:** Restore patch functionality from prior merge ([7686bbe](https://github.com/ReVanced/revanced-patches/commit/7686bbe975644e1e582fa52f166879da5694ed93))
# [5.25.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.2...v5.25.0-dev.3) (2025-05-22)
### Bug Fixes
* **YouTube - Hide ads:** Hide new type of general ad ([#5004](https://github.com/ReVanced/revanced-patches/issues/5004)) ([37e59d2](https://github.com/ReVanced/revanced-patches/commit/37e59d2771528c631dc13e73dac095fec95c6485))
# [5.25.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.1...v5.25.0-dev.2) (2025-05-22)
### Bug Fixes
* **Disable Pairip license check:** Change patch to default off ([74b6a94](https://github.com/ReVanced/revanced-patches/commit/74b6a94577ac3f73b04bd0cce98fb7011a6607fd))
# [5.25.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.24.0...v5.25.0-dev.1) (2025-05-22)
### Features
* Add `Disable pairip license check` patch ([#4927](https://github.com/ReVanced/revanced-patches/issues/4927)) ([42d2c27](https://github.com/ReVanced/revanced-patches/commit/42d2c277982ef63e6ad42d85e46f13c3ab50243c))
* **Messenger:** Add `Remove Meta AI` patch ([#4945](https://github.com/ReVanced/revanced-patches/issues/4945)) ([012dff7](https://github.com/ReVanced/revanced-patches/commit/012dff7b6511b9e519ccac96f6713cf1a1b327b4))
# [5.24.0](https://github.com/ReVanced/revanced-patches/compare/v5.23.0...v5.24.0) (2025-05-19)
### Bug Fixes
* **Spotify - Fix third party launchers widgets:** Add missing compatibility annotation ([0493f80](https://github.com/ReVanced/revanced-patches/commit/0493f8035b26b90c5f8e42be2e2a5ce73d8685a5))
* **YouTube - Hide layout components:** Fix `Hide video recommendation labels` ([#4956](https://github.com/ReVanced/revanced-patches/issues/4956)) ([ae05ac3](https://github.com/ReVanced/revanced-patches/commit/ae05ac38151ebd3197953af97ca0dd847a04cc2d))
* **YouTube - Settings:** Correctly show summary text if search box is closed before searching ([d0ae835](https://github.com/ReVanced/revanced-patches/commit/d0ae835d3381fc659c9bb4a2d130d4db8a1499cf))
* **YouTube - SponsorBlock:** Fix segment category summary not showing category description ([06934a6](https://github.com/ReVanced/revanced-patches/commit/06934a60d91b40a5cdf7f4cd92deae4a136c149b))
### Features
* **GmsCore support:** Open vendor specific DontKillMyApp if available ([#4952](https://github.com/ReVanced/revanced-patches/issues/4952)) ([b89927a](https://github.com/ReVanced/revanced-patches/commit/b89927a10e3b909a3c37fbb75c16a7abbce44560))
* **NU.nl:** Support version `11.3.0` ([#4925](https://github.com/ReVanced/revanced-patches/issues/4925)) ([bedde60](https://github.com/ReVanced/revanced-patches/commit/bedde60fc1a52b0fd491174b3b5b887435eb621a))
* **Spotify:** Add `Fix third party launchers widgets` patch ([#4893](https://github.com/ReVanced/revanced-patches/issues/4893)) ([23bfdc9](https://github.com/ReVanced/revanced-patches/commit/23bfdc98fbbcc8ecf0ffbf8704f58dd2272e4af2))
* **YouTube - Hide description components:** Add `Hide Ask` ([#4972](https://github.com/ReVanced/revanced-patches/issues/4972)) ([ebc94a5](https://github.com/ReVanced/revanced-patches/commit/ebc94a5da6214b67399c9c01515689bd4b20547c))
* **YouTube - Hide layout components:** Add `Hide ticket shelf` ([#4969](https://github.com/ReVanced/revanced-patches/issues/4969)) ([6436af7](https://github.com/ReVanced/revanced-patches/commit/6436af7e77c77d2034dfceba8bc51132ad7632be))
* **YouTube - Hide player components:** Hide related video overlay in fullscreen ([#4938](https://github.com/ReVanced/revanced-patches/issues/4938)) ([ac9be97](https://github.com/ReVanced/revanced-patches/commit/ac9be9760c9965e54df196b227a310d64ead4bf5))
* **YouTube - Settings:** Add ability to search in settings ([#4881](https://github.com/ReVanced/revanced-patches/issues/4881)) ([aca8b20](https://github.com/ReVanced/revanced-patches/commit/aca8b207c15f254bcc9ad94bc7dfb895f21d4058))
# [5.24.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.8...v5.24.0-dev.9) (2025-05-18)
### Bug Fixes
* **YouTube - SponsorBlock:** Fix segment category summary not showing category description ([06934a6](https://github.com/ReVanced/revanced-patches/commit/06934a60d91b40a5cdf7f4cd92deae4a136c149b))
# [5.24.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.7...v5.24.0-dev.8) (2025-05-17)
### Bug Fixes
* **YouTube - Settings:** Correctly show summary text if search box is closed before searching ([d0ae835](https://github.com/ReVanced/revanced-patches/commit/d0ae835d3381fc659c9bb4a2d130d4db8a1499cf))
# [5.24.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.6...v5.24.0-dev.7) (2025-05-17)
### Features
* **YouTube - Hide layout components:** Add `Hide ticket shelf` ([#4969](https://github.com/ReVanced/revanced-patches/issues/4969)) ([6436af7](https://github.com/ReVanced/revanced-patches/commit/6436af7e77c77d2034dfceba8bc51132ad7632be))
# [5.24.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.5...v5.24.0-dev.6) (2025-05-17)
### Features
* **YouTube - Hide description components:** Add `Hide Ask` ([#4972](https://github.com/ReVanced/revanced-patches/issues/4972)) ([ebc94a5](https://github.com/ReVanced/revanced-patches/commit/ebc94a5da6214b67399c9c01515689bd4b20547c))
# [5.24.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.4...v5.24.0-dev.5) (2025-05-17)
### Bug Fixes
* **Spotify - Fix third party launchers widgets:** Add missing compatibility annotation ([0493f80](https://github.com/ReVanced/revanced-patches/commit/0493f8035b26b90c5f8e42be2e2a5ce73d8685a5))
### Features
* **YouTube - Settings:** Add ability to search in settings ([#4881](https://github.com/ReVanced/revanced-patches/issues/4881)) ([aca8b20](https://github.com/ReVanced/revanced-patches/commit/aca8b207c15f254bcc9ad94bc7dfb895f21d4058))
# [5.24.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.3...v5.24.0-dev.4) (2025-05-16)
### Features
* **Spotify:** Add `Fix third party launchers widgets` patch ([#4893](https://github.com/ReVanced/revanced-patches/issues/4893)) ([23bfdc9](https://github.com/ReVanced/revanced-patches/commit/23bfdc98fbbcc8ecf0ffbf8704f58dd2272e4af2))
# [5.24.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.2...v5.24.0-dev.3) (2025-05-14)
### Bug Fixes
* **YouTube - Hide layout components:** Fix `Hide video recommendation labels` ([#4956](https://github.com/ReVanced/revanced-patches/issues/4956)) ([ae05ac3](https://github.com/ReVanced/revanced-patches/commit/ae05ac38151ebd3197953af97ca0dd847a04cc2d))
# [5.24.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.1...v5.24.0-dev.2) (2025-05-14)
### Features
* **GmsCore support:** Open vendor specific DontKillMyApp if available ([#4952](https://github.com/ReVanced/revanced-patches/issues/4952)) ([b89927a](https://github.com/ReVanced/revanced-patches/commit/b89927a10e3b909a3c37fbb75c16a7abbce44560))
* **YouTube - Hide player components:** Hide related video overlay in fullscreen ([#4938](https://github.com/ReVanced/revanced-patches/issues/4938)) ([ac9be97](https://github.com/ReVanced/revanced-patches/commit/ac9be9760c9965e54df196b227a310d64ead4bf5))
# [5.24.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.23.0...v5.24.0-dev.1) (2025-05-12)
### Features
* **NU.nl:** Support version `11.3.0` ([#4925](https://github.com/ReVanced/revanced-patches/issues/4925)) ([bedde60](https://github.com/ReVanced/revanced-patches/commit/bedde60fc1a52b0fd491174b3b5b887435eb621a))
# [5.23.0](https://github.com/ReVanced/revanced-patches/compare/v5.22.0...v5.23.0) (2025-05-10)
### Bug Fixes
* Correct incorrect fingerprint ([c3bab89](https://github.com/ReVanced/revanced-patches/commit/c3bab89fc4189e38c10eee0caa36289de7e29dfa))
* Fix incorrect fingerprints ([#4917](https://github.com/ReVanced/revanced-patches/issues/4917)) ([49ca329](https://github.com/ReVanced/revanced-patches/commit/49ca3290a726cdba7bc9b62ffcd8d46e6f04778e))
* **Spotify - Unlock Spotify Premium:** Remove pop up premium ads ([#4842](https://github.com/ReVanced/revanced-patches/issues/4842)) ([00aa200](https://github.com/ReVanced/revanced-patches/commit/00aa2000ba2eef15a0dd827c2bd84c2e85c412e0))
* **YouTube:** Improve litho filtering performance ([#4904](https://github.com/ReVanced/revanced-patches/issues/4904)) ([7b43986](https://github.com/ReVanced/revanced-patches/commit/7b43986871a68e5cb43331d2fb2fdb9ef67438ad))
* **YouTube:** Simplify litho filtering patch ([#4910](https://github.com/ReVanced/revanced-patches/issues/4910)) ([bd53955](https://github.com/ReVanced/revanced-patches/commit/bd53955df738bb7b819eb91a3e776e9d2ca5c74a))
### Features
* **Lightroom:** Constrain patches to last working version ([efef03b](https://github.com/ReVanced/revanced-patches/commit/efef03b80da21552d0d8be6913faba64e4fb5ed1))
* **Pandora:** Add `Disable audio ads` and `Unlimited skips` patch ([#4841](https://github.com/ReVanced/revanced-patches/issues/4841)) ([0cf7a4c](https://github.com/ReVanced/revanced-patches/commit/0cf7a4c6be615ed0a52a6bacf87592f5f43ff575))
* **Prime Video:** Add `Skip ads` patch ([#4824](https://github.com/ReVanced/revanced-patches/issues/4824)) ([bb672c4](https://github.com/ReVanced/revanced-patches/commit/bb672c4674ddc201b8b2648c3906cfc31ef43f10))
* **Spotify:** Add `Sanitize sharing links` patch ([#4829](https://github.com/ReVanced/revanced-patches/issues/4829)) ([2e3511d](https://github.com/ReVanced/revanced-patches/commit/2e3511d03c8198bbdb9336888df038a33fb3ab8c))
# [5.23.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.6...v5.23.0-dev.7) (2025-05-06)
### Bug Fixes
* Fix incorrect fingerprints ([#4917](https://github.com/ReVanced/revanced-patches/issues/4917)) ([49ca329](https://github.com/ReVanced/revanced-patches/commit/49ca3290a726cdba7bc9b62ffcd8d46e6f04778e))
# [5.23.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.5...v5.23.0-dev.6) (2025-05-06)
### Bug Fixes
* Correct incorrect fingerprint ([c3bab89](https://github.com/ReVanced/revanced-patches/commit/c3bab89fc4189e38c10eee0caa36289de7e29dfa))
# [5.23.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.4...v5.23.0-dev.5) (2025-05-06)
### Bug Fixes
* **Spotify - Unlock Spotify Premium:** Remove pop up premium ads ([#4842](https://github.com/ReVanced/revanced-patches/issues/4842)) ([00aa200](https://github.com/ReVanced/revanced-patches/commit/00aa2000ba2eef15a0dd827c2bd84c2e85c412e0))
### Features
* **Pandora:** Add `Disable audio ads` and `Unlimited skips` patch ([#4841](https://github.com/ReVanced/revanced-patches/issues/4841)) ([0cf7a4c](https://github.com/ReVanced/revanced-patches/commit/0cf7a4c6be615ed0a52a6bacf87592f5f43ff575))
* **Prime Video:** Add `Skip ads` patch ([#4824](https://github.com/ReVanced/revanced-patches/issues/4824)) ([bb672c4](https://github.com/ReVanced/revanced-patches/commit/bb672c4674ddc201b8b2648c3906cfc31ef43f10))
# [5.23.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.3...v5.23.0-dev.4) (2025-05-06)

View File

@@ -0,0 +1,3 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,25 @@
package app.revanced.extension.messenger.metaai;
import java.util.*;
import app.revanced.extension.shared.Logger;
@SuppressWarnings("unused")
public class RemoveMetaAIPatch {
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;
}
}

View File

@@ -82,7 +82,7 @@ public class HideAdsPatch {
// Filter HeaderBlock with known ads until next HeaderBlock.
if (currentBlock instanceof HeaderBlock headerBlock) {
StyledText headerText = headerBlock.component20();
StyledText headerText = headerBlock.getTitle();
if (headerText != null) {
skipFullHeader = false;
for (String blockedHeaderBlock : blockedHeaderBlocks) {

View File

@@ -3,8 +3,7 @@ package nl.nu.performance.api.client.objects;
import nl.nu.performance.api.client.interfaces.Block;
public class HeaderBlock extends Block {
// returns title
public final StyledText component20() {
public final StyledText getTitle() {
throw new UnsupportedOperationException("Stub");
}
}

View File

@@ -0,0 +1,4 @@
dependencies {
compileOnly(project(":extensions:shared:library"))
compileOnly(project(":extensions:primevideo:stub"))
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,36 @@
package app.revanced.extension.primevideo.ads;
import com.amazon.avod.fsm.SimpleTrigger;
import com.amazon.avod.media.ads.AdBreak;
import com.amazon.avod.media.ads.internal.state.AdBreakTrigger;
import com.amazon.avod.media.ads.internal.state.AdEnabledPlayerTriggerType;
import com.amazon.avod.media.playback.VideoPlayer;
import com.amazon.avod.media.ads.internal.state.ServerInsertedAdBreakState;
import app.revanced.extension.shared.Logger;
@SuppressWarnings("unused")
public final class SkipAdsPatch {
public static void enterServerInsertedAdBreakState(ServerInsertedAdBreakState state, AdBreakTrigger trigger, VideoPlayer player) {
try {
AdBreak adBreak = trigger.getBreak();
// There are two scenarios when entering the original method:
// 1. Player naturally entered an ad break while watching a video.
// 2. User is skipped/scrubbed to a position on the timeline. If seek position is past an ad break,
// user is forced to watch an ad before continuing.
//
// Scenario 2 is indicated by trigger.getSeekStartPosition() != null, so skip directly to the scrubbing
// target. Otherwise, just calculate when the ad break should end and skip to there.
if (trigger.getSeekStartPosition() != null)
player.seekTo(trigger.getSeekTarget().getTotalMilliseconds());
else
player.seekTo(player.getCurrentPosition() + adBreak.getDurationExcludingAux().getTotalMilliseconds());
// Send "end of ads" trigger to state machine so everything doesn't get whacky.
state.doTrigger(new SimpleTrigger(AdEnabledPlayerTriggerType.NO_MORE_ADS_SKIP_TRANSITION));
} catch (Exception ex) {
Logger.printException(() -> "Failed skipping ads", ex);
}
}
}

View File

@@ -0,0 +1,17 @@
plugins {
id(libs.plugins.android.library.get().pluginId)
}
android {
namespace = "app.revanced.extension"
compileSdk = 34
defaultConfig {
minSdk = 21
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}

View File

@@ -0,0 +1 @@
<manifest/>

View File

@@ -0,0 +1,6 @@
package com.amazon.avod.fsm;
public final class SimpleTrigger<T> implements Trigger<T> {
public SimpleTrigger(T triggerType) {
}
}

View File

@@ -0,0 +1,7 @@
package com.amazon.avod.fsm;
public abstract class StateBase<S, T> {
// This method orginally has protected access (modified in patch code).
public void doTrigger(Trigger<T> trigger) {
}
}

View File

@@ -0,0 +1,4 @@
package com.amazon.avod.fsm;
public interface Trigger<T> {
}

View File

@@ -0,0 +1,7 @@
package com.amazon.avod.media;
public final class TimeSpan {
public long getTotalMilliseconds() {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,7 @@
package com.amazon.avod.media.ads;
import com.amazon.avod.media.TimeSpan;
public interface AdBreak {
TimeSpan getDurationExcludingAux();
}

View File

@@ -0,0 +1,4 @@
package com.amazon.avod.media.ads.internal.state;
public abstract class AdBreakState extends AdEnabledPlaybackState {
}

View File

@@ -0,0 +1,18 @@
package com.amazon.avod.media.ads.internal.state;
import com.amazon.avod.media.ads.AdBreak;
import com.amazon.avod.media.TimeSpan;
public class AdBreakTrigger {
public AdBreak getBreak() {
throw new UnsupportedOperationException();
}
public TimeSpan getSeekTarget() {
throw new UnsupportedOperationException();
}
public TimeSpan getSeekStartPosition() {
throw new UnsupportedOperationException();
}
}

View File

@@ -0,0 +1,8 @@
package com.amazon.avod.media.ads.internal.state;
import com.amazon.avod.fsm.StateBase;
import com.amazon.avod.media.playback.state.PlayerStateType;
import com.amazon.avod.media.playback.state.trigger.PlayerTriggerType;
public class AdEnabledPlaybackState extends StateBase<PlayerStateType, PlayerTriggerType> {
}

View File

@@ -0,0 +1,5 @@
package com.amazon.avod.media.ads.internal.state;
public enum AdEnabledPlayerTriggerType {
NO_MORE_ADS_SKIP_TRANSITION
}

View File

@@ -0,0 +1,4 @@
package com.amazon.avod.media.ads.internal.state;
public class ServerInsertedAdBreakState extends AdBreakState {
}

View File

@@ -0,0 +1,7 @@
package com.amazon.avod.media.playback;
public interface VideoPlayer {
long getCurrentPosition();
void seekTo(long positionMs);
}

View File

@@ -0,0 +1,4 @@
package com.amazon.avod.media.playback.state;
public interface PlayerStateType {
}

View File

@@ -0,0 +1,4 @@
package com.amazon.avod.media.playback.state.trigger;
public interface PlayerTriggerType {
}

View File

@@ -1,10 +1,11 @@
package app.revanced.extension.shared;
import static app.revanced.extension.shared.StringRef.str;
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;
@@ -14,11 +15,20 @@ 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;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
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 {
@@ -29,10 +39,24 @@ public class GmsCoreSupport {
= getGmsCoreVendorGroupId() + ".android.gms";
private static final Uri GMS_CORE_PROVIDER
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
private static final String DONT_KILL_MY_APP_LINK
= "https://dontkillmyapp.com";
private static final String DONT_KILL_MY_APP_URL
= "https://dontkillmyapp.com/";
private static final Route DONT_KILL_MY_APP_MANUFACTURER_API
= new Route(GET, "/api/v2/{manufacturer}.json");
private static final String DONT_KILL_MY_APP_NAME_PARAMETER
= "?app=MicroG";
private static final String BUILD_MANUFACTURER
= Build.MANUFACTURER.toLowerCase(Locale.ROOT).replace(" ", "-");
/**
* If a manufacturer specific page exists on DontKillMyApp.
*/
@Nullable
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
private static void open(String queryOrLink) {
Logger.printInfo(() -> "Opening link: " + queryOrLink);
Intent intent;
try {
// Check if queryOrLink is a valid URL.
@@ -57,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);
}
@@ -86,7 +124,7 @@ public class GmsCoreSupport {
// Do not exit. If the app exits before launch completes (and without
// opening another activity), then on some devices such as Pixel phone Android 10
// no toast will be shown and the app will continually be relaunched
// no toast will be shown and the app will continually relaunch
// with the appearance of a hung app.
}
@@ -122,11 +160,12 @@ public class GmsCoreSupport {
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
if (client == null) {
Logger.printInfo(() -> "GmsCore is not running in the background");
checkIfDontKillMyAppSupportsManufacturer();
showBatteryOptimizationDialog(context,
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
"gms_core_dialog_open_website_text",
(dialog, id) -> open(DONT_KILL_MY_APP_LINK));
(dialog, id) -> openDontKillMyApp());
}
}
} catch (Exception ex) {
@@ -141,6 +180,48 @@ public class GmsCoreSupport {
activity.startActivityForResult(intent, 0);
}
private static void checkIfDontKillMyAppSupportsManufacturer() {
Utils.runOnBackgroundThread(() -> {
try {
final long start = System.currentTimeMillis();
HttpURLConnection connection = Requester.getConnectionFromRoute(
DONT_KILL_MY_APP_URL, DONT_KILL_MY_APP_MANUFACTURER_API, BUILD_MANUFACTURER);
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
final boolean supported = connection.getResponseCode() == 200;
Logger.printInfo(() -> "Manufacturer is " + (supported ? "" : "NOT ")
+ "listed on DontKillMyApp: " + BUILD_MANUFACTURER
+ " fetch took: " + (System.currentTimeMillis() - start) + "ms");
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = supported;
} catch (Exception ex) {
Logger.printInfo(() -> "Could not check if manufacturer is listed on DontKillMyApp: "
+ BUILD_MANUFACTURER, ex);
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = null;
}
});
}
private static void openDontKillMyApp() {
final Boolean manufacturerSupported = DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
String manufacturerPageToOpen;
if (manufacturerSupported == null) {
// Fetch has not completed yet. Only happens on extremely slow internet connections
// and the user spends less than 1 second reading what's on screen.
// Instead of waiting for the fetch (which may timeout),
// open the website without a vendor.
manufacturerPageToOpen = "";
} else if (manufacturerSupported) {
manufacturerPageToOpen = BUILD_MANUFACTURER;
} else {
// No manufacturer specific page exists. Open the general page.
manufacturerPageToOpen = "general";
}
open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER);
}
/**
* @return If GmsCore is not whitelisted from battery optimizations.
*/

View File

@@ -1,15 +1,27 @@
package app.revanced.extension.shared;
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG;
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_STACKTRACE;
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.settings.BaseSettings;
import java.io.PrintWriter;
import java.io.StringWriter;
import static app.revanced.extension.shared.settings.BaseSettings.*;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.shared.settings.preference.LogBufferManager;
/**
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
* and additionally accessible thru {@link LogBufferManager}.
*
* All methods are thread safe, and are safe to call even
* if {@link Utils#getContext()} is not available.
*/
public class Logger {
/**
@@ -17,99 +29,173 @@ public class Logger {
*/
@FunctionalInterface
public interface LogMessage {
/**
* @return Logger string message. This method is only called if logging is enabled.
*/
@NonNull
String buildMessageString();
}
/**
* @return For outer classes, this returns {@link Class#getSimpleName()}.
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
* <br>
* For example, each of these classes return 'SomethingView':
* <code>
* com.company.SomethingView
* com.company.SomethingView$StaticClass
* com.company.SomethingView$1
* </code>
*/
private String findOuterClassSimpleName() {
var selfClass = this.getClass();
private enum LogLevel {
DEBUG,
INFO,
ERROR
}
String fullClassName = selfClass.getName();
final int dollarSignIndex = fullClassName.indexOf('$');
if (dollarSignIndex < 0) {
return selfClass.getSimpleName(); // Already an outer class.
/**
* Log tag prefix. Only used for system logging.
*/
private static final String REVANCED_LOG_TAG_PREFIX = "revanced: ";
private static final String LOGGER_CLASS_NAME = Logger.class.getName();
/**
* @return For outer classes, this returns {@link Class#getSimpleName()}.
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
* <br>
* For example, each of these classes returns 'SomethingView':
* <code>
* com.company.SomethingView
* com.company.SomethingView$StaticClass
* com.company.SomethingView$1
* </code>
*/
private static String getOuterClassSimpleName(Object obj) {
Class<?> logClass = obj.getClass();
String fullClassName = logClass.getName();
final int dollarSignIndex = fullClassName.indexOf('$');
if (dollarSignIndex < 0) {
return logClass.getSimpleName(); // Already an outer class.
}
// Class is inner, static, or anonymous.
// Parse the simple name full name.
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
}
/**
* Internal method to handle logging to Android Log and {@link LogBufferManager}.
* Appends the log message, stack trace (if enabled), and exception (if present) to logBuffer
* with class name but without 'revanced:' prefix.
*
* @param logLevel The log level.
* @param message Log message object.
* @param ex Optional exception.
* @param includeStackTrace If the current stack should be included.
* @param showToast If a toast is to be shown.
*/
private static void logInternal(LogLevel logLevel, LogMessage message, @Nullable Throwable ex,
boolean includeStackTrace, boolean showToast) {
// It's very important that no Settings are used in this method,
// as this code is used when a context is not set and thus referencing
// a setting will crash the app.
String messageString = message.buildMessageString();
String className = getOuterClassSimpleName(message);
String logText = messageString;
// Append exception message if present.
if (ex != null) {
var exceptionMessage = ex.getMessage();
if (exceptionMessage != null) {
logText += "\nException: " + exceptionMessage;
}
}
// Class is inner, static, or anonymous.
// Parse the simple name full name.
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
if (includeStackTrace) {
var sw = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(sw));
String stackTrace = sw.toString();
// Remove the stacktrace elements of this class.
final int loggerIndex = stackTrace.lastIndexOf(LOGGER_CLASS_NAME);
final int loggerBegins = stackTrace.indexOf('\n', loggerIndex);
logText += stackTrace.substring(loggerBegins);
}
// Do not include "revanced:" prefix in clipboard logs.
String managerToastString = className + ": " + logText;
LogBufferManager.appendToLogBuffer(managerToastString);
String logTag = REVANCED_LOG_TAG_PREFIX + className;
switch (logLevel) {
case DEBUG:
if (ex == null) Log.d(logTag, logText);
else Log.d(logTag, logText, ex);
break;
case INFO:
if (ex == null) Log.i(logTag, logText);
else Log.i(logTag, logText, ex);
break;
case ERROR:
if (ex == null) Log.e(logTag, logText);
else Log.e(logTag, logText, ex);
break;
}
if (showToast) {
Utils.showToastLong(managerToastString);
}
}
private static final String REVANCED_LOG_PREFIX = "revanced: ";
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.
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
* <p>
* Whenever possible, the log string should be constructed entirely inside
* {@link LogMessage#buildMessageString()} so the performance cost of
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/
public static void printDebug(@NonNull LogMessage message) {
public static void printDebug(LogMessage message) {
printDebug(message, null);
}
/**
* Logs debug messages under the outer class name of the code calling this method.
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
* <p>
* Whenever possible, the log string should be constructed entirely inside
* {@link LogMessage#buildMessageString()} so the performance cost of
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
*/
public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
if (DEBUG.get()) {
String logMessage = message.buildMessageString();
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
if (DEBUG_STACKTRACE.get()) {
var builder = new StringBuilder(logMessage);
var sw = new StringWriter();
new Throwable().printStackTrace(new PrintWriter(sw));
builder.append('\n').append(sw);
logMessage = builder.toString();
}
if (ex == null) {
Log.d(logTag, logMessage);
} else {
Log.d(logTag, logMessage, ex);
}
public static void printDebug(LogMessage message, @Nullable Exception ex) {
if (shouldLogDebug()) {
logInternal(LogLevel.DEBUG, message, ex, includeStackTrace(), false);
}
}
/**
* Logs information messages using the outer class name of the code calling this method.
*/
public static void printInfo(@NonNull LogMessage message) {
public static void printInfo(LogMessage message) {
printInfo(message, null);
}
/**
* Logs information messages using the outer class name of the code calling this method.
*/
public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) {
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
String logMessage = message.buildMessageString();
if (ex == null) {
Log.i(logTag, logMessage);
} else {
Log.i(logTag, logMessage, ex);
}
public static void printInfo(LogMessage message, @Nullable Exception ex) {
logInternal(LogLevel.INFO, message, ex, includeStackTrace(), false);
}
/**
* Logs exceptions under the outer class name of the code calling this method.
* Appends the log message, exception (if present), and toast message (if enabled) to logBuffer.
*/
public static void printException(@NonNull LogMessage message) {
public static void printException(LogMessage message) {
printException(message, null);
}
@@ -122,35 +208,7 @@ public class Logger {
* @param message log message
* @param ex exception (optional)
*/
public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) {
String messageString = message.buildMessageString();
String outerClassSimpleName = message.findOuterClassSimpleName();
String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName;
if (ex == null) {
Log.e(logMessage, messageString);
} else {
Log.e(logMessage, messageString, ex);
}
if (DEBUG_TOAST_ON_ERROR.get()) {
Utils.showToastLong(outerClassSimpleName + ": " + messageString);
}
public static void printException(LogMessage message, @Nullable Throwable ex) {
logInternal(LogLevel.ERROR, message, ex, includeStackTrace(), shouldShowErrorToast());
}
/**
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
* Normally this method should not be used.
*/
public static void initializationInfo(@NonNull Class<?> callingClass, @NonNull String message) {
Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message);
}
/**
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
* Normally this method should not be used.
*/
public static void initializationException(@NonNull Class<?> callingClass, @NonNull String message,
@Nullable Exception ex) {
Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex);
}
}
}

View File

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

View File

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

View File

@@ -89,9 +89,11 @@ public enum AppLanguage {
ZU;
private final String language;
private final Locale locale;
AppLanguage() {
language = name().toLowerCase(Locale.US);
locale = Locale.forLanguageTag(language);
}
/**
@@ -112,6 +114,6 @@ public enum AppLanguage {
return Locale.getDefault();
}
return Locale.forLanguageTag(language);
return locale;
}
}

View File

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

View File

@@ -0,0 +1,449 @@
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.Dialog;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Typeface;
import android.os.Build;
import android.os.Bundle;
import android.preference.EditTextPreference;
import android.text.Editable;
import android.text.InputType;
import android.text.SpannableString;
import android.text.Spanned;
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.*;
import androidx.annotation.ColorInt;
import java.util.Locale;
import java.util.regex.Pattern;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.settings.StringSetting;
/**
* A custom preference for selecting a color via a hexadecimal code or a color picker dialog.
* Extends {@link EditTextPreference} to display a colored dot in the widget area,
* reflecting the currently selected color. The dot is dimmed when the preference is disabled.
*/
@SuppressWarnings({"unused", "deprecation"})
public class ColorPickerPreference extends EditTextPreference {
/**
* Character to show the color appearance.
*/
public static final String COLOR_DOT_STRING = "";
/**
* Length of a valid color string of format #RRGGBB.
*/
public static final int COLOR_STRING_LENGTH = 7;
/**
* Matches everything that is not a hex number/letter.
*/
private static final Pattern PATTERN_NOT_HEX = Pattern.compile("[^0-9A-Fa-f]");
/**
* Alpha for dimming when the preference is disabled.
*/
private static final float DISABLED_ALPHA = 0.5f; // 50%
/**
* View displaying a colored dot in the widget area.
*/
private View widgetColorDot;
/**
* Current color in RGB format (without alpha).
*/
@ColorInt
private int currentColor;
/**
* Associated setting for storing the color value.
*/
private StringSetting colorSetting;
/**
* Dialog TextWatcher for the EditText to monitor color input changes.
*/
private TextWatcher colorTextWatcher;
/**
* Dialog TextView displaying a colored dot for the selected color preview in the dialog.
*/
private TextView dialogColorPreview;
/**
* Dialog color picker view.
*/
private ColorPickerView dialogColorPickerView;
/**
* Removes non valid hex characters, converts to all uppercase,
* and adds # character to the start if not present.
*/
public static String cleanupColorCodeString(String colorString) {
// Remove non-hex chars, convert to uppercase, and ensure correct length
String result = "#" + PATTERN_NOT_HEX.matcher(colorString)
.replaceAll("").toUpperCase(Locale.ROOT);
if (result.length() < COLOR_STRING_LENGTH) {
return result;
}
return result.substring(0, COLOR_STRING_LENGTH);
}
/**
* @param color RGB color, without an alpha channel.
* @return #RRGGBB hex color string
*/
public static String getColorString(@ColorInt int color) {
String colorString = String.format("#%06X", color);
if ((color & 0xFF000000) != 0) {
// Likely a bug somewhere.
Logger.printException(() -> "getColorString: color has alpha channel: " + colorString);
}
return colorString;
}
/**
* Creates a Spanned object for a colored dot using SpannableString.
*
* @param color The RGB color (without alpha).
* @return A Spanned object with the colored dot.
*/
public static Spanned getColorDot(@ColorInt int color) {
SpannableString spannable = new SpannableString(COLOR_DOT_STRING);
spannable.setSpan(new ForegroundColorSpan(color | 0xFF000000), 0, COLOR_DOT_STRING.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
spannable.setSpan(new RelativeSizeSpan(1.5f), 0, 1,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannable;
}
public ColorPickerPreference(Context context) {
super(context);
init();
}
public ColorPickerPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
/**
* Initializes the preference by setting up the EditText, loading the color, and set the widget layout.
*/
private void init() {
colorSetting = (StringSetting) Setting.getSettingFromPath(getKey());
if (colorSetting == null) {
Logger.printException(() -> "Could not find color setting for: " + getKey());
}
EditText editText = getEditText();
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
editText.setAutofillHints((String) null);
}
// Set the widget layout to a custom layout containing the colored dot.
setWidgetLayoutResource(getResourceIdentifier("revanced_color_dot_widget", "layout"));
}
/**
* Sets the selected color and updates the UI and settings.
*
* @param colorString The color in hexadecimal format (e.g., "#RRGGBB").
* @throws IllegalArgumentException If the color string is invalid.
*/
@Override
public final void setText(String colorString) {
try {
Logger.printDebug(() -> "setText: " + colorString);
super.setText(colorString);
currentColor = Color.parseColor(colorString) & 0x00FFFFFF;
if (colorSetting != null) {
colorSetting.save(getColorString(currentColor));
}
updateColorPreview();
updateWidgetColorDot();
} catch (IllegalArgumentException ex) {
// This code is reached if the user pastes settings json with an invalid color
// since this preference is updated with the new setting text.
Logger.printDebug(() -> "Parse color error: " + colorString, ex);
Utils.showToastShort(str("revanced_settings_color_invalid"));
setText(colorSetting.resetToDefault());
} catch (Exception ex) {
Logger.printException(() -> "setText failure: " + colorString, ex);
}
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
widgetColorDot = view.findViewById(getResourceIdentifier(
"revanced_color_dot_widget", "id"));
widgetColorDot.setBackgroundResource(getResourceIdentifier(
"revanced_settings_circle_background", "drawable"));
widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
}
/**
* Updates the color preview TextView with a colored dot.
*/
private void updateColorPreview() {
if (dialogColorPreview != null) {
dialogColorPreview.setText(getColorDot(currentColor));
}
}
private void updateWidgetColorDot() {
if (widgetColorDot != null) {
widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
}
}
/**
* Creates a TextWatcher to monitor changes in the EditText for color input.
*
* @return A TextWatcher that updates the color preview on valid input.
*/
private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) {
return new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable edit) {
try {
String colorString = edit.toString();
String sanitizedColorString = cleanupColorCodeString(colorString);
if (!sanitizedColorString.equals(colorString)) {
edit.replace(0, colorString.length(), sanitizedColorString);
return;
}
if (sanitizedColorString.length() != COLOR_STRING_LENGTH) {
// User is still typing out the color.
return;
}
final int newColor = Color.parseColor(colorString);
if (currentColor != newColor) {
Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString);
currentColor = newColor;
updateColorPreview();
updateWidgetColorDot();
colorPickerView.setColor(newColor);
}
} catch (Exception ex) {
// Should never be reached since input is validated before using.
Logger.printException(() -> "afterTextChanged failure", ex);
}
}
};
}
/**
* Creates a Dialog with a color preview and EditText for hex color input.
*/
@Override
protected void showDialog(Bundle state) {
Context context = getContext();
// 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);
// 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
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (colorTextWatcher != null) {
getEditText().removeTextChangedListener(colorTextWatcher);
colorTextWatcher = null;
}
dialogColorPreview = null;
dialogColorPickerView = null;
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
updateWidgetColorDot();
}
}

View File

@@ -0,0 +1,500 @@
package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.Utils.dipToPixels;
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ComposeShader;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.RectF;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.ColorInt;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
/**
* A custom color picker view that allows the user to select a color using a hue slider and a saturation-value selector.
* This implementation is density-independent and responsive across different screen sizes and DPIs.
*
* <p>
* This view displays two main components for color selection:
* <ul>
* <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>
*
* <p>
* The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar and the
* saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles).
*
* <p>
* The selected color can be retrieved using {@link #getColor()} and can be set using {@link #setColor(int)}.
* An {@link OnColorChangedListener} can be registered to receive notifications when the selected color changes.
*/
public class ColorPickerView extends View {
/**
* Interface definition for a callback to be invoked when the selected color changes.
*/
public interface OnColorChangedListener {
/**
* Called when the selected color has changed.
*
* Important: Callback color uses RGB format with zero alpha channel.
*
* @param color The new selected color.
*/
void onColorChanged(@ColorInt int color);
}
/** Expanded touch area for the hue bar to increase the touch-sensitive area. */
public static final float TOUCH_EXPANSION = dipToPixels(20f);
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
private static final float VIEW_PADDING = dipToPixels(16);
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,
* 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;
/** Thin dark outline stroke width for the selector rings. */
private static final float SELECTOR_EDGE_STROKE_WIDTH = 1;
public static final float SELECTOR_EDGE_RADIUS =
SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2;
/** Selector outline inner color. */
@ColorInt
private static final int SELECTOR_OUTLINE_COLOR = Color.WHITE;
/** Dark edge color for the selector rings. */
@ColorInt
private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF");
private static final int[] HUE_COLORS = new int[361];
static {
for (int i = 0; i < 361; i++) {
HUE_COLORS[i] = Color.HSVToColor(new float[]{i, 1, 1});
}
}
/** Hue bar. */
private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/** Saturation-value selector. */
private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
/** Draggable selector. */
private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
{
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
}
/** Bounds of the hue bar. */
private final RectF hueRect = new RectF();
/** Bounds of the saturation-value selector. */
private final RectF saturationValueRect = new RectF();
/** HSV color calculations to avoid allocations during drawing. */
private final float[] hsvArray = {1, 1, 1};
/** Current hue value (0-360). */
private float hue = 0f;
/** Current saturation value (0-1). */
private float saturation = 1f;
/** Current value (brightness) value (0-1). */
private float value = 1f;
/** The currently selected color in RGB format with no alpha channel. */
@ColorInt
private int selectedColor;
private OnColorChangedListener colorChangedListener;
/** Track if we're currently dragging the hue or saturation handle. */
private boolean isDraggingHue;
private boolean isDraggingSaturation;
public ColorPickerView(Context context) {
super(context);
}
public ColorPickerView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
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) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
int width = resolveSize(minWidth, widthMeasureSpec);
int height = resolveSize(minHeight, heightMeasureSpec);
// 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) + (int) (HUE_BAR_HEIGHT + MARGIN_BETWEEN_AREAS);
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
height = desiredHeight;
}
setMeasuredDimension(width, height);
}
/**
* Called when the size of the view changes.
* This method calculates and sets the bounds of the hue bar and saturation-value selector.
* It also creates the necessary shaders for the gradients.
*/
@Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
// Calculate bounds with hue bar at the bottom.
final float effectiveWidth = width - (2 * VIEW_PADDING);
final float effectiveHeight = height - (2 * VIEW_PADDING) - HUE_BAR_HEIGHT - MARGIN_BETWEEN_AREAS;
// Adjust rectangles to account for padding and density-independent dimensions.
saturationValueRect.set(
VIEW_PADDING,
VIEW_PADDING,
VIEW_PADDING + effectiveWidth,
VIEW_PADDING + effectiveHeight
);
hueRect.set(
VIEW_PADDING,
height - VIEW_PADDING - HUE_BAR_HEIGHT,
VIEW_PADDING + effectiveWidth,
height - VIEW_PADDING
);
// Update the shaders.
updateHueShader();
updateSaturationValueShader();
}
/**
* Updates the hue full spectrum (0-360 degrees).
*/
private void updateHueShader() {
LinearGradient hueShader = new LinearGradient(
hueRect.left, hueRect.top,
hueRect.right, hueRect.top,
HUE_COLORS,
null,
Shader.TileMode.CLAMP
);
huePaint.setShader(hueShader);
}
/**
* Updates the shader for the saturation-value selector based on the currently selected hue.
* This method creates a combined shader that blends a saturation gradient with a value gradient.
*/
private void updateSaturationValueShader() {
// Create a saturation-value gradient based on the current hue.
// Calculate the start color (white with the selected hue) for the saturation gradient.
final int startColor = Color.HSVToColor(new float[]{hue, 0f, 1f});
// Calculate the middle color (fully saturated color with the selected hue) for the saturation gradient.
final int midColor = Color.HSVToColor(new float[]{hue, 1f, 1f});
// Create a linear gradient for the saturation from startColor to midColor (horizontal).
LinearGradient satShader = new LinearGradient(
saturationValueRect.left, saturationValueRect.top,
saturationValueRect.right, saturationValueRect.top,
startColor,
midColor,
Shader.TileMode.CLAMP
);
// Create a linear gradient for the value (brightness) from white to black (vertical).
//noinspection ExtractMethodRecommender
LinearGradient valShader = new LinearGradient(
saturationValueRect.left, saturationValueRect.top,
saturationValueRect.left, saturationValueRect.bottom,
Color.WHITE,
Color.BLACK,
Shader.TileMode.CLAMP
);
// Combine the saturation and value shaders using PorterDuff.Mode.MULTIPLY to create the final color.
ComposeShader combinedShader = new ComposeShader(satShader, valShader, PorterDuff.Mode.MULTIPLY);
// Set the combined shader for the saturation-value paint.
saturationValuePaint.setShader(combinedShader);
}
/**
* Draws the color picker view on the canvas.
* This method draws the saturation-value selector, the hue bar with rounded corners,
* and the draggable handles.
*
* @param canvas The canvas on which to draw.
*/
@Override
protected void onDraw(Canvas canvas) {
// Draw the saturation-value selector rectangle.
canvas.drawRect(saturationValueRect, saturationValuePaint);
// Draw the hue bar.
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
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();
// Draw the saturation and hue selector handle filled with the selected color.
hsvArray[0] = hue;
final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray);
selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
selectorPaint.setColor(hueHandleColor);
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
selectorPaint.setColor(selectedColor | 0xFF000000);
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
// Draw white outlines for the handles.
selectorPaint.setColor(SELECTOR_OUTLINE_COLOR);
selectorPaint.setStyle(Paint.Style.STROKE);
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint);
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint);
// Draw thin dark outlines for the handles at the outer edge of the white outline.
selectorPaint.setColor(SELECTOR_EDGE_COLOR);
selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH);
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
}
/**
* Handles touch events on the view.
* This method determines whether the touch event occurred within the hue bar or the saturation-value selector,
* updates the corresponding values (hue, saturation, value), and invalidates the view to trigger a redraw.
* <p>
* In addition to testing if the touch is within the strict rectangles, an expanded hit area (by selectorRadius)
* is used so that the draggable handles remain active even when half of the handle is outside the drawn bounds.
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
@SuppressLint("ClickableViewAccessibility") // performClick is not overridden, but not needed in this case.
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
final float x = event.getX();
final float y = event.getY();
final int action = event.getAction();
Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y);
// Define touch expansion for the hue bar.
RectF expandedHueRect = new RectF(
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.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();
// Create hit areas for both handles.
RectF hueHitRect = new RectF(
hueSelectorX - SELECTOR_RADIUS,
hueSelectorY - SELECTOR_RADIUS,
hueSelectorX + SELECTOR_RADIUS,
hueSelectorY + SELECTOR_RADIUS
);
RectF satValHitRect = new RectF(
satSelectorX - SELECTOR_RADIUS,
valSelectorY - SELECTOR_RADIUS,
satSelectorX + SELECTOR_RADIUS,
valSelectorY + SELECTOR_RADIUS
);
// Check if the touch started on a handle or within the expanded hue bar area.
if (hueHitRect.contains(x, y)) {
isDraggingHue = true;
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(x);
} else if (saturationValueRect.contains(x, y)) {
isDraggingSaturation = true;
updateSaturationValueFromTouch(x, y);
}
break;
case MotionEvent.ACTION_MOVE:
// Continue updating values even if touch moves outside the view.
if (isDraggingHue) {
updateHueFromTouch(x);
} else if (isDraggingSaturation) {
updateSaturationValueFromTouch(x, y);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
isDraggingHue = false;
isDraggingSaturation = false;
break;
}
} catch (Exception ex) {
Logger.printException(() -> "onTouchEvent failure", ex);
}
return true;
}
/**
* Updates the hue value based on touch position, clamping to valid range.
*
* @param x The x-coordinate of the touch position.
*/
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;
}
hue = updatedHue;
updateSaturationValueShader();
updateSelectedColor();
}
/**
* Updates saturation and value based on touch position, clamping to valid range.
*
* @param x The x-coordinate of the touch position.
* @param y The y-coordinate of the touch position.
*/
private void updateSaturationValueFromTouch(float x, float y) {
// Clamp x and y to the saturation-value rectangle bounds.
final float clampedX = Utils.clamp(x, saturationValueRect.left, saturationValueRect.right);
final float clampedY = Utils.clamp(y, saturationValueRect.top, saturationValueRect.bottom);
final float updatedSaturation = (clampedX - saturationValueRect.left) / saturationValueRect.width();
final float updatedValue = 1 - ((clampedY - saturationValueRect.top) / saturationValueRect.height());
if (saturation == updatedSaturation && value == updatedValue) {
return;
}
saturation = updatedSaturation;
value = updatedValue;
updateSelectedColor();
}
/**
* Updates the selected color and notifies listeners.
*/
private void updateSelectedColor() {
final int updatedColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
if (selectedColor != updatedColor) {
selectedColor = updatedColor;
if (colorChangedListener != null) {
colorChangedListener.onColorChanged(updatedColor);
}
}
// Must always redraw, otherwise if saturation is pure grey or black
// then the hue slider cannot be changed.
invalidate();
}
/**
* Sets the currently selected color.
*
* @param color The color to set in either ARGB or RGB format.
*/
public void setColor(@ColorInt int color) {
color &= 0x00FFFFFF;
if (selectedColor == color) {
return;
}
// Update the selected color.
selectedColor = color;
Logger.printDebug(() -> "setColor: " + getColorString(selectedColor));
// Convert the ARGB color to HSV values.
float[] hsv = new float[3];
Color.colorToHSV(color, hsv);
// Update the hue, saturation, and value.
hue = hsv[0];
saturation = hsv[1];
value = hsv[2];
// Update the saturation-value shader based on the new hue.
updateSaturationValueShader();
// Notify the listener if it's set.
if (colorChangedListener != null) {
colorChangedListener.onColorChanged(selectedColor);
}
// Invalidate the view to trigger a redraw.
invalidate();
}
/**
* Gets the currently selected color.
*
* @return The selected color in RGB format with no alpha channel.
*/
@ColorInt
public int getColor() {
return selectedColor;
}
/**
* Sets the listener to be notified when the selected color changes.
*
* @param listener The listener to set.
*/
public void setOnColorChangedListener(OnColorChangedListener listener) {
colorChangedListener = listener;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,113 @@
package app.revanced.extension.shared.settings.preference;
import static app.revanced.extension.shared.StringRef.str;
import java.util.Deque;
import java.util.Objects;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.atomic.AtomicInteger;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.shared.settings.BaseSettings;
/**
* Manages a buffer for storing debug logs from {@link Logger}.
* Stores just under 1MB of the most recent log data.
*
* All methods are thread-safe.
*/
public final class LogBufferManager {
/** Maximum byte size of all buffer entries. Must be less than Android's 1 MB Binder transaction limit. */
private static final int BUFFER_MAX_BYTES = 900_000;
/** Limit number of log lines. */
private static final int BUFFER_MAX_SIZE = 10_000;
private static final Deque<String> logBuffer = new ConcurrentLinkedDeque<>();
private static final AtomicInteger logBufferByteSize = new AtomicInteger();
/**
* Appends a log message to the internal buffer if debugging is enabled.
* The buffer is limited to approximately {@link #BUFFER_MAX_BYTES} or {@link #BUFFER_MAX_SIZE}
* to prevent excessive memory usage.
*
* @param message The log message to append.
*/
public static void appendToLogBuffer(String message) {
Objects.requireNonNull(message);
// It's very important that no Settings are used in this method,
// as this code is used when a context is not set and thus referencing
// a setting will crash the app.
logBuffer.addLast(message);
int newSize = logBufferByteSize.addAndGet(message.length());
// Remove oldest entries if over the log size limits.
while (newSize > BUFFER_MAX_BYTES || logBuffer.size() > BUFFER_MAX_SIZE) {
String removed = logBuffer.pollFirst();
if (removed == null) {
// Thread race of two different calls to this method, and the other thread won.
return;
}
newSize = logBufferByteSize.addAndGet(-removed.length());
}
}
/**
* Exports all logs from the internal buffer to the clipboard.
* Displays a toast with the result.
*/
public static void exportToClipboard() {
try {
if (!BaseSettings.DEBUG.get()) {
Utils.showToastShort(str("revanced_debug_logs_disabled"));
return;
}
if (logBuffer.isEmpty()) {
Utils.showToastShort(str("revanced_debug_logs_none_found"));
clearLogBufferData(); // Clear toast log entry that was just created.
return;
}
// Most (but not all) Android 13+ devices always show a "copied to clipboard" toast
// and there is no way to programmatically detect if a toast will show or not.
// Show a toast even if using Android 13+, but show ReVanced toast first (before copying to clipboard).
Utils.showToastShort(str("revanced_debug_logs_copied_to_clipboard"));
Utils.setClipboard(String.join("\n", logBuffer));
} catch (Exception ex) {
// Handle security exception if clipboard access is denied.
String errorMessage = String.format(str("revanced_debug_logs_failed_to_export"), ex.getMessage());
Utils.showToastLong(errorMessage);
Logger.printDebug(() -> errorMessage, ex);
}
}
private static void clearLogBufferData() {
// Cannot simply clear the log buffer because there is no
// write lock for both the deque and the atomic int.
// Instead pop off log entries and decrement the size one by one.
while (!logBuffer.isEmpty()) {
String removed = logBuffer.pollFirst();
if (removed != null) {
logBufferByteSize.addAndGet(-removed.length());
}
}
}
/**
* Clears the internal log buffer and displays a toast with the result.
*/
public static void clearLogBuffer() {
if (!BaseSettings.DEBUG.get()) {
Utils.showToastShort(str("revanced_debug_logs_disabled"));
return;
}
// Show toast before clearing, otherwise toast log will still remain.
Utils.showToastShort(str("revanced_debug_logs_clear_toast"));
clearLogBufferData();
}
}

View File

@@ -21,6 +21,10 @@ public class NoTitlePreferenceCategory extends PreferenceCategory {
super(context, attrs, defStyleAttr);
}
public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public NoTitlePreferenceCategory(Context context) {
super(context);
}

View File

@@ -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,17 +9,19 @@ import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
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;
@@ -49,28 +52,6 @@ public class ReVancedAboutPreference extends Preference {
return text.replace("-", "&#8209;"); // #8209 = non breaking hyphen.
}
private static String getColorHexString(int color) {
return String.format("#%06X", (0x00FFFFFF & color));
}
protected boolean isDarkModeEnabled() {
return Utils.isDarkModeEnabled(getContext());
}
/**
* 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.
*
@@ -87,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>",
@@ -221,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 {
@@ -316,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;
}

View File

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

View File

@@ -0,0 +1,124 @@
package app.revanced.extension.shared.settings.preference;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Pair;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import app.revanced.extension.shared.Utils;
/**
* PreferenceList that sorts itself.
* By default the first entry is preserved in its original position,
* and all other entries are sorted alphabetically.
*
* Ideally the 'keep first entries to preserve' is an xml parameter,
* but currently that's not so simple since Extensions code cannot use
* generated code from the Patches repo (which is required for custom xml parameters).
*
* If any class wants to use a different getFirstEntriesToPreserve value,
* it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}.
*/
@SuppressWarnings({"unused", "deprecation"})
public class SortedListPreference extends CustomDialogListPreference {
/**
* Sorts the current list entries.
*
* @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();
CharSequence[] entryValues = getEntryValues();
if (entries == null || entryValues == null) {
return;
}
final int entrySize = entries.length;
if (entrySize != entryValues.length) {
// Xml array declaration has a missing/extra entry.
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.
// Cannot easily use a SortedMap, because if two entries incorrectly have
// identical names then the duplicates entries are not preserved.
List<Pair<String, Pair<CharSequence, CharSequence>>> lastEntries = new ArrayList<>();
for (int i = 0; i < entrySize; i++) {
Pair<CharSequence, CharSequence> pair = new Pair<>(entries[i], entryValues[i]);
if (i < firstEntriesToPreserve) {
firstEntries.add(pair);
} else {
lastEntries.add(new Pair<>(Utils.removePunctuationToLowercase(pair.first), pair));
}
}
//noinspection ComparatorCombinators
Collections.sort(lastEntries, (pair1, pair2)
-> pair1.first.compareTo(pair2.first));
CharSequence[] sortedEntries = new CharSequence[entrySize];
CharSequence[] sortedEntryValues = new CharSequence[entrySize];
int i = 0;
for (Pair<CharSequence, CharSequence> pair : firstEntries) {
sortedEntries[i] = pair.first;
sortedEntryValues[i] = pair.second;
i++;
}
for (Pair<String, Pair<CharSequence, CharSequence>> outer : lastEntries) {
Pair<CharSequence, CharSequence> inner = outer.second;
sortedEntries[i] = inner.first;
sortedEntryValues[i] = inner.second;
i++;
}
super.setEntries(sortedEntries);
super.setEntryValues(sortedEntryValues);
}
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
sortEntryAndValues(getFirstEntriesToPreserve());
}
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
sortEntryAndValues(getFirstEntriesToPreserve());
}
public SortedListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
sortEntryAndValues(getFirstEntriesToPreserve());
}
public SortedListPreference(Context context) {
super(context);
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ package app.revanced.extension.tiktok;
import static app.revanced.extension.shared.Utils.isDarkModeEnabled;
import android.content.Context;
import android.graphics.Color;
import android.view.View;
import android.widget.TextView;
@@ -43,8 +42,8 @@ public class Utils {
private static final @ColorInt int TEXT_LIGHT_MODE_SUMMARY
= Color.argb(255, 80, 80, 80);
public static void setTitleAndSummaryColor(Context context, View view) {
final boolean darkModeEnabled = isDarkModeEnabled(context);
public static void setTitleAndSummaryColor(View view) {
final boolean darkModeEnabled = isDarkModeEnabled();
TextView title = view.findViewById(android.R.id.title);
title.setTextColor(darkModeEnabled

View File

@@ -101,7 +101,7 @@ public class DownloadPathPreference extends DialogPreference {
protected void onBindView(View view) {
super.onBindView(view);
Utils.setTitleAndSummaryColor(getContext(), view);
Utils.setTitleAndSummaryColor(view);
}
@Override

View File

@@ -22,6 +22,6 @@ public class InputTextPreference extends EditTextPreference {
protected void onBindView(View view) {
super.onBindView(view);
Utils.setTitleAndSummaryColor(getContext(), view);
Utils.setTitleAndSummaryColor(view);
}
}

View File

@@ -127,7 +127,7 @@ public class RangeValuePreference extends DialogPreference {
protected void onBindView(View view) {
super.onBindView(view);
Utils.setTitleAndSummaryColor(getContext(), view);
Utils.setTitleAndSummaryColor(view);
}
@Override

View File

@@ -48,6 +48,6 @@ public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference {
protected void onBindView(View view) {
super.onBindView(view);
Utils.setTitleAndSummaryColor(getContext(), view);
Utils.setTitleAndSummaryColor(view);
}
}

View File

@@ -22,6 +22,6 @@ public class TogglePreference extends SwitchPreference {
protected void onBindView(View view) {
super.onBindView(view);
Utils.setTitleAndSummaryColor(getContext(), view);
Utils.setTitleAndSummaryColor(view);
}
}

View File

@@ -16,9 +16,7 @@ public class SpoofSimPatch {
return false;
}
Logger.initializationException(SpoofSimPatch.class,
"Context is not yet set, cannot spoof: " + fieldSpoofed, null);
Logger.printException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null);
return true;
}

View File

@@ -1,124 +0,0 @@
package app.revanced.extension.youtube;
import android.app.Activity;
import android.graphics.Color;
import android.os.Build;
import android.view.Window;
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);
}
}
}

View File

@@ -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.

View File

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

View File

@@ -0,0 +1,35 @@
package app.revanced.extension.youtube.patches;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class DisableHapticFeedbackPatch {
/**
* Injection point.
*/
public static boolean disableChapterVibrate() {
return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get();
}
/**
* Injection point.
*/
public static boolean disableSeekUndoVibrate() {
return Settings.DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO.get();
}
/**
* Injection point.
*/
public static boolean disablePreciseSeekingVibrate() {
return Settings.DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING.get();
}
/**
* Injection point.
*/
public static boolean disableZoomVibrate() {
return Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get();
}
}

View File

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

View File

@@ -0,0 +1,13 @@
package app.revanced.extension.youtube.patches;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public final class HideRelatedVideoOverlayPatch {
/**
* Injection point.
*/
public static boolean hideRelatedVideoOverlay() {
return Settings.HIDE_RELATED_VIDEO_OVERLAY.get();
}
}

View File

@@ -95,7 +95,7 @@ public final class NavigationButtonsPatch {
return false;
}
return Utils.isDarkModeEnabled(Utils.getContext())
return Utils.isDarkModeEnabled()
? !DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
: !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT;
}

View File

@@ -31,6 +31,8 @@ public class OpenShortsInRegularPlayerPatch {
private static WeakReference<Activity> mainActivityRef = new WeakReference<>(null);
private static volatile boolean overrideBackPressToExit;
/**
* Injection point.
*/
@@ -38,6 +40,18 @@ public class OpenShortsInRegularPlayerPatch {
mainActivityRef = new WeakReference<>(activity);
}
/**
* Injection point.
*/
public static boolean overrideBackPressToExit(boolean original) {
if (overrideBackPressToExit) {
Logger.printDebug(() -> "Overriding back press to exit activity");
return false;
}
return original;
}
/**
* Injection point.
*/
@@ -45,6 +59,7 @@ public class OpenShortsInRegularPlayerPatch {
try {
ShortsPlayerType type = Settings.SHORTS_PLAYER_TYPE.get();
if (type == ShortsPlayerType.SHORTS_PLAYER) {
overrideBackPressToExit = false;
return false; // Default unpatched behavior.
}
@@ -61,13 +76,17 @@ public class OpenShortsInRegularPlayerPatch {
// set to open in the regular player, so it's ignored as
// checking the map makes the patch more complicated.
Logger.printDebug(() -> "Ignoring Short with no videoId");
overrideBackPressToExit = false;
return false;
}
if (NavigationButton.getSelectedNavigationButton() == NavigationButton.SHORTS) {
overrideBackPressToExit = false;
return false; // Always use Shorts player for the Shorts nav button.
}
overrideBackPressToExit = true;
final boolean forceFullScreen = (type == ShortsPlayerType.REGULAR_PLAYER_FULLSCREEN);
OpenVideosFullscreenHookPatch.setOpenNextVideoFullscreen(forceFullScreen);

View File

@@ -33,7 +33,7 @@ public class OpenVideosFullscreenHookPatch {
}
if (!isFullScreenPatchIncluded()) {
return false;
return original;
}
return Settings.OPEN_VIDEOS_FULLSCREEN_PORTRAIT.get();

View File

@@ -18,7 +18,6 @@ import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch;
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@@ -69,13 +68,6 @@ public class ReturnYouTubeDislikePatch {
@Nullable
private static volatile String lastPrefetchedVideoId;
public static void onRYDStatusChange(boolean rydEnabled) {
ReturnYouTubeDislikeApi.resetRateLimits();
// Must remove all values to protect against using stale data
// if the user enables RYD while a video is on screen.
clearData();
}
private static void clearData() {
currentVideoData = null;
lastLithoShortsVideoData = null;
@@ -160,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);
@@ -274,7 +268,7 @@ public class ReturnYouTubeDislikePatch {
Logger.printDebug(() -> "Adding rolling number TextView changes");
view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels);
ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable();
if (Utils.isRightToLeftTextLayout()) {
if (Utils.isRightToLeftLocale()) {
view.setCompoundDrawables(null, null, separator, null);
} else {
view.setCompoundDrawables(separator, null, null, null);
@@ -369,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,
@@ -423,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.

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
package app.revanced.extension.youtube.patches;
import android.content.res.Resources;
import android.util.TypedValue;
import android.view.View;
import app.revanced.extension.shared.Logger;
@@ -33,10 +31,9 @@ public final class WideSearchbarPatch {
final int paddingRight = searchBarView.getPaddingRight();
final int paddingTop = searchBarView.getPaddingTop();
final int paddingBottom = searchBarView.getPaddingBottom();
final int paddingStart = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
8, Resources.getSystem().getDisplayMetrics());
final int paddingStart = Utils.dipToPixels(8);
if (Utils.isRightToLeftTextLayout()) {
if (Utils.isRightToLeftLocale()) {
searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom);
} else {
searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom);

View File

@@ -1,10 +0,0 @@
package app.revanced.extension.youtube.patches;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class ZoomHapticsPatch {
public static boolean shouldVibrate() {
return !Settings.DISABLE_ZOOM_HAPTICS.get();
}
}

View File

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

View File

@@ -64,48 +64,45 @@ public final class AdsFilter extends Filter {
"_interstitial"
);
final var buttonedAd = new StringFilterGroup(
Settings.HIDE_BUTTONED_ADS,
"_ad_with",
"_buttoned_layout",
// text_image_button_group_layout, landscape_image_button_group_layout, full_width_square_image_button_group_layout
"image_button_group_layout",
"full_width_square_image_layout",
"video_display_button_group_layout",
"landscape_image_wide_button_layout",
"video_display_carousel_button_group_layout",
"video_display_full_buttoned_short_dr_layout",
"compact_landscape_image_layout", // Tablet layout search results.
"text_image_no_button_layout" // Tablet layout search results.
);
final var generalAds = new StringFilterGroup(
Settings.HIDE_GENERAL_ADS,
"_ad_with",
"_buttoned_layout",
"ads_video_with_context",
"banner_text_icon",
"square_image_layout",
"watch_metadata_app_promo",
"video_display_full_layout",
"hero_promo_image",
"statement_banner",
"brand_video_shelf",
"brand_video_singleton",
"carousel_footered_layout",
"text_image_button_layout",
"carousel_headered_layout",
"compact_landscape_image_layout", // Tablet layout search results.
"composite_concurrent_carousel_layout",
"full_width_portrait_image_layout",
"full_width_square_image_carousel_layout",
"full_width_square_image_layout",
"hero_promo_image",
// text_image_button_group_layout, landscape_image_button_group_layout, full_width_square_image_button_group_layout
"image_button_group_layout",
"landscape_image_wide_button_layout",
"primetime_promo",
"product_details",
"composite_concurrent_carousel_layout",
"carousel_headered_layout",
"full_width_portrait_image_layout",
"brand_video_shelf",
"brand_video_singleton"
"square_image_layout",
"statement_banner",
"text_image_button_layout",
"text_image_no_button_layout", // Tablet layout search results.
"video_display_button_group_layout",
"video_display_carousel_button_group_layout",
"video_display_full_buttoned_short_dr_layout",
"video_display_full_layout",
"watch_metadata_app_promo"
);
final var movieAds = new StringFilterGroup(
Settings.HIDE_MOVIES_SECTION,
"browsy_bar",
"compact_movie",
"compact_tvfilm_item",
"horizontal_movie_shelf",
"movie_and_show_upsell_card",
"compact_tvfilm_item",
"offer_module_root"
);
@@ -124,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(
@@ -160,7 +159,6 @@ public final class AdsFilter extends Filter {
addPathCallbacks(
generalAds,
buttonedAd,
merchandise,
viewProducts,
selfSponsor,
@@ -177,34 +175,30 @@ public final class AdsFilter extends Filter {
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == playerShoppingShelf) {
if (contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
return contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered();
}
// Check for the index because of likelihood of false positives.
if (matchedGroup == shoppingLinks && contentIndex != 0) {
if (contentIndex != 0 && matchedGroup == shoppingLinks) {
return false;
}
if (exceptions.matches(path))
if (exceptions.matches(path)) {
return false;
}
if (matchedGroup == fullscreenAd) {
if (path.contains("|ImageType|")) closeFullscreenAd();
return false; // Do not actually filter the fullscreen ad otherwise it will leave a dimmed screen.
}
if (matchedGroup == channelProfile) {
if (visitStoreButton.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
// Do not actually filter the fullscreen ad otherwise it will leave a dimmed screen.
return false;
}
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
if (matchedGroup == channelProfile) {
return visitStoreButton.check(protobufBufferArray).isFiltered();
}
return true;
}
/**

View File

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

View File

@@ -99,29 +99,23 @@ final class ButtonsFilter extends Filter {
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == likeSubscribeGlow) {
if ((path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
&& path.contains(ANIMATED_VECTOR_TYPE_PATH)) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
&& path.contains(ANIMATED_VECTOR_TYPE_PATH);
}
// If the current matched group is the action bar group,
// in case every filter group is enabled, hide the action bar.
if (matchedGroup == actionBarGroup) {
if (!isEveryFilterGroupEnabled()) {
return false;
}
} else if (matchedGroup == bufferFilterPathGroup) {
// Make sure the current path is the right one
// to avoid false positives.
if (!path.startsWith(VIDEO_ACTION_BAR_PATH)) return false;
// In case the group list has no match, return false.
if (!bufferButtonsGroupList.check(protobufBufferArray).isFiltered()) return false;
return isEveryFilterGroupEnabled();
}
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
if (matchedGroup == bufferFilterPathGroup) {
// Make sure the current path is the right one
// to avoid false positives.
return path.startsWith(VIDEO_ACTION_BAR_PATH)
&& bufferButtonsGroupList.check(protobufBufferArray).isFiltered();
}
return true;
}
}

View File

@@ -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,25 +75,10 @@ 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.
if (contentIndex == 0
&& path.endsWith(TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH)
&& emojiPickerBufferGroup.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
if (matchedGroup == filterChipBar) {
if (aiCommentsSummary.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
return aiCommentsSummary.check(protobufBufferArray).isFiltered();
}
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
return true;
}
}

View File

@@ -153,9 +153,11 @@ final class CustomFilter extends Filter {
if (custom.startsWith && contentIndex != 0) {
return false;
}
if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) {
return false;
if (custom.bufferSearch == null) {
return true; // No buffer filter, only path filtering.
}
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
return custom.bufferSearch.matches(protobufBufferArray);
}
}

View File

@@ -28,6 +28,11 @@ final class DescriptionComponentsFilter extends Filter {
"cell_expandable_metadata.eml"
);
final StringFilterGroup askSection = new StringFilterGroup(
Settings.HIDE_ASK_SECTION,
"youchat_entrypoint.eml"
);
final StringFilterGroup attributesSection = new StringFilterGroup(
Settings.HIDE_ATTRIBUTES_SECTION,
"gaming_section",
@@ -73,6 +78,7 @@ final class DescriptionComponentsFilter extends Filter {
addPathCallbacks(
aiGeneratedVideoSummarySection,
askSection,
attributesSection,
infoCardsSection,
howThisWasMadeSection,
@@ -88,13 +94,9 @@ final class DescriptionComponentsFilter extends Filter {
if (exceptions.matches(path)) return false;
if (matchedGroup == macroMarkersCarousel) {
if (contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
return contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered();
}
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
return true;
}
}

View File

@@ -6,9 +6,6 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.BaseSettings;
/**
* Filters litho based components.
*
@@ -62,10 +59,7 @@ abstract class Filter {
* Called after an enabled filter has been matched.
* Default implementation is to always filter the matched component and log the action.
* Subclasses can perform additional or different checks if needed.
* <p>
* If the content is to be filtered, subclasses should always
* call this method (and never return a plain 'true').
* That way the logs will always show when a component was filtered and which filter hide it.
*
* <p>
* Method is called off the main thread.
*
@@ -76,14 +70,6 @@ abstract class Filter {
*/
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (BaseSettings.DEBUG.get()) {
String filterSimpleName = getClass().getSimpleName();
if (contentType == FilterContentType.IDENTIFIER) {
Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier);
} else {
Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path);
}
}
return true;
}
}

View File

@@ -576,7 +576,7 @@ final class KeywordContentFilter extends Filter {
MutableReference<String> matchRef = new MutableReference<>();
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
updateStats(true, matchRef.value);
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
return true;
}
updateStats(false, null);

View File

@@ -34,12 +34,11 @@ public final class LayoutComponentsFilter extends Filter {
private final StringFilterGroup notifyMe;
private final StringFilterGroup singleItemInformationPanel;
private final StringFilterGroup expandableMetadata;
private final ByteArrayFilterGroup searchResultRecommendations;
private final StringFilterGroup searchResultVideo;
private final StringFilterGroup compactChannelBarInner;
private final StringFilterGroup compactChannelBarInnerButton;
private final ByteArrayFilterGroup joinMembershipButton;
private final StringFilterGroup horizontalShelves;
private final ByteArrayFilterGroup ticketShelf;
public LayoutComponentsFilter() {
exceptions.addPatterns(
@@ -233,14 +232,9 @@ public final class LayoutComponentsFilter extends Filter {
"mixed_content_shelf"
);
searchResultVideo = new StringFilterGroup(
Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS,
"search_video_with_context.eml"
);
searchResultRecommendations = new ByteArrayFilterGroup(
Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS,
"endorsement_header_footer"
final var searchResultRecommendationLabels = new StringFilterGroup(
Settings.HIDE_SEARCH_RESULT_RECOMMENDATION_LABELS,
"endorsement_header_footer.eml"
);
horizontalShelves = new StringFilterGroup(
@@ -251,6 +245,11 @@ public final class LayoutComponentsFilter extends Filter {
"horizontal_tile_shelf.eml"
);
ticketShelf = new ByteArrayFilterGroup(
Settings.HIDE_TICKET_SHELF,
"ticket"
);
addPathCallbacks(
expandableMetadata,
inFeedSurvey,
@@ -258,7 +257,7 @@ public final class LayoutComponentsFilter extends Filter {
compactChannelBar,
communityPosts,
paidPromotion,
searchResultVideo,
searchResultRecommendationLabels,
latestPosts,
channelWatermark,
communityGuidelines,
@@ -293,50 +292,29 @@ public final class LayoutComponentsFilter extends Filter {
// From 2025, the medical information panel is no longer shown in the search results.
// Therefore, this identifier does not filter when the search bar is activated.
if (matchedGroup == singleItemInformationPanel) {
if (PlayerType.getCurrent().isMaximizedOrFullscreen() || !NavigationBar.isSearchBarActive()) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
}
if (matchedGroup == searchResultVideo) {
if (searchResultRecommendations.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
return PlayerType.getCurrent().isMaximizedOrFullscreen() || !NavigationBar.isSearchBarActive();
}
// The groups are excluded from the filter due to the exceptions list below.
// Filter them separately here.
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata)
{
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) {
return true;
}
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
if (matchedGroup == compactChannelBarInner) {
if (compactChannelBarInnerButton.check(path).isFiltered()) {
// The filter may be broad, but in the context of a compactChannelBarInnerButton,
// it's safe to assume that the button is the only thing that should be hidden.
if (joinMembershipButton.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
}
return false;
return compactChannelBarInnerButton.check(path).isFiltered()
// The filter may be broad, but in the context of a compactChannelBarInnerButton,
// it's safe to assume that the button is the only thing that should be hidden.
&& joinMembershipButton.check(protobufBufferArray).isFiltered();
}
if (matchedGroup == horizontalShelves) {
if (contentIndex == 0 && hideShelves()) {
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
return contentIndex == 0 && (hideShelves() || ticketShelf.check(protobufBufferArray).isFiltered());
}
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
return true;
}
/**

View File

@@ -7,6 +7,7 @@ import java.nio.ByteBuffer;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.youtube.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
@@ -73,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.
};
@@ -114,12 +136,29 @@ public final class LithoFilterPatch {
if (!group.includeInSearch()) {
continue;
}
for (String pattern : group.filters) {
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
String filterSimpleName = filter.getClass().getSimpleName();
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex,
matchedLength, callbackParameter) -> {
if (!group.isEnabled()) return false;
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
return filter.isFiltered(parameters.identifier, parameters.path, parameters.protoBuffer,
group, type, matchedStartIndex);
final boolean isFiltered = filter.isFiltered(parameters.identifier,
parameters.path, parameters.protoBuffer, group, type, matchedStartIndex);
if (isFiltered && BaseSettings.DEBUG.get()) {
if (type == Filter.FilterContentType.IDENTIFIER) {
Logger.printDebug(() -> "Filtered " + filterSimpleName
+ " identifier: " + parameters.identifier);
} else {
Logger.printDebug(() -> "Filtered " + filterSimpleName
+ " path: " + parameters.path);
}
}
return isFiltered;
}
);
}
@@ -195,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;
}
}

View File

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

View File

@@ -99,7 +99,7 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == videoQualityMenuFooter) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
return true;
}
if (contentIndex != 0) {
@@ -111,11 +111,6 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
return false;
}
if (flyoutFilterGroupList.check(protobufBufferArray).isFiltered()) {
// Super class handles logging.
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
return flyoutFilterGroupList.check(protobufBufferArray).isFiltered();
}
}

View File

@@ -143,12 +143,14 @@ public final class ShortsFilter extends Filter {
StringFilterGroup likeButton = new StringFilterGroup(
Settings.HIDE_SHORTS_LIKE_BUTTON,
"shorts_like_button.eml"
"shorts_like_button.eml",
"reel_like_button.eml"
);
StringFilterGroup dislikeButton = new StringFilterGroup(
Settings.HIDE_SHORTS_DISLIKE_BUTTON,
"shorts_dislike_button.eml"
"shorts_dislike_button.eml",
"reel_dislike_button.eml"
);
joinButton = new StringFilterGroup(
@@ -168,12 +170,13 @@ public final class ShortsFilter extends Filter {
shortsActionBar = new StringFilterGroup(
null,
"shorts_action_bar.eml"
"shorts_action_bar.eml",
"reel_action_bar.eml"
);
actionButton = new StringFilterGroup(
null,
// Can be simply 'button.eml' or 'shorts_video_action_button.eml'
// Can be simply 'button.eml', 'shorts_video_action_button.eml' or 'reel_action_button.eml'
"button.eml"
);
@@ -195,15 +198,18 @@ public final class ShortsFilter extends Filter {
videoActionButtonGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
"reel_comment_button"
"reel_comment_button",
"youtube_shorts_comment_outline"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SHARE_BUTTON,
"reel_share_button"
"reel_share_button",
"youtube_shorts_share_outline"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_REMIX_BUTTON,
"reel_remix_button"
"reel_remix_button",
"youtube_shorts_remix_outline"
)
);
@@ -211,6 +217,12 @@ public final class ShortsFilter extends Filter {
// Suggested actions.
//
suggestedActionsGroupList.addAll(
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_PREVIEW_COMMENT,
// Preview comment that can popup while a Short is playing.
// Uses no bundled icons, and instead the users profile photo is shown.
"shorts-comments-panel"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_SHOP_BUTTON,
"yt_outline_bag_"
@@ -255,6 +267,10 @@ public final class ShortsFilter extends Filter {
Settings.HIDE_SHORTS_GREEN_SCREEN_BUTTON,
"greenscreen_temp"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_NEW_POSTS_BUTTON,
"yt_outline_box_pencil"
),
new ByteArrayFilterGroup(
Settings.HIDE_SHORTS_HASHTAG_BUTTON,
"yt_outline_hashtag_"
@@ -278,27 +294,18 @@ public final class ShortsFilter extends Filter {
if (contentType == FilterContentType.PATH) {
if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
// Selectively filter to avoid false positive filtering of other subscribe/join buttons.
if (path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH)) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
return path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH);
}
if (matchedGroup == shortsCompactFeedVideoPath) {
if (shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
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) {
if (actionButton.check(path).isFiltered()
&& videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
return actionButton.check(path).isFiltered()
&& videoActionButtonGroupList.check(protobufBufferArray).isFiltered();
}
if (matchedGroup == suggestedAction) {
@@ -306,28 +313,23 @@ public final class ShortsFilter extends Filter {
// This has a secondary effect of hiding all new un-identified actions
// under the assumption that the user wants all suggestions hidden.
if (isEverySuggestedActionFilterEnabled()) {
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
return true;
}
if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) {
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
}
return false;
return suggestedActionsGroupList.check(protobufBufferArray).isFiltered();
}
} else {
// Feed/search identifier components.
if (matchedGroup == shelfHeader) {
// Because the header is used in watch history and possibly other places, check for the index,
// which is 0 when the shelf header is used for Shorts.
if (contentIndex != 0) return false;
}
if (!shouldHideShortsFeedItems()) return false;
return true;
}
// Super class handles logging.
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
// Feed/search identifier components.
if (matchedGroup == shelfHeader) {
// Because the header is used in watch history and possibly other places, check for the index,
// which is 0 when the shelf header is used for Shorts.
if (contentIndex != 0) return false;
}
return shouldHideShortsFeedItems();
}
private static boolean shouldHideShortsFeedItems() {

View File

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

View File

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

View File

@@ -1,30 +1,58 @@
package app.revanced.extension.youtube.patches.playback.speed;
import static app.revanced.extension.shared.StringRef.sf;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.dipToPixels;
import android.preference.ListPreference;
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 androidx.annotation.NonNull;
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 {
private static final float PLAYBACK_SPEED_AUTO = Settings.PLAYBACK_SPEED_DEFAULT.defaultValue;
/**
* 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.
@@ -32,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.
*/
@@ -40,21 +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();
/**
* PreferenceList entries and values, of all available playback speeds.
* Weak reference to the currently open dialog.
*/
private static String[] preferenceListEntries, preferenceListEntryValues;
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 {
@@ -62,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];
}
/**
@@ -76,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();
}
}
@@ -117,33 +163,6 @@ public class CustomPlaybackSpeedPatch {
return false;
}
/**
* Initialize a settings preference list with the available playback speeds.
*/
@SuppressWarnings("deprecation")
public static void initializeListPreference(ListPreference preference) {
if (preferenceListEntries == null) {
final int numberOfEntries = customPlaybackSpeeds.length + 1;
preferenceListEntries = new String[numberOfEntries];
preferenceListEntryValues = new String[numberOfEntries];
// Auto speed (same behavior as unpatched).
preferenceListEntries[0] = sf("revanced_custom_playback_speeds_auto").toString();
preferenceListEntryValues[0] = String.valueOf(PLAYBACK_SPEED_AUTO);
int i = 1;
for (float speed : customPlaybackSpeeds) {
String speedString = String.valueOf(speed);
preferenceListEntries[i] = speedString + "x";
preferenceListEntryValues[i] = speedString;
i++;
}
}
preference.setEntries(preferenceListEntries);
preference.setEntryValues(preferenceListEntryValues);
}
/**
* Injection point.
*/
@@ -151,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;
}
@@ -206,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;
}
}

View File

@@ -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) {

View File

@@ -20,13 +20,16 @@ import app.revanced.extension.youtube.settings.Settings;
public class ProgressBarDrawable extends Drawable {
private final Paint paint = new Paint();
{
paint.setColor(SeekbarColorPatch.getSeekbarColor());
}
@Override
public void draw(@NonNull Canvas canvas) {
if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) {
return;
}
paint.setColor(SeekbarColorPatch.getSeekbarColor());
canvas.drawRect(getBounds(), paint);
}

View File

@@ -1,6 +1,8 @@
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;
@@ -59,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.
@@ -76,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();
}
}
@@ -113,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));
@@ -170,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(Utils.getContext())
? "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]";
@@ -196,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);
}
@@ -231,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);
}
}
@@ -378,14 +368,4 @@ public final class SeekbarColorPatch {
return originalColor;
}
}
/** @noinspection SameParameterValue */
private static int clamp(int value, int lower, int upper) {
return Math.max(lower, Math.min(value, upper));
}
/** @noinspection SameParameterValue */
private static float clamp(float value, float lower, float upper) {
return Math.max(lower, Math.min(value, upper));
}
}

View File

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

View File

@@ -21,8 +21,6 @@ import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import android.text.style.ImageSpan;
import android.text.style.ReplacementSpan;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
@@ -32,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;
@@ -120,16 +122,13 @@ public class ReturnYouTubeDislike {
private static final ShapeDrawable leftSeparatorShape;
static {
DisplayMetrics dp = Objects.requireNonNull(Utils.getContext()).getResources().getDisplayMetrics();
leftSeparatorBounds = new Rect(0, 0,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp),
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dp));
final int middleSeparatorSize =
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
Utils.dipToPixels(1.2f),
Utils.dipToPixels(14f));
final int middleSeparatorSize = Utils.dipToPixels(3.7f);
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.4f, dp);
leftSeparatorShapePaddingPixels = Utils.dipToPixels(8.4f);
leftSeparatorShape = new ShapeDrawable(new RectShape());
leftSeparatorShape.setBounds(leftSeparatorBounds);
@@ -182,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;
}
@@ -235,7 +234,7 @@ public class ReturnYouTubeDislike {
final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
if (!compactLayout) {
String leftSeparatorString = getTextDirectionString();
String leftSeparatorString = Utils.getTextDirectionString();
final Spannable leftSeparatorSpan;
if (isRollingNumber) {
leftSeparatorSpan = new SpannableString(leftSeparatorString);
@@ -279,12 +278,6 @@ public class ReturnYouTubeDislike {
return new SpannableString(builder);
}
private static @NonNull String getTextDirectionString() {
return Utils.isRightToLeftTextLayout()
? "\u200F" // u200F = right to left character
: "\u200E"; // u200E = left to right character
}
/**
* @return If the text is likely for a previously created likes/dislikes segmented span.
*/

View File

@@ -0,0 +1,29 @@
package app.revanced.extension.youtube.returnyoutubedislike.ui;
import android.content.Context;
import android.util.AttributeSet;
import app.revanced.extension.youtube.settings.preference.UrlLinkPreference;
/**
* Allows tapping the RYD about preference to open the website.
*/
@SuppressWarnings("unused")
public class ReturnYouTubeDislikeAboutPreference extends UrlLinkPreference {
{
externalUrl = "https://returnyoutubedislike.com";
}
public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReturnYouTubeDislikeAboutPreference(Context context) {
super(context);
}
}

View File

@@ -0,0 +1,126 @@
package app.revanced.extension.youtube.returnyoutubedislike.ui;
import static app.revanced.extension.shared.StringRef.str;
import android.content.Context;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.settings.BaseSettings;
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
@SuppressWarnings({"unused", "deprecation"})
public class ReturnYouTubeDislikeDebugStatsPreferenceCategory extends PreferenceCategory {
private static final boolean SHOW_RYD_DEBUG_STATS = BaseSettings.DEBUG.get();
private static String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) {
if (value == 0) {
return str(summaryStringZeroKey);
}
return str(summaryStringOneOrMoreKey, value);
}
private static String createMillisecondStringFromNumber(long number) {
return String.format(str("revanced_ryd_statistics_millisecond_text"), number);
}
public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
@Override
protected View onCreateView(ViewGroup parent) {
if (!SHOW_RYD_DEBUG_STATS) {
// Use an empty view to hide without removing.
return new View(getContext());
}
return super.onCreateView(parent);
}
protected void onAttachedToActivity() {
try {
super.onAttachedToActivity();
if (!SHOW_RYD_DEBUG_STATS) {
return;
}
Logger.printDebug(() -> "Updating stats preferences");
removeAll();
addStatisticPreference(
"revanced_ryd_statistics_getFetchCallResponseTimeAverage_title",
createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage())
);
addStatisticPreference(
"revanced_ryd_statistics_getFetchCallResponseTimeMin_title",
createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin())
);
addStatisticPreference(
"revanced_ryd_statistics_getFetchCallResponseTimeMax_title",
createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax())
);
String fetchCallTimeWaitingLastSummary;
final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast();
if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) {
fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary");
} else {
fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast);
}
addStatisticPreference(
"revanced_ryd_statistics_getFetchCallResponseTimeLast_title",
fetchCallTimeWaitingLastSummary
);
addStatisticPreference(
"revanced_ryd_statistics_getFetchCallCount_title",
createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(),
"revanced_ryd_statistics_getFetchCallCount_zero_summary",
"revanced_ryd_statistics_getFetchCallCount_non_zero_summary"
)
);
addStatisticPreference(
"revanced_ryd_statistics_getFetchCallNumberOfFailures_title",
createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(),
"revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary",
"revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary"
)
);
addStatisticPreference(
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title",
createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(),
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary",
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary"
)
);
} catch (Exception ex) {
Logger.printException(() -> "onAttachedToActivity failure", ex);
}
}
private void addStatisticPreference(String titleKey, String SummaryText) {
Preference statisticPreference = new Preference(getContext());
statisticPreference.setSelectable(false);
statisticPreference.setTitle(str(titleKey));
statisticPreference.setSummary(SummaryText);
addPreference(statisticPreference);
}
}

View File

@@ -6,23 +6,17 @@ import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.preference.PreferenceFragment;
import android.util.TypedValue;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toolbar;
import java.util.Objects;
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;
import app.revanced.extension.youtube.settings.preference.ReturnYouTubeDislikePreferenceFragment;
import app.revanced.extension.youtube.settings.preference.SponsorBlockPreferenceFragment;
/**
* Hooks LicenseActivity.
@@ -32,6 +26,8 @@ import app.revanced.extension.youtube.settings.preference.SponsorBlockPreference
@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) {
@@ -83,33 +79,20 @@ 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"));
PreferenceFragment fragment;
String toolbarTitleResourceName;
String dataString = Objects.requireNonNull(licenseActivity.getIntent().getDataString());
switch (dataString) {
case "revanced_sb_settings_intent":
toolbarTitleResourceName = "revanced_sb_settings_title";
fragment = new SponsorBlockPreferenceFragment();
break;
case "revanced_ryd_settings_intent":
toolbarTitleResourceName = "revanced_ryd_settings_title";
fragment = new ReturnYouTubeDislikePreferenceFragment();
break;
case "revanced_settings_intent":
toolbarTitleResourceName = "revanced_settings_title";
fragment = new ReVancedPreferenceFragment();
break;
default:
Logger.printException(() -> "Unknown setting: " + dataString);
return;
// Sanity check.
String dataString = licenseActivity.getIntent().getDataString();
if (!"revanced_settings_intent".equals(dataString)) {
Logger.printException(() -> "Unknown intent: " + dataString);
return;
}
createToolbar(licenseActivity, toolbarTitleResourceName);
PreferenceFragment fragment = new ReVancedPreferenceFragment();
createToolbar(licenseActivity, fragment);
//noinspection deprecation
licenseActivity.getFragmentManager()
@@ -122,7 +105,7 @@ public class LicenseActivityHook {
}
@SuppressLint("UseCompatLoadingForDrawables")
private static void createToolbar(Activity activity, String toolbarTitleResourceName) {
private static void createToolbar(Activity activity, PreferenceFragment fragment) {
// Replace dummy placeholder toolbar.
// This is required to fix submenu title alignment issue with Android ASOP 15+
ViewGroup toolBarParent = activity.findViewById(
@@ -132,22 +115,55 @@ public class LicenseActivityHook {
toolBarParent.removeView(dummyToolbar);
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
toolbar.setBackgroundColor(ThemeHelper.getToolbarBackgroundColor());
toolbar.setBackgroundColor(getToolbarBackgroundColor());
toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable());
toolbar.setNavigationOnClickListener(view -> activity.onBackPressed());
toolbar.setTitle(getResourceIdentifier(toolbarTitleResourceName, "string"));
toolbar.setTitle(getResourceIdentifier("revanced_settings_title", "string"));
final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
Utils.getContext().getResources().getDisplayMetrics());
final int margin = Utils.dipToPixels(16);
toolbar.setTitleMarginStart(margin);
toolbar.setTitleMarginEnd(margin);
TextView toolbarTextView = Utils.getChildView(toolbar, false,
view -> view instanceof TextView);
if (toolbarTextView != null) {
toolbarTextView.setTextColor(ThemeHelper.getForegroundColor());
toolbarTextView.setTextColor(Utils.getAppForegroundColor());
}
setToolbarLayoutParams(toolbar);
// Add Search Icon and EditText for ReVancedPreferenceFragment only.
if (fragment instanceof ReVancedPreferenceFragment) {
SearchViewController.addSearchViewComponents(activity, toolbar, (ReVancedPreferenceFragment) fragment);
}
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);
}
}
}

View File

@@ -0,0 +1,385 @@
package app.revanced.extension.youtube.settings;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
import android.app.Activity;
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;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.SearchView;
import android.widget.TextView;
import android.widget.Toolbar;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
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.shared.settings.StringSetting;
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
/**
* Controller for managing the search view in ReVanced settings.
*/
@SuppressWarnings({"deprecated", "DiscouragedApi"})
public class SearchViewController {
private static final int MAX_HISTORY_SIZE = 5;
private final SearchView searchView;
private final FrameLayout searchContainer;
private final Toolbar toolbar;
private final Activity activity;
private boolean isSearchActive;
private final CharSequence originalTitle;
private final Deque<String> searchHistory;
private final AutoCompleteTextView autoCompleteTextView;
private final boolean showSettingsSearchHistory;
/**
* Creates a background drawable for the SearchView with rounded corners.
*/
private static GradientDrawable createBackgroundDrawable(Context context) {
GradientDrawable background = new GradientDrawable();
background.setShape(GradientDrawable.RECTANGLE);
background.setCornerRadius(28 * context.getResources().getDisplayMetrics().density); // 28dp corner radius.
background.setColor(getSearchViewBackground());
return background;
}
/**
* Creates a background drawable for suggestion items with rounded corners.
*/
private static GradientDrawable createSuggestionBackgroundDrawable(Context context) {
GradientDrawable background = new GradientDrawable();
background.setShape(GradientDrawable.RECTANGLE);
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.
*/
public static void addSearchViewComponents(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
new SearchViewController(activity, toolbar, fragment);
}
private SearchViewController(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
this.activity = activity;
this.toolbar = toolbar;
this.originalTitle = toolbar.getTitle();
this.showSettingsSearchHistory = Settings.SETTINGS_SEARCH_HISTORY.get();
this.searchHistory = new LinkedList<>();
StringSetting searchEntries = Settings.SETTINGS_SEARCH_ENTRIES;
if (showSettingsSearchHistory) {
String entries = searchEntries.get();
if (!entries.isBlank()) {
searchHistory.addAll(Arrays.asList(entries.split("\n")));
}
} else {
// Clear old saved history if the user turns off the feature.
searchEntries.resetToDefault();
}
// Retrieve SearchView and container from XML.
searchView = activity.findViewById(getResourceIdentifier(
"revanced_search_view", "id"));
searchContainer = activity.findViewById(getResourceIdentifier(
"revanced_search_view_container", "id"));
// Initialize AutoCompleteTextView.
autoCompleteTextView = searchView.findViewById(
searchView.getContext().getResources().getIdentifier(
"android:id/search_src_text", null, null));
// Set background and query hint.
searchView.setBackground(createBackgroundDrawable(toolbar.getContext()));
searchView.setQueryHint(str("revanced_settings_search_hint"));
// Configure RTL support based on app language.
AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get();
if (Utils.isRightToLeftLocale(appLanguage.getLocale())) {
searchView.setTextDirection(View.TEXT_DIRECTION_RTL);
searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
}
// Set up search history suggestions.
if (showSettingsSearchHistory) {
setupSearchHistory();
}
// Set up query text listener.
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
try {
String queryTrimmed = query.trim();
if (!queryTrimmed.isEmpty()) {
saveSearchQuery(queryTrimmed);
}
// Hide suggestions on submit.
if (showSettingsSearchHistory && autoCompleteTextView != null) {
autoCompleteTextView.dismissDropDown();
}
} catch (Exception ex) {
Logger.printException(() -> "onQueryTextSubmit failure", ex);
}
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
try {
Logger.printDebug(() -> "Search query: " + newText);
fragment.filterPreferences(newText);
// Prevent suggestions from showing during text input.
if (showSettingsSearchHistory && autoCompleteTextView != null) {
if (!newText.isEmpty()) {
autoCompleteTextView.dismissDropDown();
autoCompleteTextView.setThreshold(Integer.MAX_VALUE); // Disable autocomplete suggestions.
} else {
autoCompleteTextView.setThreshold(1); // Re-enable for empty input.
}
}
} catch (Exception ex) {
Logger.printException(() -> "onQueryTextChange failure", ex);
}
return true;
}
});
// Set menu and search icon.
final int actionSearchId = getResourceIdentifier("action_search", "id");
toolbar.inflateMenu(getResourceIdentifier("revanced_search_menu", "menu"));
MenuItem searchItem = toolbar.getMenu().findItem(actionSearchId);
// Set menu item click listener.
toolbar.setOnMenuItemClickListener(item -> {
try {
if (item.getItemId() == actionSearchId) {
if (!isSearchActive) {
openSearch();
}
return true;
}
} catch (Exception ex) {
Logger.printException(() -> "menu click failure", ex);
}
return false;
});
// Set navigation click listener.
toolbar.setNavigationOnClickListener(view -> {
try {
if (isSearchActive) {
closeSearch();
} else {
activity.onBackPressed();
}
} catch (Exception ex) {
Logger.printException(() -> "navigation click failure", ex);
}
});
}
/**
* Sets up the search history suggestions for the SearchView with custom adapter.
*/
private void setupSearchHistory() {
if (autoCompleteTextView != null) {
SearchHistoryAdapter adapter = new SearchHistoryAdapter(activity, new ArrayList<>(searchHistory));
autoCompleteTextView.setAdapter(adapter);
autoCompleteTextView.setThreshold(1); // Initial threshold for empty input.
autoCompleteTextView.setLongClickable(true);
// Show suggestions only when search bar is active and query is empty.
autoCompleteTextView.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus && isSearchActive && autoCompleteTextView.getText().length() == 0) {
autoCompleteTextView.showDropDown();
}
});
}
}
/**
* Saves a search query to the search history.
* @param query The search query to save.
*/
private void saveSearchQuery(String query) {
if (!showSettingsSearchHistory) {
return;
}
searchHistory.remove(query); // Remove if already exists to update position.
searchHistory.addFirst(query); // Add to the most recent.
// Remove extra old entries.
while (searchHistory.size() > MAX_HISTORY_SIZE) {
String last = searchHistory.removeLast();
Logger.printDebug(() -> "Removing search history query: " + last);
}
saveSearchHistory();
updateSearchHistoryAdapter();
}
/**
* Removes a search query from the search history.
* @param query The search query to remove.
*/
private void removeSearchQuery(String query) {
searchHistory.remove(query);
saveSearchHistory();
updateSearchHistoryAdapter();
}
/**
* Save the search history to the shared preferences.
*/
private void saveSearchHistory() {
Logger.printDebug(() -> "Saving search history: " + searchHistory);
Settings.SETTINGS_SEARCH_ENTRIES.save(
String.join("\n", searchHistory)
);
}
/**
* Updates the search history adapter with the latest history.
*/
private void updateSearchHistoryAdapter() {
if (autoCompleteTextView == null) {
return;
}
SearchHistoryAdapter adapter = (SearchHistoryAdapter) autoCompleteTextView.getAdapter();
if (adapter != null) {
adapter.clear();
adapter.addAll(searchHistory);
adapter.notifyDataSetChanged();
}
}
/**
* Opens the search view and shows the keyboard.
*/
private void openSearch() {
isSearchActive = true;
toolbar.getMenu().findItem(getResourceIdentifier(
"action_search", "id")).setVisible(false);
toolbar.setTitle("");
searchContainer.setVisibility(View.VISIBLE);
searchView.requestFocus();
// Show keyboard.
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT);
// Show suggestions with a slight delay.
if (showSettingsSearchHistory && autoCompleteTextView != null && autoCompleteTextView.getText().length() == 0) {
searchView.postDelayed(() -> {
if (isSearchActive && autoCompleteTextView.getText().length() == 0) {
autoCompleteTextView.showDropDown();
}
}, 100); // 100ms delay to ensure focus is stable.
}
}
/**
* Closes the search view and hides the keyboard.
*/
private void closeSearch() {
isSearchActive = false;
toolbar.getMenu().findItem(getResourceIdentifier(
"action_search", "id")).setVisible(true);
toolbar.setTitle(originalTitle);
searchContainer.setVisibility(View.GONE);
searchView.setQuery("", false);
// Hide keyboard.
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(searchView.getWindowToken(), 0);
}
/**
* Custom ArrayAdapter for search history.
*/
private class SearchHistoryAdapter extends ArrayAdapter<String> {
public SearchHistoryAdapter(Context context, List<String> history) {
super(context, 0, history);
}
@NonNull
@Override
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
if (convertView == null) {
convertView = LinearLayout.inflate(getContext(), getResourceIdentifier(
"revanced_search_suggestion_item", "layout"), null);
}
// Apply rounded corners programmatically.
convertView.setBackground(createSuggestionBackgroundDrawable(getContext()));
String query = getItem(position);
// Set query text.
TextView textView = convertView.findViewById(getResourceIdentifier(
"suggestion_text", "id"));
if (textView != null) {
textView.setText(query);
}
// Set click listener for inserting query into SearchView.
convertView.setOnClickListener(v -> {
searchView.setQuery(query, true); // Insert selected query and submit.
});
// Set long click listener for deletion confirmation.
convertView.setOnLongClickListener(v -> {
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;
});
return convertView;
}
}
}

View File

@@ -21,11 +21,11 @@ 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.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
import android.graphics.Color;
@@ -38,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);
@@ -58,15 +62,16 @@ 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());
// Ads
public static final BooleanSetting HIDE_BUTTONED_ADS = new BooleanSetting("revanced_hide_buttoned_ads", TRUE);
public static final BooleanSetting HIDE_END_SCREEN_STORE_BANNER = new BooleanSetting("revanced_hide_end_screen_store_banner", TRUE, true);
public static final BooleanSetting HIDE_FULLSCREEN_ADS = new BooleanSetting("revanced_hide_fullscreen_ads", TRUE);
public static final BooleanSetting HIDE_GENERAL_ADS = new BooleanSetting("revanced_hide_general_ads", TRUE);
@@ -103,8 +108,9 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_MOVIES_SECTION = new BooleanSetting("revanced_hide_movies_section", TRUE);
public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", TRUE);
public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE);
public static final BooleanSetting HIDE_SEARCH_RESULT_RECOMMENDATIONS = new BooleanSetting("revanced_hide_search_result_recommendations", TRUE);
public static final BooleanSetting HIDE_SEARCH_RESULT_RECOMMENDATION_LABELS = new BooleanSetting("revanced_hide_search_result_recommendation_labels", TRUE);
public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true);
public static final BooleanSetting HIDE_TICKET_SHELF = new BooleanSetting("revanced_hide_ticket_shelf", FALSE);
// Alternative thumbnails
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscription", ThumbnailOption.ORIGINAL);
@@ -133,12 +139,14 @@ 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);
public static final BooleanSetting HIDE_EMERGENCY_BOX = new BooleanSetting("revanced_hide_emergency_box", TRUE);
public static final BooleanSetting HIDE_ENDSCREEN_CARDS = new BooleanSetting("revanced_hide_endscreen_cards", FALSE);
public static final BooleanSetting HIDE_END_SCREEN_SUGGESTED_VIDEO = new BooleanSetting("revanced_end_screen_suggested_video", FALSE, true);
public static final BooleanSetting HIDE_RELATED_VIDEO_OVERLAY = new BooleanSetting("revanced_hide_related_video_overlay", FALSE, true);
public static final BooleanSetting HIDE_HIDE_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_channel_guidelines", TRUE);
public static final BooleanSetting HIDE_INFO_PANELS = new BooleanSetting("revanced_hide_info_panels", TRUE);
public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE);
@@ -176,12 +184,13 @@ 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);
// Description
public static final BooleanSetting HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION = new BooleanSetting("revanced_hide_ai_generated_video_summary_section", FALSE);
public static final BooleanSetting HIDE_ASK_SECTION = new BooleanSetting("revanced_hide_ask_section", FALSE);
public static final BooleanSetting HIDE_ATTRIBUTES_SECTION = new BooleanSetting("revanced_hide_attributes_section", FALSE);
public static final BooleanSetting HIDE_CHAPTERS_SECTION = new BooleanSetting("revanced_hide_chapters_section", TRUE);
public static final BooleanSetting HIDE_HOW_THIS_WAS_MADE_SECTION = new BooleanSetting("revanced_hide_how_this_was_made_section", FALSE);
@@ -217,9 +226,13 @@ public class Settings extends BaseSettings {
// General layout
public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("revanced_restore_old_settings_menus", FALSE, true);
public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "", true);
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");
@@ -254,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);
@@ -269,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);
@@ -303,16 +318,16 @@ public class Settings extends BaseSettings {
public static final BooleanSetting AUTO_REPEAT = new BooleanSetting("revanced_auto_repeat", FALSE);
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
public static final BooleanSetting DISABLE_ZOOM_HAPTICS = new BooleanSetting("revanced_disable_zoom_haptics", TRUE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING = new BooleanSetting("revanced_disable_haptic_feedback_precise_seeking", FALSE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO = new BooleanSetting("revanced_disable_haptic_feedback_seek_undo", FALSE);
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_ZOOM = new BooleanSetting("revanced_disable_haptic_feedback_zoom", FALSE);
public static final BooleanSetting EXTERNAL_BROWSER = new BooleanSetting("revanced_external_browser", TRUE, true);
public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE);
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true,
"revanced_spoof_device_dimensions_user_dialog_message");
/**
* When enabled, share the debug logs with care.
* The buffer contains select user data, including the client ip address and information that could identify the end user.
*/
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, parent(BaseSettings.DEBUG));
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
// Swipe controls
public static final BooleanSetting SWIPE_CHANGE_VIDEO = new BooleanSetting("revanced_swipe_change_video", FALSE, true);
@@ -331,28 +346,30 @@ public class Settings extends BaseSettings {
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
public static final IntegerSetting SWIPE_OVERLAY_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true,
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
public static final StringSetting SWIPE_OVERLAY_PROGRESS_COLOR = new StringSetting("revanced_swipe_overlay_progress_color", "#FFFFFF", true,
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
public static final StringSetting SWIPE_OVERLAY_BRIGHTNESS_COLOR = new StringSetting("revanced_swipe_overlay_progress_brightness_color", "#FFFFFF", true,
parent(SWIPE_BRIGHTNESS));
public static final StringSetting SWIPE_OVERLAY_VOLUME_COLOR = new StringSetting("revanced_swipe_overlay_progress_volume_color", "#FFFFFF", true,
parent(SWIPE_VOLUME));
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true,
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
public static final BooleanSetting SWIPE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_swipe_save_and_restore_brightness", TRUE, true, parent(SWIPE_BRIGHTNESS));
public static final BooleanSetting SWIPE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_swipe_save_and_restore_brightness", TRUE, true,
parent(SWIPE_BRIGHTNESS));
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1f);
public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true, parent(SWIPE_BRIGHTNESS));
public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true,
parent(SWIPE_BRIGHTNESS));
// ReturnYoutubeDislike
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE);
public static final StringSetting RYD_USER_ID = new StringSetting("ryd_user_id", "", false, false);
public static final BooleanSetting RYD_SHORTS = new BooleanSetting("ryd_shorts", TRUE, parent(RYD_ENABLED));
public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("ryd_dislike_percentage", FALSE, parent(RYD_ENABLED));
public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("ryd_compact_layout", FALSE, parent(RYD_ENABLED));
public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("ryd_estimated_like", TRUE, parent(RYD_ENABLED));
public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("ryd_toast_on_connection_error", TRUE, parent(RYD_ENABLED));
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("revanced_ryd_enabled", TRUE);
public static final StringSetting RYD_USER_ID = new StringSetting("revanced_ryd_user_id", "", false, false);
public static final BooleanSetting RYD_SHORTS = new BooleanSetting("revanced_ryd_shorts", TRUE, parent(RYD_ENABLED));
public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("revanced_ryd_dislike_percentage", FALSE, true, parent(RYD_ENABLED));
public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("revanced_ryd_compact_layout", FALSE, true, parent(RYD_ENABLED));
public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("revanced_ryd_estimated_like", TRUE, true, parent(RYD_ENABLED));
public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("revanced_ryd_toast_on_connection_error", TRUE, parent(RYD_ENABLED));
// SponsorBlock
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
/**
* Do not use directly, instead use {@link SponsorBlockSettings}
*/
/** Do not use id setting directly. Instead use {@link SponsorBlockSettings}. */
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "");
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
@@ -416,12 +433,10 @@ public class Settings extends BaseSettings {
// region Migration
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_BUTTONS, HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS);
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER, HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER);
migrateOldSettingToNew(DEPRECATED_DISABLE_SUGGESTED_VIDEO_END_SCREEN, HIDE_END_SCREEN_SUGGESTED_VIDEO);
migrateOldSettingToNew(DEPRECATED_RESTORE_OLD_VIDEO_QUALITY_MENU, ADVANCED_VIDEO_QUALITY_MENU);
migrateOldSettingToNew(DEPRECATED_AUTO_CAPTIONS, DISABLE_AUTO_CAPTIONS);
// Migrate renamed enum.
//noinspection deprecation
@@ -464,10 +479,15 @@ public class Settings extends BaseSettings {
SPOOF_APP_VERSION_TARGET.resetToDefault();
}
if (!DEPRECATED_AUTO_CAPTIONS.isSetToDefault()) {
DISABLE_AUTO_CAPTIONS.save(true);
DEPRECATED_AUTO_CAPTIONS.resetToDefault();
}
// RYD requires manually migrating old settings since the lack of
// a "revanced_" on the old setting causes duplicate key exceptions during export.
SharedPrefCategory revancedPrefs = Setting.preferences;
Setting.migrateFromOldPreferences(revancedPrefs, RYD_USER_ID, "ryd_user_id");
Setting.migrateFromOldPreferences(revancedPrefs, RYD_ENABLED, "ryd_enabled");
Setting.migrateFromOldPreferences(revancedPrefs, RYD_DISLIKE_PERCENTAGE, "ryd_dislike_percentage");
Setting.migrateFromOldPreferences(revancedPrefs, RYD_COMPACT_LAYOUT, "ryd_compact_layout");
Setting.migrateFromOldPreferences(revancedPrefs, RYD_ESTIMATED_LIKE, "ryd_estimated_like");
Setting.migrateFromOldPreferences(revancedPrefs, RYD_TOAST_ON_CONNECTION_ERROR, "ryd_toast_on_connection_error");
// endregion
@@ -478,4 +498,3 @@ public class Settings extends BaseSettings {
// endregion
}
}

View File

@@ -1,23 +1,15 @@
package app.revanced.extension.youtube.settings.preference;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.preference.Preference;
import android.util.AttributeSet;
/**
* Allows tapping the DeArrow about preference to open the DeArrow website.
*/
@SuppressWarnings({"unused", "deprecation"})
public class AlternativeThumbnailsAboutDeArrowPreference extends Preference {
@SuppressWarnings("unused")
public class AlternativeThumbnailsAboutDeArrowPreference extends UrlLinkPreference {
{
setOnPreferenceClickListener(pref -> {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse("https://dearrow.ajay.app"));
pref.getContext().startActivity(i);
return false;
});
externalUrl = "https://dearrow.ajay.app";
}
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {

View File

@@ -0,0 +1,34 @@
package app.revanced.extension.youtube.settings.preference;
import android.content.Context;
import android.util.AttributeSet;
import android.preference.Preference;
import app.revanced.extension.shared.settings.preference.LogBufferManager;
/**
* A custom preference that clears the ReVanced debug log buffer when clicked.
* Invokes the {@link LogBufferManager#clearLogBuffer} method.
*/
@SuppressWarnings("unused")
public class ClearLogBufferPreference extends Preference {
{
setOnPreferenceClickListener(pref -> {
LogBufferManager.clearLogBuffer();
return true;
});
}
public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ClearLogBufferPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ClearLogBufferPreference(Context context) {
super(context);
}
}

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