Compare commits

...

40 Commits

Author SHA1 Message Date
semantic-release-bot
182829d51c chore: Release v5.34.0-dev.4 [skip ci]
# [5.34.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.3...v5.34.0-dev.4) (2025-08-10)

### Bug Fixes

* **YouTube - Hide layout components:** Do not hide community posts on channel profiles ([#5634](https://github.com/ReVanced/revanced-patches/issues/5634)) ([61824ad](61824ade23))
2025-08-10 13:11:44 +00:00
LisoUseInAIKyrios
61824ade23 fix(YouTube - Hide layout components): Do not hide community posts on channel profiles (#5634) 2025-08-10 09:09:08 -04:00
LisoUseInAIKyrios
ff4308e961 refactor(YouTube Music - Hide category bar): Fix possible crash when patching certain app targets 2025-08-09 11:13:35 -04:00
semantic-release-bot
b5eb13c0a8 chore: Release v5.34.0-dev.3 [skip ci]
# [5.34.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.2...v5.34.0-dev.3) (2025-08-09)

### Bug Fixes

* **pixiv - Hide ads:** Constrain patch to last working app target ([b702dce](b702dceda0))
2025-08-09 15:06:42 +00:00
LisoUseInAIKyrios
b702dceda0 fix(pixiv - Hide ads): Constrain patch to last working app target 2025-08-09 11:04:07 -04:00
semantic-release-bot
d616652058 chore: Release v5.34.0-dev.2 [skip ci]
# [5.34.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.1...v5.34.0-dev.2) (2025-08-09)

### Bug Fixes

* **Backdrops:** Remove broken patch that is no longer supported ([#5627](https://github.com/ReVanced/revanced-patches/issues/5627)) ([c3e571e](c3e571e765))

### Features

* **YouTube - Playback speed:** Show current playback speed on player speed dialog button ([#5607](https://github.com/ReVanced/revanced-patches/issues/5607)) ([30176a3](30176a3318))
2025-08-09 01:31:11 +00:00
LisoUseInAIKyrios
c3e571e765 fix(Backdrops): Remove broken patch that is no longer supported (#5627) 2025-08-08 21:28:07 -04:00
MarcaD
30176a3318 feat(YouTube - Playback speed): Show current playback speed on player speed dialog button (#5607)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-08-08 21:27:52 -04:00
semantic-release-bot
9c0638d128 chore: Release v5.34.0-dev.1 [skip ci]
# [5.34.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.33.0...v5.34.0-dev.1) (2025-08-08)

### Bug Fixes

* **Twitch:** Constrain patches to last working app targets ([#5373](https://github.com/ReVanced/revanced-patches/issues/5373)) ([d7eb6e8](d7eb6e87a5))

### Features

* **Instagram:** Support latest app version ([#5611](https://github.com/ReVanced/revanced-patches/issues/5611)) ([562e005](562e005772))
2025-08-08 01:55:22 +00:00
LisoUseInAIKyrios
d7eb6e87a5 fix(Twitch): Constrain patches to last working app targets (#5373) 2025-08-07 21:51:52 -04:00
LisoUseInAIKyrios
562e005772 feat(Instagram): Support latest app version (#5611) 2025-08-07 21:51:01 -04:00
github-actions[bot]
f61218de52 chore: Sync translations (#5616) 2025-08-07 21:50:45 -04:00
semantic-release-bot
a19b670e19 chore: Release v5.33.0 [skip ci]
# [5.33.0](https://github.com/ReVanced/revanced-patches/compare/v5.32.0...v5.33.0) (2025-08-05)

### Bug Fixes

* **Messenger - Hide Facebook button:** Support the latest app version ([#5590](https://github.com/ReVanced/revanced-patches/issues/5590)) ([a28891e](a28891e5f3))
* **NFC Tools:** Remove broken patch that is no longer supported ([#5584](https://github.com/ReVanced/revanced-patches/issues/5584)) ([2e177a8](2e177a8839))
* **YouTube - Force original audio:** Disable a/b feature flag that forces localized audio ([#5582](https://github.com/ReVanced/revanced-patches/issues/5582)) ([1dd01cf](1dd01cf54a))
* **YouTube - Litho filter:** Correctly filter identifier of older YouTube targets ([b1d164b](b1d164b446))
* **YouTube - Playback speed:** Use old speed menu for player button if enabled ([a4817df](a4817dfdd0))
* **YouTube - Video quality:** Fix 144p default not always used ([9afa7d2](9afa7d2ac6))
* **YouTube - Video quality:** Fix dialog quality list check mark not always shown ([1bc63e5](1bc63e50a7))
* **YouTube - Video quality:** Fix wrong qualities sometimes shown in player button dialog ([178eed7](178eed7fcd))
* **YouTube - Video quality:** Use 1080p enhanced bitrate for Premium users ([#5565](https://github.com/ReVanced/revanced-patches/issues/5565)) ([1adbd56](1adbd563b2))

### Features

* **ORF ON:** Add `Remove root detection` patch ([#5551](https://github.com/ReVanced/revanced-patches/issues/5551)) ([d92362b](d92362b0d9))
* **YouTube - Playback speed:** Add "Restore old playback speed menu" option ([#5552](https://github.com/ReVanced/revanced-patches/issues/5552)) ([e9e4cf3](e9e4cf39b6))
* **YouTube:** Add player button to change video quality ([#5435](https://github.com/ReVanced/revanced-patches/issues/5435)) ([7bdc328](7bdc32867a))

### Performance Improvements

* **YouTube:** Filter identifier callback only on root component creation ([#5558](https://github.com/ReVanced/revanced-patches/issues/5558)) ([5d08fdd](5d08fdddb8))
2025-08-05 17:57:09 +00:00
LisoUseInAIKyrios
300d816350 chore: Merge branch dev to main (#5553) 2025-08-05 13:53:31 -04:00
github-actions[bot]
63d64a5c87 chore: Sync translations (#5595) 2025-08-05 13:52:06 -04:00
semantic-release-bot
0cfc31c8f7 chore: Release v5.33.0-dev.13 [skip ci]
# [5.33.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.12...v5.33.0-dev.13) (2025-08-05)

### Bug Fixes

* **Messenger - Hide Facebook button:** Support the latest app version ([#5590](https://github.com/ReVanced/revanced-patches/issues/5590)) ([a28891e](a28891e5f3))
2025-08-05 03:23:38 +00:00
Dawid Krajcarz
a28891e5f3 fix(Messenger - Hide Facebook button): Support the latest app version (#5590) 2025-08-04 23:21:10 -04:00
semantic-release-bot
36036b082d chore: Release v5.33.0-dev.12 [skip ci]
# [5.33.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.11...v5.33.0-dev.12) (2025-08-04)

### Bug Fixes

* **YouTube - Video quality:** Fix dialog quality list check mark not always shown ([1bc63e5](1bc63e50a7))
2025-08-04 19:19:31 +00:00
LisoUseInAIKyrios
1bc63e50a7 fix(YouTube - Video quality): Fix dialog quality list check mark not always shown 2025-08-04 15:17:00 -04:00
semantic-release-bot
4b2b5e3029 chore: Release v5.33.0-dev.11 [skip ci]
# [5.33.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.10...v5.33.0-dev.11) (2025-08-04)

### Bug Fixes

* **YouTube - Video quality:** Fix 144p default not always used ([9afa7d2](9afa7d2ac6))
2025-08-04 19:03:03 +00:00
LisoUseInAIKyrios
9afa7d2ac6 fix(YouTube - Video quality): Fix 144p default not always used 2025-08-04 15:00:14 -04:00
semantic-release-bot
1a8146dbc8 chore: Release v5.33.0-dev.10 [skip ci]
# [5.33.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.9...v5.33.0-dev.10) (2025-08-04)

### Bug Fixes

* **YouTube - Video quality:** Fix wrong qualities sometimes shown in player button dialog ([178eed7](178eed7fcd))
2025-08-04 17:23:50 +00:00
LisoUseInAIKyrios
178eed7fcd fix(YouTube - Video quality): Fix wrong qualities sometimes shown in player button dialog 2025-08-04 13:21:02 -04:00
semantic-release-bot
621292644c chore: Release v5.33.0-dev.9 [skip ci]
# [5.33.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.8...v5.33.0-dev.9) (2025-08-04)

### Bug Fixes

* **YouTube - Force original audio:** Disable a/b feature flag that forces localized audio ([#5582](https://github.com/ReVanced/revanced-patches/issues/5582)) ([1dd01cf](1dd01cf54a))
2025-08-04 01:27:12 +00:00
LisoUseInAIKyrios
1dd01cf54a fix(YouTube - Force original audio): Disable a/b feature flag that forces localized audio (#5582) 2025-08-03 21:23:27 -04:00
semantic-release-bot
8c31374c53 chore: Release v5.33.0-dev.8 [skip ci]
# [5.33.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.7...v5.33.0-dev.8) (2025-08-03)

### Bug Fixes

* **NFC Tools:** Remove broken patch that is no longer supported ([#5584](https://github.com/ReVanced/revanced-patches/issues/5584)) ([2e177a8](2e177a8839))
2025-08-03 23:44:42 +00:00
LisoUseInAIKyrios
2e177a8839 fix(NFC Tools): Remove broken patch that is no longer supported (#5584) 2025-08-03 19:41:54 -04:00
github-actions[bot]
cfffd422f8 chore: Sync translations (#5586) 2025-08-03 19:41:07 -04:00
github-actions[bot]
37aab8382e chore: Sync translations (#5585) 2025-08-03 19:38:17 -04:00
semantic-release-bot
f4950ec2ea chore: Release v5.33.0-dev.7 [skip ci]
# [5.33.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.6...v5.33.0-dev.7) (2025-08-03)

### Features

* **YouTube:** Add player button to change video quality ([#5435](https://github.com/ReVanced/revanced-patches/issues/5435)) ([7bdc328](7bdc32867a))
2025-08-03 15:26:39 +00:00
MarcaD
7bdc32867a feat(YouTube): Add player button to change video quality (#5435)
Co-authored-by: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com>
2025-08-03 11:23:46 -04:00
semantic-release-bot
6e60ac6963 chore: Release v5.33.0-dev.6 [skip ci]
# [5.33.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.5...v5.33.0-dev.6) (2025-07-31)

### Bug Fixes

* **YouTube - Video quality:** Use 1080p enhanced bitrate for Premium users ([#5565](https://github.com/ReVanced/revanced-patches/issues/5565)) ([1adbd56](1adbd563b2))
2025-07-31 18:30:03 +00:00
LisoUseInAIKyrios
1adbd563b2 fix(YouTube - Video quality): Use 1080p enhanced bitrate for Premium users (#5565) 2025-07-31 14:27:17 -04:00
github-actions[bot]
9ccf13b680 chore: Sync translations (#5567) 2025-07-31 14:27:05 -04:00
semantic-release-bot
7b8ca9c018 chore: Release v5.33.0-dev.5 [skip ci]
# [5.33.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.4...v5.33.0-dev.5) (2025-07-31)

### Bug Fixes

* **YouTube - Litho filter:** Correctly filter identifier of older YouTube targets ([b1d164b](b1d164b446))
2025-07-31 10:35:51 +00:00
github-actions[bot]
ae6dd23d08 chore: Sync translations (#5564) 2025-07-31 06:33:31 -04:00
LisoUseInAIKyrios
b1d164b446 fix(YouTube - Litho filter): Correctly filter identifier of older YouTube targets 2025-07-31 06:33:12 -04:00
dependabot[bot]
87c39dd485 chore(deps-dev): Bump semantic-release from 24.2.6 to 24.2.7 (#5545) 2025-07-30 10:31:08 -04:00
semantic-release-bot
1549ac12aa chore: Release v5.33.0-dev.4 [skip ci]
# [5.33.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.3...v5.33.0-dev.4) (2025-07-30)

### Performance Improvements

* **YouTube:** Filter identifier callback only on root component creation ([#5558](https://github.com/ReVanced/revanced-patches/issues/5558)) ([5d08fdd](5d08fdddb8))
2025-07-30 13:21:43 +00:00
LisoUseInAIKyrios
5d08fdddb8 perf(YouTube): Filter identifier callback only on root component creation (#5558) 2025-07-30 09:18:20 -04:00
151 changed files with 2193 additions and 791 deletions

View File

@@ -1,3 +1,138 @@
# [5.34.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.3...v5.34.0-dev.4) (2025-08-10)
### Bug Fixes
* **YouTube - Hide layout components:** Do not hide community posts on channel profiles ([#5634](https://github.com/ReVanced/revanced-patches/issues/5634)) ([9e3d5a2](https://github.com/ReVanced/revanced-patches/commit/9e3d5a2b36106479470f3f69920518b57e8c4dca))
# [5.34.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.2...v5.34.0-dev.3) (2025-08-09)
### Bug Fixes
* **pixiv - Hide ads:** Constrain patch to last working app target ([d8ea56c](https://github.com/ReVanced/revanced-patches/commit/d8ea56ca4be47df1c43f96ec41b91c800f1d9daf))
# [5.34.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.34.0-dev.1...v5.34.0-dev.2) (2025-08-09)
### Bug Fixes
* **Backdrops:** Remove broken patch that is no longer supported ([#5627](https://github.com/ReVanced/revanced-patches/issues/5627)) ([ebb8332](https://github.com/ReVanced/revanced-patches/commit/ebb83320838aa99dd4417d45a50333dd42c1218a))
### Features
* **YouTube - Playback speed:** Show current playback speed on player speed dialog button ([#5607](https://github.com/ReVanced/revanced-patches/issues/5607)) ([279436a](https://github.com/ReVanced/revanced-patches/commit/279436a3657b50f98bb4cc64dc88dc14e422f204))
# [5.34.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.33.0...v5.34.0-dev.1) (2025-08-08)
### Bug Fixes
* **Twitch:** Constrain patches to last working app targets ([#5373](https://github.com/ReVanced/revanced-patches/issues/5373)) ([29a4748](https://github.com/ReVanced/revanced-patches/commit/29a47481c4efa209a3a53df60613b59a73adbe07))
### Features
* **Instagram:** Support latest app version ([#5611](https://github.com/ReVanced/revanced-patches/issues/5611)) ([26fe690](https://github.com/ReVanced/revanced-patches/commit/26fe690dfbefe6c412c5f81f208a3b1d2fbd7a0a))
# [5.33.0](https://github.com/ReVanced/revanced-patches/compare/v5.32.0...v5.33.0) (2025-08-05)
### Bug Fixes
* **Messenger - Hide Facebook button:** Support the latest app version ([#5590](https://github.com/ReVanced/revanced-patches/issues/5590)) ([0cab98d](https://github.com/ReVanced/revanced-patches/commit/0cab98df1689dbf7a042f18f4a961d47da1430ad))
* **NFC Tools:** Remove broken patch that is no longer supported ([#5584](https://github.com/ReVanced/revanced-patches/issues/5584)) ([cd3a6be](https://github.com/ReVanced/revanced-patches/commit/cd3a6be75c6bd3cc33c0b17a044bd6147f27b5ce))
* **YouTube - Force original audio:** Disable a/b feature flag that forces localized audio ([#5582](https://github.com/ReVanced/revanced-patches/issues/5582)) ([9fe13ee](https://github.com/ReVanced/revanced-patches/commit/9fe13ee1af104c009efd19b826adef375e48e191))
* **YouTube - Litho filter:** Correctly filter identifier of older YouTube targets ([bf29d69](https://github.com/ReVanced/revanced-patches/commit/bf29d6909e389819bad878ad3b94bbc90d823cc9))
* **YouTube - Playback speed:** Use old speed menu for player button if enabled ([1e8f436](https://github.com/ReVanced/revanced-patches/commit/1e8f4368e117f4b278c24709231cb32546e46dc0))
* **YouTube - Video quality:** Fix 144p default not always used ([2f7483a](https://github.com/ReVanced/revanced-patches/commit/2f7483a2d789c28a243b58bb7a252c0d590858ee))
* **YouTube - Video quality:** Fix dialog quality list check mark not always shown ([295f0f2](https://github.com/ReVanced/revanced-patches/commit/295f0f216b5e8aa9d68457862e73e312b7342703))
* **YouTube - Video quality:** Fix wrong qualities sometimes shown in player button dialog ([7378ae3](https://github.com/ReVanced/revanced-patches/commit/7378ae3c5fc88f91bf5cd6db47c6cd170a8c5a4f))
* **YouTube - Video quality:** Use 1080p enhanced bitrate for Premium users ([#5565](https://github.com/ReVanced/revanced-patches/issues/5565)) ([bd3ace0](https://github.com/ReVanced/revanced-patches/commit/bd3ace0bd04ccd0369adb49d63aa0cf986402346))
### Features
* **ORF ON:** Add `Remove root detection` patch ([#5551](https://github.com/ReVanced/revanced-patches/issues/5551)) ([6c6aa35](https://github.com/ReVanced/revanced-patches/commit/6c6aa35411a139dddc3a15dd757fbeded5d1a0a3))
* **YouTube - Playback speed:** Add "Restore old playback speed menu" option ([#5552](https://github.com/ReVanced/revanced-patches/issues/5552)) ([b01f15b](https://github.com/ReVanced/revanced-patches/commit/b01f15b9acb0427aed99b0141ae271831b7936bf))
* **YouTube:** Add player button to change video quality ([#5435](https://github.com/ReVanced/revanced-patches/issues/5435)) ([d5f51bf](https://github.com/ReVanced/revanced-patches/commit/d5f51bf400dd22626ff65d7563b6fde70d53fb25))
### Performance Improvements
* **YouTube:** Filter identifier callback only on root component creation ([#5558](https://github.com/ReVanced/revanced-patches/issues/5558)) ([ccac46e](https://github.com/ReVanced/revanced-patches/commit/ccac46eebc2e14b094454e37ef4461d48a62c53f))
# [5.33.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.12...v5.33.0-dev.13) (2025-08-05)
### Bug Fixes
* **Messenger - Hide Facebook button:** Support the latest app version ([#5590](https://github.com/ReVanced/revanced-patches/issues/5590)) ([0cab98d](https://github.com/ReVanced/revanced-patches/commit/0cab98df1689dbf7a042f18f4a961d47da1430ad))
# [5.33.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.11...v5.33.0-dev.12) (2025-08-04)
### Bug Fixes
* **YouTube - Video quality:** Fix dialog quality list check mark not always shown ([295f0f2](https://github.com/ReVanced/revanced-patches/commit/295f0f216b5e8aa9d68457862e73e312b7342703))
# [5.33.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.10...v5.33.0-dev.11) (2025-08-04)
### Bug Fixes
* **YouTube - Video quality:** Fix 144p default not always used ([2f7483a](https://github.com/ReVanced/revanced-patches/commit/2f7483a2d789c28a243b58bb7a252c0d590858ee))
# [5.33.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.9...v5.33.0-dev.10) (2025-08-04)
### Bug Fixes
* **YouTube - Video quality:** Fix wrong qualities sometimes shown in player button dialog ([7378ae3](https://github.com/ReVanced/revanced-patches/commit/7378ae3c5fc88f91bf5cd6db47c6cd170a8c5a4f))
# [5.33.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.8...v5.33.0-dev.9) (2025-08-04)
### Bug Fixes
* **YouTube - Force original audio:** Disable a/b feature flag that forces localized audio ([#5582](https://github.com/ReVanced/revanced-patches/issues/5582)) ([9fe13ee](https://github.com/ReVanced/revanced-patches/commit/9fe13ee1af104c009efd19b826adef375e48e191))
# [5.33.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.7...v5.33.0-dev.8) (2025-08-03)
### Bug Fixes
* **NFC Tools:** Remove broken patch that is no longer supported ([#5584](https://github.com/ReVanced/revanced-patches/issues/5584)) ([cd3a6be](https://github.com/ReVanced/revanced-patches/commit/cd3a6be75c6bd3cc33c0b17a044bd6147f27b5ce))
# [5.33.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.6...v5.33.0-dev.7) (2025-08-03)
### Features
* **YouTube:** Add player button to change video quality ([#5435](https://github.com/ReVanced/revanced-patches/issues/5435)) ([d5f51bf](https://github.com/ReVanced/revanced-patches/commit/d5f51bf400dd22626ff65d7563b6fde70d53fb25))
# [5.33.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.5...v5.33.0-dev.6) (2025-07-31)
### Bug Fixes
* **YouTube - Video quality:** Use 1080p enhanced bitrate for Premium users ([#5565](https://github.com/ReVanced/revanced-patches/issues/5565)) ([bd3ace0](https://github.com/ReVanced/revanced-patches/commit/bd3ace0bd04ccd0369adb49d63aa0cf986402346))
# [5.33.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.4...v5.33.0-dev.5) (2025-07-31)
### Bug Fixes
* **YouTube - Litho filter:** Correctly filter identifier of older YouTube targets ([bf29d69](https://github.com/ReVanced/revanced-patches/commit/bf29d6909e389819bad878ad3b94bbc90d823cc9))
# [5.33.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.3...v5.33.0-dev.4) (2025-07-30)
### Performance Improvements
* **YouTube:** Filter identifier callback only on root component creation ([#5558](https://github.com/ReVanced/revanced-patches/issues/5558)) ([ccac46e](https://github.com/ReVanced/revanced-patches/commit/ccac46eebc2e14b094454e37ef4461d48a62c53f))
# [5.33.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.33.0-dev.2...v5.33.0-dev.3) (2025-07-30)

View File

@@ -329,7 +329,7 @@ public class Utils {
return (R) child;
}
throw new IllegalArgumentException("View with resource name '" + str + "' not found");
throw new IllegalArgumentException("View with resource name not found: " + str);
}
/**
@@ -1460,6 +1460,16 @@ public class Utils {
return (int) (metrics.widthPixels * (percentage / 100.0f));
}
/**
* Uses {@link #adjustColorBrightness(int, float)} depending if light or dark mode is active.
*/
@ColorInt
public static int adjustColorBrightness(@ColorInt int baseColor, float lightThemeFactor, float darkThemeFactor) {
return isDarkModeEnabled()
? adjustColorBrightness(baseColor, darkThemeFactor)
: adjustColorBrightness(baseColor, lightThemeFactor);
}
/**
* Adjusts the brightness of a color by lightening or darkening it based on the given factor.
* <p>

View File

@@ -1,16 +1,20 @@
package app.revanced.extension.youtube
import app.revanced.extension.shared.Logger
import java.util.Collections
/**
* generic event provider class
*/
class Event<T> {
private val eventListeners = mutableSetOf<(T) -> Unit>()
private val eventListeners = Collections.synchronizedSet(mutableSetOf<(T) -> Unit>())
operator fun plusAssign(observer: (T) -> Unit) {
addObserver(observer)
}
fun addObserver(observer: (T) -> Unit) {
Logger.printDebug { "Adding observer: $observer" }
eventListeners.add(observer)
}
@@ -23,7 +27,8 @@ class Event<T> {
}
operator fun invoke(value: T) {
for (observer in eventListeners)
for (observer in eventListeners) {
observer.invoke(value)
}
}
}

View File

@@ -24,6 +24,16 @@ public class ForceOriginalAudioPatch {
}
}
/**
* Injection point.
*/
public static boolean ignoreDefaultAudioStream(boolean original) {
if (Settings.FORCE_ORIGINAL_AUDIO.get()) {
return false;
}
return original;
}
/**
* Injection point.
*/
@@ -50,7 +60,6 @@ public class ForceOriginalAudioPatch {
return isOriginal;
} catch (Exception ex) {
Logger.printException(() -> "isDefaultAudioStream failure", ex);
return isDefault;
}
}

View File

@@ -1,12 +1,18 @@
package app.revanced.extension.youtube.patches;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Objects;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.Event;
import app.revanced.extension.youtube.shared.ShortsPlayerState;
import app.revanced.extension.youtube.shared.VideoState;
/**
@@ -16,11 +22,31 @@ import app.revanced.extension.youtube.shared.VideoState;
public final class VideoInformation {
public interface PlaybackController {
// Methods are added to YT classes during patching.
boolean seekTo(long videoTime);
void seekToRelative(long videoTimeOffset);
// Methods are added during patching.
boolean patch_seekTo(long videoTime);
void patch_seekToRelative(long videoTimeOffset);
}
/**
* Interface to use obfuscated methods.
*/
public interface VideoQualityMenuInterface {
// Method is added during patching.
void patch_setQuality(VideoQuality quality);
}
/**
* Video resolution of the automatic quality option..
*/
public static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
/**
* All quality names are the same for all languages.
* VideoQuality also has a resolution enum that can be used if needed.
*/
public static final String VIDEO_QUALITY_1080P_PREMIUM_NAME = "1080p Premium";
private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f;
/**
* Prefix present in all Short player parameters signature.
@@ -30,12 +56,10 @@ public final class VideoInformation {
private static WeakReference<PlaybackController> playerControllerRef = new WeakReference<>(null);
private static WeakReference<PlaybackController> mdxPlayerDirectorRef = new WeakReference<>(null);
@NonNull
private static String videoId = "";
private static long videoLength = 0;
private static long videoTime = -1;
@NonNull
private static volatile String playerResponseVideoId = "";
private static volatile boolean playerResponseVideoIdIsShort;
private static volatile boolean videoIdIsShort;
@@ -45,6 +69,44 @@ public final class VideoInformation {
*/
private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
private static int desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE;
private static boolean qualityNeedsUpdating;
/**
* The available qualities of the current video.
*/
@Nullable
private static VideoQuality[] currentQualities;
/**
* The current quality of the video playing.
* This is always the actual quality even if Automatic quality is active.
*/
@Nullable
private static VideoQuality currentQuality;
/**
* The current VideoQualityMenuInterface, set during setVideoQuality.
*/
@Nullable
private static VideoQualityMenuInterface currentMenuInterface;
/**
* Callback for when the current quality changes.
*/
public static final Event<VideoQuality> onQualityChange = new Event<>();
@Nullable
public static VideoQuality[] getCurrentQualities() {
return currentQualities;
}
@Nullable
public static VideoQuality getCurrentQuality() {
return currentQuality;
}
/**
* Injection point.
*
@@ -52,12 +114,18 @@ public final class VideoInformation {
*/
public static void initialize(@NonNull PlaybackController playerController) {
try {
Logger.printDebug(() -> "newVideoStarted");
playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController));
videoTime = -1;
videoLength = 0;
playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED;
desiredVideoResolution = AUTOMATIC_VIDEO_QUALITY_VALUE;
currentQualities = null;
currentMenuInterface = null;
setCurrentQuality(null);
} catch (Exception ex) {
Logger.printException(() -> "Failed to initialize", ex);
Logger.printException(() -> "initialize failure", ex);
}
}
@@ -197,14 +265,14 @@ public final class VideoInformation {
if (controller == null) {
Logger.printDebug(() -> "Cannot seekTo because player controller is null");
} else {
if (controller.seekTo(adjustedSeekTime)) return true;
if (controller.patch_seekTo(adjustedSeekTime)) return true;
Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD.");
// Else the video is loading or changing videos, or video is casting to a different device.
}
// Try calling the seekTo method of the MDX player director (called when casting).
// The difference has to be a different second mark in order to avoid infinite skip loops
// as the Lounge API only supports seconds.
// as the Lounge API only supports whole seconds.
if (adjustedSeekTime / 1000 == videoTime / 1000) {
Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small "
+ "(" + (adjustedSeekTime - videoTime) + "ms)");
@@ -217,9 +285,9 @@ public final class VideoInformation {
return false;
}
return controller.seekTo(adjustedSeekTime);
return controller.patch_seekTo(adjustedSeekTime);
} catch (Exception ex) {
Logger.printException(() -> "Failed to seek", ex);
Logger.printException(() -> "seekTo failure", ex);
return false;
}
}
@@ -239,7 +307,7 @@ public final class VideoInformation {
if (controller == null) {
Logger.printDebug(() -> "Cannot seek relative as player controller is null");
} else {
controller.seekToRelative(seekTime);
controller.patch_seekToRelative(seekTime);
}
// Adjust the fine adjustment function so it's at least 1 second before/after.
@@ -255,10 +323,10 @@ public final class VideoInformation {
if (controller == null) {
Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null");
} else {
controller.seekToRelative(adjustedSeekTime);
controller.patch_seekToRelative(adjustedSeekTime);
}
} catch (Exception ex) {
Logger.printException(() -> "Failed to seek relative", ex);
Logger.printException(() -> "seekToRelative failure", ex);
}
}
@@ -373,4 +441,123 @@ public final class VideoInformation {
playbackSpeed = newlyLoadedPlaybackSpeed;
}
}
/**
* @param resolution The desired video quality resolution to use.
*/
public static void setDesiredVideoResolution(int resolution) {
Utils.verifyOnMainThread();
Logger.printDebug(() -> "Setting desired video resolution: " + resolution);
desiredVideoResolution = resolution;
qualityNeedsUpdating = true;
}
private static void setCurrentQuality(@Nullable VideoQuality quality) {
Utils.verifyOnMainThread();
if (currentQuality != quality) {
Logger.printDebug(() -> "Current quality changed to: " + quality);
currentQuality = quality;
onQualityChange.invoke(quality);
}
}
/**
* Forcefully changes the video quality of the currently playing video.
*/
public static void changeQuality(VideoQuality quality) {
Utils.verifyOnMainThread();
if (currentMenuInterface == null) {
Logger.printException(() -> "Cannot change quality, menu interface is null");
return;
}
currentMenuInterface.patch_setQuality(quality);
}
/**
* Injection point. Fixes bad data used by YouTube.
*/
public static int fixVideoQualityResolution(String name, int quality) {
final int correctQuality = 480;
if (name.equals("480p") && quality != correctQuality) {
return correctQuality;
}
return quality;
}
/**
* Injection point.
*
* @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
* @param originalQualityIndex quality index to use, as chosen by YouTube
*/
public static int setVideoQuality(VideoQuality[] qualities, VideoQualityMenuInterface menu, int originalQualityIndex) {
try {
Utils.verifyOnMainThread();
currentMenuInterface = menu;
final boolean availableQualitiesChanged = (currentQualities == null)
|| !Arrays.equals(currentQualities, qualities);
if (availableQualitiesChanged) {
currentQualities = qualities;
Logger.printDebug(() -> "VideoQualities: " + Arrays.toString(currentQualities));
}
VideoQuality updatedCurrentQuality = qualities[originalQualityIndex];
if (updatedCurrentQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE
&& (currentQuality == null || currentQuality != updatedCurrentQuality)) {
setCurrentQuality(updatedCurrentQuality);
}
final int preferredQuality = desiredVideoResolution;
if (preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
return originalQualityIndex; // Nothing to do.
}
// After changing videos the qualities can initially be for the prior video.
// If the qualities have changed and the default is not auto then an update is needed.
if (qualityNeedsUpdating) {
qualityNeedsUpdating = false;
} else if (!availableQualitiesChanged) {
return originalQualityIndex;
}
// Find the highest quality that is equal to or less than the preferred.
int i = 0;
final int lastQualityIndex = qualities.length - 1;
for (VideoQuality quality : qualities) {
final int qualityResolution = quality.patch_getResolution();
if ((qualityResolution != AUTOMATIC_VIDEO_QUALITY_VALUE && qualityResolution <= preferredQuality)
// Use the lowest video quality if the default is lower than all available.
|| i == lastQualityIndex) {
final boolean qualityNeedsChange = (i != originalQualityIndex);
Logger.printDebug(() -> qualityNeedsChange
? "Changing video quality from: " + updatedCurrentQuality + " to: " + quality
: "Video is already the preferred quality: " + quality
);
// On first load of a new regular video, if the video is already the
// desired quality then the quality flyout will show 'Auto' (ie: Auto (720p)).
//
// To prevent user confusion, set the video index even if the
// quality is already correct so the UI picker will not display "Auto".
//
// Only change Shorts quality if the quality actually needs to change,
// because the "auto" option is not shown in the flyout
// and setting the same quality again can cause the Short to restart.
if (qualityNeedsChange || !ShortsPlayerState.isOpen()) {
changeQuality(quality);
return i;
}
return originalQualityIndex;
}
i++;
}
} catch (Exception ex) {
Logger.printException(() -> "setVideoQuality failure", ex);
}
return originalQualityIndex;
}
}

View File

@@ -6,8 +6,6 @@ import android.app.Instrumentation;
import android.view.KeyEvent;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.List;
import app.revanced.extension.shared.Logger;
@@ -155,10 +153,10 @@ public final class AdsFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == playerShoppingShelf) {
return contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered();
return contentIndex == 0 && playerShoppingShelfBuffer.check(buffer).isFiltered();
}
if (exceptions.matches(path)) {

View File

@@ -1,7 +1,5 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.youtube.patches.playback.quality.AdvancedVideoQualityMenuPatch;
import app.revanced.extension.youtube.settings.Settings;
@@ -21,7 +19,7 @@ public final class AdvancedVideoQualityMenuFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
isVideoQualityMenuVisible = true;

View File

@@ -1,7 +1,5 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
@@ -100,7 +98,7 @@ final class ButtonsFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == likeSubscribeGlow) {
return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
@@ -117,7 +115,7 @@ final class ButtonsFilter extends Filter {
// Make sure the current path is the right one
// to avoid false positives.
return path.startsWith(VIDEO_ACTION_BAR_PATH)
&& bufferButtonsGroupList.check(protobufBufferArray).isFiltered();
&& bufferButtonsGroupList.check(buffer).isFiltered();
}
return true;

View File

@@ -1,7 +1,5 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@@ -87,12 +85,12 @@ final class CommentsFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == chipBar) {
// Playlist sort button uses same components and must only filter if the player is opened.
return PlayerType.getCurrent().isMaximizedOrFullscreen()
&& aiCommentsSummary.check(protobufBufferArray).isFiltered();
&& aiCommentsSummary.check(buffer).isFiltered();
}
return true;

View File

@@ -3,7 +3,6 @@ package app.revanced.extension.youtube.patches.components;
import static app.revanced.extension.shared.StringRef.str;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.Collection;
@@ -146,7 +145,7 @@ final class CustomFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// All callbacks are custom filter groups.
CustomFilterGroup custom = (CustomFilterGroup) matchedGroup;
@@ -158,6 +157,6 @@ final class CustomFilter extends Filter {
return true; // No buffer filter, only path filtering.
}
return custom.bufferSearch.matches(protobufBufferArray);
return custom.bufferSearch.matches(buffer);
}
}

View File

@@ -1,7 +1,5 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.youtube.StringTrieSearch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@@ -105,7 +103,7 @@ final class DescriptionComponentsFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == aiGeneratedVideoSummarySection) {
@@ -116,11 +114,11 @@ final class DescriptionComponentsFilter extends Filter {
if (exceptions.matches(path)) return false;
if (matchedGroup == macroMarkersCarousel) {
return contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered();
return contentIndex == 0 && macroMarkersCarouselGroupList.check(buffer).isFiltered();
}
if (matchedGroup == horizontalShelf) {
return cellVideoAttribute.check(protobufBufferArray).isFiltered();
return cellVideoAttribute.check(buffer).isFiltered();
}
return true;

View File

@@ -1,7 +1,5 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -42,6 +40,7 @@ abstract class Filter {
/**
* Adds callbacks to {@link #isFiltered(String, String, byte[], StringFilterGroup, FilterContentType, int)}
* if any of the groups are found.
* <p>
*/
protected final void addIdentifierCallbacks(StringFilterGroup... groups) {
identifierCallbacks.addAll(Arrays.asList(groups));
@@ -68,7 +67,7 @@ abstract class Filter {
* @param contentIndex Matched index of the identifier or path.
* @return True if the litho component should be filtered out.
*/
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
return true;
}

View File

@@ -554,7 +554,7 @@ final class KeywordContentFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (contentIndex != 0 && matchedGroup == startsWithFilter) {
return false;
@@ -574,7 +574,7 @@ final class KeywordContentFilter extends Filter {
}
MutableReference<String> matchRef = new MutableReference<>();
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
if (bufferSearch.matches(buffer, matchRef)) {
updateStats(true, matchRef.value);
return true;
}

View File

@@ -32,6 +32,7 @@ public final class LayoutComponentsFilter extends Filter {
);
private final StringTrieSearch exceptions = new StringTrieSearch();
private final StringFilterGroup communityPosts;
private final StringFilterGroup surveys;
private final StringFilterGroup notifyMe;
private final StringFilterGroup singleItemInformationPanel;
@@ -68,7 +69,7 @@ public final class LayoutComponentsFilter extends Filter {
// Paths.
final var communityPosts = new StringFilterGroup(
communityPosts = new StringFilterGroup(
Settings.HIDE_COMMUNITY_POSTS,
"post_base_wrapper", // may be obsolete and no longer needed.
"text_post_root.eml",
@@ -304,7 +305,7 @@ public final class LayoutComponentsFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
// This identifier is used not only in players but also in search results:
// https://github.com/ReVanced/revanced-patches/issues/3245
@@ -322,7 +323,13 @@ public final class LayoutComponentsFilter extends Filter {
}
if (matchedGroup == channelProfile) {
return channelProfileBuffer.check(protobufBufferArray).isFiltered();
return channelProfileBuffer.check(buffer).isFiltered();
}
if (matchedGroup == communityPosts && NavigationBar.isBackButtonVisible()) {
// Allow community posts on channel profile page,
// or if viewing an individual channel in the feed.
return false;
}
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
@@ -331,11 +338,11 @@ public final class LayoutComponentsFilter extends Filter {
return compactChannelBarInnerButton.check(path).isFiltered()
// The filter may be broad, but in the context of a compactChannelBarInnerButton,
// it's safe to assume that the button is the only thing that should be hidden.
&& joinMembershipButton.check(protobufBufferArray).isFiltered();
&& joinMembershipButton.check(buffer).isFiltered();
}
if (matchedGroup == horizontalShelves) {
return contentIndex == 0 && (hideShelves() || ticketShelf.check(protobufBufferArray).isFiltered());
return contentIndex == 0 && (hideShelves() || ticketShelf.check(buffer).isFiltered());
}
if (matchedGroup == chipBar) {

View File

@@ -17,29 +17,28 @@ public final class LithoFilterPatch {
* Simple wrapper to pass the litho parameters through the prefix search.
*/
private static final class LithoFilterParameters {
@Nullable
final String identifier;
final String path;
final byte[] protoBuffer;
final byte[] buffer;
LithoFilterParameters(@Nullable String lithoIdentifier, String lithoPath, byte[] protoBuffer) {
LithoFilterParameters(String lithoIdentifier, String lithoPath, byte[] buffer) {
this.identifier = lithoIdentifier;
this.path = lithoPath;
this.protoBuffer = protoBuffer;
this.buffer = buffer;
}
@NonNull
@Override
public String toString() {
// Estimate the percentage of the buffer that are Strings.
StringBuilder builder = new StringBuilder(Math.max(100, protoBuffer.length / 2));
StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2));
builder.append( "ID: ");
builder.append(identifier);
builder.append(" Path: ");
builder.append(path);
if (Settings.DEBUG_PROTOBUFFER.get()) {
builder.append(" BufferStrings: ");
findAsciiStrings(builder, protoBuffer);
findAsciiStrings(builder, buffer);
}
return builder.toString();
@@ -128,21 +127,21 @@ public final class LithoFilterPatch {
private static void filterUsingCallbacks(StringTrieSearch pathSearchTree,
Filter filter, List<StringFilterGroup> groups,
Filter.FilterContentType type) {
String filterSimpleName = filter.getClass().getSimpleName();
for (StringFilterGroup group : groups) {
if (!group.includeInSearch()) {
continue;
}
for (String pattern : group.filters) {
String filterSimpleName = filter.getClass().getSimpleName();
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex,
matchedLength, callbackParameter) -> {
if (!group.isEnabled()) return false;
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
final boolean isFiltered = filter.isFiltered(parameters.identifier,
parameters.path, parameters.protoBuffer, group, type, matchedStartIndex);
parameters.path, parameters.buffer, group, type, matchedStartIndex);
if (isFiltered && BaseSettings.DEBUG.get()) {
if (type == Filter.FilterContentType.IDENTIFIER) {
@@ -163,6 +162,7 @@ public final class LithoFilterPatch {
/**
* Injection point. Called off the main thread.
* Targets 20.22+
*/
public static void setProtoBuffer(byte[] buffer) {
// Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes.
@@ -193,9 +193,9 @@ public final class LithoFilterPatch {
/**
* Injection point.
*/
public static boolean shouldFilter(@Nullable String lithoIdentifier, StringBuilder pathBuilder) {
public static boolean isFiltered(String lithoIdentifier, StringBuilder pathBuilder) {
try {
if (pathBuilder.length() == 0) {
if (lithoIdentifier.isEmpty() && pathBuilder.length() == 0) {
return false;
}
@@ -210,7 +210,7 @@ public final class LithoFilterPatch {
lithoIdentifier, pathBuilder.toString(), buffer);
Logger.printDebug(() -> "Searching " + parameter);
if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) {
if (identifierSearchTree.matches(parameter.identifier, parameter)) {
return true;
}
@@ -218,7 +218,7 @@ public final class LithoFilterPatch {
return true;
}
} catch (Exception ex) {
Logger.printException(() -> "Litho filter failure", ex);
Logger.printException(() -> "isFiltered failure", ex);
}
return false;

View File

@@ -1,7 +1,5 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
import app.revanced.extension.youtube.settings.Settings;
@@ -38,7 +36,7 @@ public final class PlaybackSpeedMenuFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == oldPlaybackMenuGroup) {
isOldPlaybackSpeedMenuVisible = true;

View File

@@ -1,7 +1,5 @@
package app.revanced.extension.youtube.patches.components;
import androidx.annotation.Nullable;
import app.revanced.extension.shared.settings.Setting;
import app.revanced.extension.shared.spoof.SpoofVideoStreamsPatch;
import app.revanced.extension.youtube.settings.Settings;
@@ -96,7 +94,7 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (matchedGroup == videoQualityMenuFooter) {
return true;
@@ -107,10 +105,10 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
}
// Shorts also use this player flyout panel
if (PlayerType.getCurrent().isNoneOrHidden() || exception.check(protobufBufferArray).isFiltered()) {
if (PlayerType.getCurrent().isNoneOrHidden() || exception.check(buffer).isFiltered()) {
return false;
}
return flyoutFilterGroupList.check(protobufBufferArray).isFiltered();
return flyoutFilterGroupList.check(buffer).isFiltered();
}
}

View File

@@ -84,15 +84,15 @@ public final class ReturnYouTubeDislikeFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) {
return false;
}
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray);
FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(buffer);
if (result.isFiltered()) {
String matchedVideoId = findVideoId(protobufBufferArray);
String matchedVideoId = findVideoId(buffer);
// Matched video will be null if in incognito mode.
// Must pass a null id to correctly clear out the current video data.
// Otherwise if a Short is opened in non-incognito, then incognito is enabled and another Short is opened,

View File

@@ -4,8 +4,6 @@ import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButt
import android.view.View;
import androidx.annotation.Nullable;
import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
import java.lang.ref.WeakReference;
@@ -13,7 +11,6 @@ import java.util.Arrays;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.NavigationBar;
import app.revanced.extension.youtube.shared.PlayerType;
@@ -321,7 +318,7 @@ public final class ShortsFilter extends Filter {
}
@Override
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
boolean isFiltered(String identifier, String path, byte[] buffer,
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
if (contentType == FilterContentType.PATH) {
if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
@@ -330,22 +327,22 @@ public final class ShortsFilter extends Filter {
}
if (matchedGroup == useSoundButton) {
return useSoundButtonBuffer.check(protobufBufferArray).isFiltered();
return useSoundButtonBuffer.check(buffer).isFiltered();
}
if (matchedGroup == useTemplateButton) {
return useTemplateButtonBuffer.check(protobufBufferArray).isFiltered();
return useTemplateButtonBuffer.check(buffer).isFiltered();
}
if (matchedGroup == shortsCompactFeedVideo) {
return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered();
return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(buffer).isFiltered();
}
// Video action buttons (comment, share, remix) have the same path.
// Like and dislike are separate path filters and don't require buffer searching.
if (matchedGroup == shortsActionBar) {
return videoActionButton.check(path).isFiltered()
&& videoActionButtonBuffer.check(protobufBufferArray).isFiltered();
&& videoActionButtonBuffer.check(buffer).isFiltered();
}
if (matchedGroup == suggestedAction) {
@@ -356,7 +353,7 @@ public final class ShortsFilter extends Filter {
return true;
}
return suggestedActionsBuffer.check(protobufBufferArray).isFiltered();
return suggestedActionsBuffer.check(buffer).isFiltered();
}
return true;

View File

@@ -18,7 +18,7 @@ import app.revanced.extension.youtube.settings.Settings;
public final class AdvancedVideoQualityMenuPatch {
/**
* Injection point.
* Injection point. Regular videos.
*/
public static void onFlyoutMenuCreate(RecyclerView recyclerView) {
if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return;
@@ -61,22 +61,12 @@ public final class AdvancedVideoQualityMenuPatch {
});
}
/**
* Injection point.
*
* Used to force the creation of the advanced menu item for the Shorts quality flyout.
*/
public static boolean forceAdvancedVideoQualityMenuCreation(boolean original) {
return Settings.ADVANCED_VIDEO_QUALITY_MENU.get() || original;
}
/**
* Injection point.
*
* Shorts video quality flyout.
*/
public static void showAdvancedVideoQualityMenu(ListView listView) {
public static void addVideoQualityListMenuListener(ListView listView) {
if (!Settings.ADVANCED_VIDEO_QUALITY_MENU.get()) return;
listView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
@@ -91,7 +81,6 @@ public final class AdvancedVideoQualityMenuPatch {
listView.setSoundEffectsEnabled(false);
final var qualityItemMenuPosition = 4;
listView.performItemClick(null, qualityItemMenuPosition, 0);
} catch (Exception ex) {
Logger.printException(() -> "showAdvancedVideoQualityMenu failure", ex);
}
@@ -102,4 +91,13 @@ public final class AdvancedVideoQualityMenuPatch {
}
});
}
/**
* Injection point.
*
* Used to force the creation of the advanced menu item for the Shorts quality flyout.
*/
public static boolean forceAdvancedVideoQualityMenuCreation(boolean original) {
return Settings.ADVANCED_VIDEO_QUALITY_MENU.get() || original;
}
}

View File

@@ -3,12 +3,7 @@ package app.revanced.extension.youtube.patches.playback.quality;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.NetworkType;
import androidx.annotation.Nullable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
@@ -20,166 +15,96 @@ import app.revanced.extension.youtube.shared.ShortsPlayerState;
@SuppressWarnings("unused")
public class RememberVideoQualityPatch {
private static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
private static final IntegerSetting videoQualityWifi = Settings.VIDEO_QUALITY_DEFAULT_WIFI;
private static final IntegerSetting videoQualityMobile = Settings.VIDEO_QUALITY_DEFAULT_MOBILE;
private static final IntegerSetting shortsQualityWifi = Settings.SHORTS_QUALITY_DEFAULT_WIFI;
private static final IntegerSetting shortsQualityMobile = Settings.SHORTS_QUALITY_DEFAULT_MOBILE;
private static boolean qualityNeedsUpdating;
/**
* If the user selected a new quality from the flyout menu,
* and {@link Settings#REMEMBER_VIDEO_QUALITY_LAST_SELECTED} is enabled.
*/
private static boolean userChangedDefaultQuality;
/**
* Index of the video quality chosen by the user from the flyout menu.
*/
private static int userSelectedQualityIndex;
/**
* The available qualities of the current video in human readable form: [1080, 720, 480]
*/
@Nullable
private static List<Integer> videoQualities;
private static boolean shouldRememberVideoQuality() {
BooleanSetting preference = ShortsPlayerState.isOpen() ?
Settings.REMEMBER_SHORTS_QUALITY_LAST_SELECTED
public static boolean shouldRememberVideoQuality() {
BooleanSetting preference = ShortsPlayerState.isOpen()
? Settings.REMEMBER_SHORTS_QUALITY_LAST_SELECTED
: Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED;
return preference.get();
}
private static void changeDefaultQuality(int defaultQuality) {
public static int getDefaultQualityResolution() {
final boolean isShorts = ShortsPlayerState.isOpen();
IntegerSetting preference = Utils.getNetworkType() == NetworkType.MOBILE
? (isShorts ? shortsQualityMobile : videoQualityMobile)
: (isShorts ? shortsQualityWifi : videoQualityWifi);
return preference.get();
}
public static void saveDefaultQuality(int qualityResolution) {
final boolean shortPlayerOpen = ShortsPlayerState.isOpen();
String networkTypeMessage;
boolean useShortsPreference = ShortsPlayerState.isOpen();
IntegerSetting qualitySetting;
if (Utils.getNetworkType() == NetworkType.MOBILE) {
if (useShortsPreference) shortsQualityMobile.save(defaultQuality);
else videoQualityMobile.save(defaultQuality);
networkTypeMessage = str("revanced_remember_video_quality_mobile");
qualitySetting = shortPlayerOpen ? shortsQualityMobile : videoQualityMobile;
} else {
if (useShortsPreference) shortsQualityWifi.save(defaultQuality);
else videoQualityWifi.save(defaultQuality);
networkTypeMessage = str("revanced_remember_video_quality_wifi");
qualitySetting = shortPlayerOpen ? shortsQualityWifi : videoQualityWifi;
}
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get())
if (qualitySetting.get() == qualityResolution) {
// User clicked the same video quality as the current video,
// or changed between 1080p Premium and non-Premium.
return;
}
qualitySetting.save(qualityResolution);
if (Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) {
String qualityLabel = qualityResolution + "p";
Utils.showToastShort(str(
useShortsPreference ? "revanced_remember_video_quality_toast_shorts" : "revanced_remember_video_quality_toast",
networkTypeMessage, (defaultQuality + "p")
));
shortPlayerOpen
? "revanced_remember_video_quality_toast_shorts"
: "revanced_remember_video_quality_toast",
networkTypeMessage,
qualityLabel)
);
}
}
/**
* Injection point.
*
* @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
* @param originalQualityIndex quality index to use, as chosen by YouTube
* @param userSelectedQualityIndex Element index of {@link VideoInformation#getCurrentQualities()}.
*/
public static int setVideoQuality(Object[] qualities, final int originalQualityIndex, Object qInterface, String qIndexMethod) {
public static void userChangedShortsQuality(int userSelectedQualityIndex) {
try {
boolean useShortsPreference = ShortsPlayerState.isOpen();
final int preferredQuality = Utils.getNetworkType() == NetworkType.MOBILE
? (useShortsPreference ? shortsQualityMobile : videoQualityMobile).get()
: (useShortsPreference ? shortsQualityWifi : videoQualityWifi).get();
if (!userChangedDefaultQuality && preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
return originalQualityIndex; // Nothing to do.
}
if (videoQualities == null || videoQualities.size() != qualities.length) {
videoQualities = new ArrayList<>(qualities.length);
for (Object streamQuality : qualities) {
for (Field field : streamQuality.getClass().getFields()) {
if (field.getType().isAssignableFrom(Integer.TYPE)
&& field.getName().length() <= 2) {
videoQualities.add(field.getInt(streamQuality));
}
}
if (shouldRememberVideoQuality()) {
VideoQuality[] currentQualities = VideoInformation.getCurrentQualities();
if (currentQualities == null) {
Logger.printDebug(() -> "Cannot save default quality, qualities is null");
return;
}
// After changing videos the qualities can initially be for the prior video.
// So if the qualities have changed an update is needed.
qualityNeedsUpdating = true;
Logger.printDebug(() -> "VideoQualities: " + videoQualities);
VideoQuality quality = currentQualities[userSelectedQualityIndex];
saveDefaultQuality(quality.patch_getResolution());
}
if (userChangedDefaultQuality) {
userChangedDefaultQuality = false;
final int quality = videoQualities.get(userSelectedQualityIndex);
Logger.printDebug(() -> "User changed default quality to: " + quality);
changeDefaultQuality(quality);
return userSelectedQualityIndex;
}
if (!qualityNeedsUpdating) {
return originalQualityIndex;
}
qualityNeedsUpdating = false;
// Find the highest quality that is equal to or less than the preferred.
int qualityToUse = videoQualities.get(0); // first element is automatic mode
int qualityIndexToUse = 0;
int i = 0;
for (Integer quality : videoQualities) {
if (quality <= preferredQuality && qualityToUse < quality) {
qualityToUse = quality;
qualityIndexToUse = i;
}
i++;
}
// If the desired quality index is equal to the original index,
// then the video is already set to the desired default quality.
final int qualityToUseFinal = qualityToUse;
if (qualityIndexToUse == originalQualityIndex) {
// On first load of a new video, if the UI video quality flyout menu
// is not updated then it will still show 'Auto' (ie: Auto (480p)),
// even though it's already set to the desired resolution.
//
// To prevent confusion, set the video index anyways (even if it matches the existing index)
// as that will force the UI picker to not display "Auto".
Logger.printDebug(() -> "Video is already preferred quality: " + qualityToUseFinal);
} else {
Logger.printDebug(() -> "Changing video quality from: "
+ videoQualities.get(originalQualityIndex) + " to: " + qualityToUseFinal);
}
Method m = qInterface.getClass().getMethod(qIndexMethod, Integer.TYPE);
m.invoke(qInterface, qualityToUse);
return qualityIndexToUse;
} catch (Exception ex) {
Logger.printException(() -> "Failed to set quality", ex);
return originalQualityIndex;
Logger.printException(() -> "userChangedShortsQuality failure", ex);
}
}
/**
* Injection point. Old quality menu.
* Injection point. Regular videos.
* @param videoResolution Human readable resolution: 480, 720, 1080.
*/
public static void userChangedQuality(int selectedQualityIndex) {
public static void userChangedQuality(int videoResolution) {
Utils.verifyOnMainThread();
Logger.printDebug(() -> "User changed quality to: " + videoResolution);
if (shouldRememberVideoQuality()) {
userSelectedQualityIndex = selectedQualityIndex;
userChangedDefaultQuality = true;
saveDefaultQuality(videoResolution);
}
}
/**
* Injection point. New quality menu.
*/
public static void userChangedQualityInNewFlyout(int selectedQuality) {
if (!shouldRememberVideoQuality()) return;
changeDefaultQuality(selectedQuality); // Quality is human readable resolution (ie: 1080).
}
/**
* Injection point.
*/
public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) {
Logger.printDebug(() -> "newVideoStarted");
qualityNeedsUpdating = true;
videoQualities = null;
VideoInformation.setDesiredVideoResolution(getDefaultQualityResolution());
}
}

View File

@@ -100,7 +100,8 @@ public class CustomPlaybackSpeedPatch {
private static WeakReference<Dialog> currentDialog = new WeakReference<>(null);
static {
// Cap at 2 decimals (rounds automatically).
// Use same 2 digit format as built in speed picker,
speedFormatter.setMinimumFractionDigits(2);
speedFormatter.setMaximumFractionDigits(2);
final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get();
@@ -321,7 +322,7 @@ public class CustomPlaybackSpeedPatch {
TextView currentSpeedText = new TextView(context);
float currentSpeed = VideoInformation.getPlaybackSpeed();
// Initially show with only 0 minimum digits, so 1.0 shows as 1x
currentSpeedText.setText(formatSpeedStringX(currentSpeed, 0));
currentSpeedText.setText(formatSpeedStringX(currentSpeed));
currentSpeedText.setTextColor(Utils.getAppForegroundColor());
currentSpeedText.setTextSize(16);
currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD);
@@ -398,10 +399,11 @@ public class CustomPlaybackSpeedPatch {
return null;
}
VideoInformation.overridePlaybackSpeed(roundedSpeed);
RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
currentSpeedText.setText(formatSpeedStringX(roundedSpeed, 2)); // Update display.
currentSpeedText.setText(formatSpeedStringX(roundedSpeed)); // Update display.
speedSlider.setProgress(speedToProgressValue(roundedSpeed)); // Update slider.
RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
VideoInformation.overridePlaybackSpeed(roundedSpeed);
return null;
};
@@ -437,7 +439,7 @@ public class CustomPlaybackSpeedPatch {
gridParams.setMargins(0, 0, 0, 0); // No margins around GridLayout.
gridLayout.setLayoutParams(gridParams);
// For all buttons show at least 1 zero in decimal (2 -> "2.0").
// For button use 1 digit minimum.
speedFormatter.setMinimumFractionDigits(1);
// Add buttons for each preset playback speed.
@@ -455,7 +457,7 @@ public class CustomPlaybackSpeedPatch {
// Create speed button.
Button speedButton = new Button(context, null, 0);
speedButton.setText(speedFormatter.format(speed)); // Do not use 'x' speed format.
speedButton.setText(speedFormatter.format(speed));
speedButton.setTextColor(Utils.getAppForegroundColor());
speedButton.setTextSize(12);
speedButton.setAllCaps(false);
@@ -498,6 +500,9 @@ public class CustomPlaybackSpeedPatch {
gridLayout.addView(buttonContainer);
}
// Restore 2 digit minimum.
speedFormatter.setMinimumFractionDigits(2);
// Add in-rows speed buttons layout to main layout.
mainLayout.addView(gridLayout);
@@ -631,8 +636,7 @@ public class CustomPlaybackSpeedPatch {
* @param speed The playback speed value to format.
* @return A string representation of the speed with 'x' (e.g. "1.25x" or "1.00x").
*/
private static String formatSpeedStringX(float speed, int minimumFractionDigits) {
speedFormatter.setMinimumFractionDigits(minimumFractionDigits);
private static String formatSpeedStringX(float speed) {
return speedFormatter.format(speed) + 'x';
}
@@ -671,11 +675,9 @@ public class CustomPlaybackSpeedPatch {
*/
public static int getAdjustedBackgroundColor(boolean isHandleBar) {
final int baseColor = Utils.getDialogBackgroundColor();
float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
return Utils.isDarkModeEnabled()
? Utils.adjustColorBrightness(baseColor, darkThemeFactor) // Lighten for dark theme.
: Utils.adjustColorBrightness(baseColor, lightThemeFactor); // Darken for light theme.
final float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
final float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
return Utils.adjustColorBrightness(baseColor, lightThemeFactor, darkThemeFactor);
}
}

View File

@@ -172,6 +172,7 @@ public class Settings extends BaseSettings {
public static final BooleanSetting HIDE_VIDEO_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE);
public static final BooleanSetting OPEN_VIDEOS_FULLSCREEN_PORTRAIT = new BooleanSetting("revanced_open_videos_fullscreen_portrait", FALSE);
public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE);
public static final BooleanSetting VIDEO_QUALITY_DIALOG_BUTTON = new BooleanSetting("revanced_video_quality_dialog_button", FALSE);
public static final IntegerSetting PLAYER_OVERLAY_OPACITY = new IntegerSetting("revanced_player_overlay_opacity", 100, true);
public static final BooleanSetting PLAYER_POPUP_PANELS = new BooleanSetting("revanced_hide_player_popup_panels", FALSE);

View File

@@ -333,10 +333,8 @@ class AbstractPreferenceSearchData<T extends Preference> {
return text;
}
final int baseColor = Utils.getAppBackgroundColor();
final int adjustedColor = Utils.isDarkModeEnabled()
? Utils.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme.
: Utils.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
final int adjustedColor = Utils.adjustColorBrightness(Utils.getAppBackgroundColor(),
0.95f, 1.20f);
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
SpannableStringBuilder spannable = new SpannableStringBuilder(text);

View File

@@ -26,6 +26,7 @@ public class CreateSegmentButton {
controlsView,
"revanced_sb_create_segment_button",
null,
null,
CreateSegmentButton::shouldBeShown,
v -> SponsorBlockViewController.toggleNewSegmentLayoutVisibility(),
null

View File

@@ -28,6 +28,7 @@ public class VotingButton {
controlsView,
"revanced_sb_voting_button",
null,
null,
VotingButton::shouldBeShown,
v -> SponsorBlockUtils.onVotingClicked(v.getContext()),
null

View File

@@ -7,7 +7,6 @@ import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.youtube.patches.CopyVideoUrlPatch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@SuppressWarnings("unused")
public class CopyVideoUrlButton {
@@ -23,6 +22,7 @@ public class CopyVideoUrlButton {
controlsView,
"revanced_copy_video_url_button",
"revanced_copy_video_url_button_placeholder",
null,
Settings.COPY_VIDEO_URL::get,
view -> CopyVideoUrlPatch.copyUrl(false),
view -> {

View File

@@ -7,7 +7,6 @@ import androidx.annotation.Nullable;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.youtube.patches.CopyVideoUrlPatch;
import app.revanced.extension.youtube.settings.Settings;
import app.revanced.extension.youtube.shared.PlayerType;
@SuppressWarnings("unused")
public class CopyVideoUrlTimestampButton {
@@ -23,6 +22,7 @@ public class CopyVideoUrlTimestampButton {
controlsView,
"revanced_copy_video_url_timestamp_button",
"revanced_copy_video_url_timestamp_button_placeholder",
null,
Settings.COPY_VIDEO_URL_TIMESTAMP::get,
view -> CopyVideoUrlPatch.copyUrl(true),
view -> {

View File

@@ -23,6 +23,7 @@ public class ExternalDownloadButton {
controlsView,
"revanced_external_download_button",
"revanced_external_download_button_placeholder",
null,
Settings.EXTERNAL_DOWNLOADER::get,
ExternalDownloadButton::onDownloadClick,
null

View File

@@ -4,16 +4,26 @@ import android.view.View;
import androidx.annotation.Nullable;
import java.text.DecimalFormat;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
import app.revanced.extension.youtube.settings.Settings;
@SuppressWarnings("unused")
public class PlaybackSpeedDialogButton {
@Nullable
private static PlayerControlButton instance;
private static final DecimalFormat speedDecimalFormatter = new DecimalFormat();
static {
speedDecimalFormatter.setMinimumFractionDigits(1);
speedDecimalFormatter.setMaximumFractionDigits(2);
}
/**
* Injection point.
*/
@@ -21,8 +31,10 @@ public class PlaybackSpeedDialogButton {
try {
instance = new PlayerControlButton(
controlsView,
"revanced_playback_speed_dialog_button_container",
"revanced_playback_speed_dialog_button",
"revanced_playback_speed_dialog_button_placeholder",
"revanced_playback_speed_dialog_button_text",
Settings.PLAYBACK_SPEED_DIALOG_BUTTON::get,
view -> {
try {
@@ -37,11 +49,11 @@ public class PlaybackSpeedDialogButton {
},
view -> {
try {
final float defaultSpeed = Settings.PLAYBACK_SPEED_DEFAULT.get();
final float speed = (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get() ||
VideoInformation.getPlaybackSpeed() == Settings.PLAYBACK_SPEED_DEFAULT.get())
VideoInformation.getPlaybackSpeed() == defaultSpeed)
? 1.0f
: Settings.PLAYBACK_SPEED_DEFAULT.get();
: defaultSpeed;
VideoInformation.overridePlaybackSpeed(speed);
} catch (Exception ex) {
Logger.printException(() -> "speed button reset failure", ex);
@@ -49,22 +61,53 @@ public class PlaybackSpeedDialogButton {
return true;
}
);
// Set the appropriate icon.
updateButtonAppearance();
} catch (Exception ex) {
Logger.printException(() -> "initializeButton failure", ex);
}
}
/**
* injection point
* Injection point.
*/
public static void setVisibilityImmediate(boolean visible) {
if (instance != null) instance.setVisibilityImmediate(visible);
if (instance != null) {
instance.setVisibilityImmediate(visible);
}
}
/**
* injection point
* Injection point.
*/
public static void setVisibility(boolean visible, boolean animated) {
if (instance != null) instance.setVisibility(visible, animated);
if (instance != null) {
instance.setVisibility(visible, animated);
}
}
}
/**
* Injection point.
*/
public static void videoSpeedChanged(float currentVideoSpeed) {
updateButtonAppearance();
}
/**
* Updates the button's appearance, including icon and text overlay.
*/
private static void updateButtonAppearance() {
if (instance == null) return;
try {
Utils.verifyOnMainThread();
String speedText = speedDecimalFormatter.format(VideoInformation.getPlaybackSpeed());
instance.setTextOverlay(speedText);
Logger.printDebug(() -> "Updated playback speed button text to: " + speedText);
} catch (Exception ex) {
Logger.printException(() -> "updateButtonAppearance failure", ex);
}
}
}

View File

@@ -3,6 +3,7 @@ package app.revanced.extension.youtube.videoplayer;
import android.view.View;
import android.view.animation.Animation;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
@@ -14,11 +15,12 @@ import app.revanced.extension.youtube.shared.PlayerType;
import kotlin.Unit;
public class PlayerControlButton {
public interface PlayerControlButtonVisibility {
public interface PlayerControlButtonStatus {
/**
* @return If the button should be shown when the player overlay is visible.
*/
boolean shouldBeShown();
boolean buttonEnabled();
}
private static final int fadeInDuration;
@@ -44,23 +46,46 @@ public class PlayerControlButton {
fadeOutImmediate.setDuration(Utils.getResourceInteger("fade_duration_fast"));
}
private final WeakReference<View> containerRef;
private final WeakReference<View> buttonRef;
/**
* Empty view with the same layout size as the button. Used to fill empty space while the
* fade out animation runs. Without this the chapter titles overlapping the button when fading out.
*/
private final WeakReference<View> placeHolderRef;
private final PlayerControlButtonVisibility visibilityCheck;
private final WeakReference<TextView> textOverlayRef;
private final PlayerControlButtonStatus enabledStatus;
private boolean isVisible;
public PlayerControlButton(View controlsViewGroup,
String imageViewButtonId,
String buttonId,
@Nullable String placeholderId,
PlayerControlButtonVisibility buttonVisibility,
@Nullable String textOverlayId,
PlayerControlButtonStatus enabledStatus,
View.OnClickListener onClickListener,
@Nullable View.OnLongClickListener longClickListener) {
ImageView imageView = Utils.getChildViewByResourceName(controlsViewGroup, imageViewButtonId);
imageView.setVisibility(View.GONE);
this(controlsViewGroup, buttonId, buttonId, placeholderId, textOverlayId,
enabledStatus, onClickListener, longClickListener);
}
public PlayerControlButton(View controlsViewGroup,
String viewToHide,
String buttonId,
@Nullable String placeholderId,
@Nullable String textOverlayId,
PlayerControlButtonStatus enabledStatus,
View.OnClickListener onClickListener,
@Nullable View.OnLongClickListener longClickListener) {
View containerView = Utils.getChildViewByResourceName(controlsViewGroup, viewToHide);
containerView.setVisibility(View.GONE);
containerRef = new WeakReference<>(containerView);
View button = Utils.getChildViewByResourceName(controlsViewGroup, buttonId);
button.setOnClickListener(onClickListener);
if (longClickListener != null) {
button.setOnLongClickListener(longClickListener);
}
buttonRef = new WeakReference<>(button);
View tempPlaceholder = null;
if (placeholderId != null) {
@@ -69,19 +94,19 @@ public class PlayerControlButton {
}
placeHolderRef = new WeakReference<>(tempPlaceholder);
imageView.setOnClickListener(onClickListener);
if (longClickListener != null) {
imageView.setOnLongClickListener(longClickListener);
TextView tempTextOverlay = null;
if (textOverlayId != null) {
tempTextOverlay = Utils.getChildViewByResourceName(controlsViewGroup, textOverlayId);
}
textOverlayRef = new WeakReference<>(tempTextOverlay);
visibilityCheck = buttonVisibility;
buttonRef = new WeakReference<>(imageView);
this.enabledStatus = enabledStatus;
isVisible = false;
// Update the visibility after the player type changes.
// This ensures that button animations are cleared and their states are updated correctly
// when switching between states like minimized, maximized, or fullscreen, preventing
// "stuck" animations or incorrect visibility. Without this fix the issue is most noticable
// "stuck" animations or incorrect visibility. Without this fix the issue is most noticeable
// when maximizing type 3 miniplayer.
PlayerType.getOnChange().addObserver((PlayerType type) -> {
playerTypeChanged(type);
@@ -111,33 +136,33 @@ public class PlayerControlButton {
if (isVisible == visible) return;
isVisible = visible;
View button = buttonRef.get();
if (button == null) return;
View container = containerRef.get();
if (container == null) return;
View placeholder = placeHolderRef.get();
final boolean shouldBeShown = visibilityCheck.shouldBeShown();
final boolean buttonEnabled = enabledStatus.buttonEnabled();
if (visible && shouldBeShown) {
button.clearAnimation();
if (visible && buttonEnabled) {
container.clearAnimation();
if (animated) {
button.startAnimation(PlayerControlButton.fadeInAnimation);
container.startAnimation(fadeInAnimation);
}
button.setVisibility(View.VISIBLE);
container.setVisibility(View.VISIBLE);
if (placeholder != null) {
placeholder.setVisibility(View.GONE);
}
} else {
if (button.getVisibility() == View.VISIBLE) {
button.clearAnimation();
if (container.getVisibility() == View.VISIBLE) {
container.clearAnimation();
if (animated) {
button.startAnimation(PlayerControlButton.fadeOutAnimation);
container.startAnimation(fadeOutAnimation);
}
button.setVisibility(View.GONE);
container.setVisibility(View.GONE);
}
if (placeholder != null) {
placeholder.setVisibility(shouldBeShown
placeholder.setVisibility(buttonEnabled
? View.VISIBLE
: View.GONE);
}
@@ -155,36 +180,59 @@ public class PlayerControlButton {
return;
}
View button = buttonRef.get();
if (button == null) return;
View container = containerRef.get();
if (container == null) return;
button.clearAnimation();
container.clearAnimation();
View placeholder = placeHolderRef.get();
if (visibilityCheck.shouldBeShown()) {
if (enabledStatus.buttonEnabled()) {
if (isVisible) {
button.setVisibility(View.VISIBLE);
container.setVisibility(View.VISIBLE);
if (placeholder != null) placeholder.setVisibility(View.GONE);
} else {
button.setVisibility(View.GONE);
container.setVisibility(View.GONE);
if (placeholder != null) placeholder.setVisibility(View.VISIBLE);
}
} else {
button.setVisibility(View.GONE);
container.setVisibility(View.GONE);
if (placeholder != null) placeholder.setVisibility(View.GONE);
}
}
public void hide() {
Utils.verifyOnMainThread();
if (!isVisible) return;
Utils.verifyOnMainThread();
View view = buttonRef.get();
View view = containerRef.get();
if (view == null) return;
view.setVisibility(View.GONE);
view = placeHolderRef.get();
if (view != null) view.setVisibility(View.GONE);
View placeHolder = placeHolderRef.get();
if (placeHolder != null) view.setVisibility(View.GONE);
isVisible = false;
}
}
/**
* Sets the icon of the button.
* @param resourceId Drawable identifier, or zero to hide the icon.
*/
public void setIcon(int resourceId) {
View button = buttonRef.get();
if (button instanceof ImageView imageButton) {
imageButton.setImageResource(resourceId);
}
}
/**
* Sets the text to be displayed on the text overlay.
* @param text The text to set on the overlay, or null to clear the text.
*/
public void setTextOverlay(CharSequence text) {
TextView textOverlay = textOverlayRef.get();
if (textOverlay != null) {
textOverlay.setText(text);
}
}
}

View File

@@ -0,0 +1,471 @@
package app.revanced.extension.youtube.videoplayer;
import static app.revanced.extension.shared.StringRef.str;
import static app.revanced.extension.shared.Utils.dipToPixels;
import static app.revanced.extension.youtube.patches.VideoInformation.AUTOMATIC_VIDEO_QUALITY_VALUE;
import static app.revanced.extension.youtube.patches.VideoInformation.VIDEO_QUALITY_1080P_PREMIUM_NAME;
import android.app.Dialog;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.ForegroundColorSpan;
import android.text.style.UnderlineSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.libraries.youtube.innertube.model.media.VideoQuality;
import java.util.ArrayList;
import java.util.List;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.youtube.patches.VideoInformation;
import app.revanced.extension.youtube.patches.playback.quality.RememberVideoQualityPatch;
import app.revanced.extension.youtube.settings.Settings;
import kotlin.Unit;
@SuppressWarnings("unused")
public class VideoQualityDialogButton {
@Nullable
private static PlayerControlButton instance;
@Nullable
private static CharSequence currentOverlayText;
static {
VideoInformation.onQualityChange.addObserver((@Nullable VideoQuality quality) -> {
updateButtonText(quality);
return Unit.INSTANCE;
});
}
/**
* Injection point.
*/
public static void initializeButton(View controlsView) {
try {
instance = new PlayerControlButton(
controlsView,
"revanced_video_quality_dialog_button_container",
"revanced_video_quality_dialog_button",
"revanced_video_quality_dialog_button_placeholder",
"revanced_video_quality_dialog_button_text",
Settings.VIDEO_QUALITY_DIALOG_BUTTON::get,
view -> {
try {
showVideoQualityDialog(view.getContext());
} catch (Exception ex) {
Logger.printException(() -> "Video quality button onClick failure", ex);
}
},
view -> {
try {
VideoQuality[] qualities = VideoInformation.getCurrentQualities();
if (qualities == null) {
Logger.printDebug(() -> "Cannot reset quality, videoQualities is null");
return true;
}
// Reset to default quality.
final int defaultResolution = RememberVideoQualityPatch.getDefaultQualityResolution();
for (VideoQuality quality : qualities) {
final int resolution = quality.patch_getResolution();
if (resolution != AUTOMATIC_VIDEO_QUALITY_VALUE && resolution <= defaultResolution) {
Logger.printDebug(() -> "Resetting quality to: " + quality);
VideoInformation.changeQuality(quality);
return true;
}
}
// Existing hook cannot set default quality to auto.
// Instead show the quality dialog.
showVideoQualityDialog(view.getContext());
return true;
} catch (Exception ex) {
Logger.printException(() -> "Video quality button reset failure", ex);
}
return false;
}
);
// Set initial text.
updateButtonText(VideoInformation.getCurrentQuality());
} catch (Exception ex) {
Logger.printException(() -> "initializeButton failure", ex);
}
}
/**
* Injection point.
*/
public static void setVisibilityImmediate(boolean visible) {
if (instance != null) {
instance.setVisibilityImmediate(visible);
}
}
/**
* Injection point.
*/
public static void setVisibility(boolean visible, boolean animated) {
if (instance != null) {
instance.setVisibility(visible, animated);
}
}
/**
* Updates the button text based on the current video quality.
*/
public static void updateButtonText(@Nullable VideoQuality quality) {
try {
Utils.verifyOnMainThread();
if (instance == null) return;
final int resolution = quality == null
? AUTOMATIC_VIDEO_QUALITY_VALUE // Video is still loading.
: quality.patch_getResolution();
SpannableStringBuilder text = new SpannableStringBuilder();
String qualityText = switch (resolution) {
case AUTOMATIC_VIDEO_QUALITY_VALUE -> "";
case 144, 240, 360 -> "LD";
case 480 -> "SD";
case 720 -> "HD";
case 1080 -> "FHD";
case 1440 -> "QHD";
case 2160 -> "4K";
default -> "?"; // Should never happen.
};
text.append(qualityText);
if (resolution == 1080 && VIDEO_QUALITY_1080P_PREMIUM_NAME.equals(quality.patch_getQualityName())) {
// Underline the entire "FHD" text for 1080p Premium.
text.setSpan(new UnderlineSpan(), 0, qualityText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
currentOverlayText = text;
Utils.runOnMainThreadDelayed(() -> {
if (currentOverlayText != text) {
Logger.printDebug(() -> "Ignoring stale button text update of: " + text);
return;
}
instance.setTextOverlay(text);
}, 100);
} catch (Exception ex) {
Logger.printException(() -> "updateButtonText failure", ex);
}
}
/**
* Shows a dialog with available video qualities, excluding Auto, with a title showing the current quality.
*/
private static void showVideoQualityDialog(Context context) {
try {
VideoQuality[] currentQualities = VideoInformation.getCurrentQualities();
VideoQuality currentQuality = VideoInformation.getCurrentQuality();
if (currentQualities == null || currentQuality == null) {
Logger.printDebug(() -> "Cannot show qualities dialog, videoQualities is null");
return;
}
if (currentQualities.length < 2) {
// Should never happen.
Logger.printException(() -> "Cannot show qualities dialog, no qualities available");
return;
}
// -1 adjustment for automatic quality at first index.
int listViewSelectedIndex = -1;
for (VideoQuality quality : currentQualities) {
if (quality.patch_getQualityName().equals(currentQuality.patch_getQualityName())) {
break;
}
listViewSelectedIndex++;
}
List<String> qualityLabels = new ArrayList<>(currentQualities.length - 1);
for (VideoQuality availableQuality : currentQualities) {
if (availableQuality.patch_getResolution() != AUTOMATIC_VIDEO_QUALITY_VALUE) {
qualityLabels.add(availableQuality.patch_getQualityName());
}
}
Dialog dialog = new Dialog(context);
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog.setCanceledOnTouchOutside(true);
dialog.setCancelable(true);
final int dip4 = dipToPixels(4); // Height for handle bar.
final int dip5 = dipToPixels(5); // Padding for mainLayout.
final int dip6 = dipToPixels(6); // Bottom margin.
final int dip8 = dipToPixels(8); // Side padding.
final int dip16 = dipToPixels(16); // Left padding for ListView.
final int dip20 = dipToPixels(20); // Margin below handle.
final int dip40 = dipToPixels(40); // Width for handle bar.
LinearLayout mainLayout = new LinearLayout(context);
mainLayout.setOrientation(LinearLayout.VERTICAL);
mainLayout.setPadding(dip5, dip8, dip5, dip8);
ShapeDrawable background = new ShapeDrawable(new RoundRectShape(
Utils.createCornerRadii(12), null, null));
background.getPaint().setColor(Utils.getDialogBackgroundColor());
mainLayout.setBackground(background);
View handleBar = new View(context);
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
Utils.createCornerRadii(4), null, null));
final int baseColor = Utils.getDialogBackgroundColor();
final int adjustedHandleBarBackgroundColor = Utils.adjustColorBrightness(
baseColor, 0.9f, 1.25f);
handleBackground.getPaint().setColor(adjustedHandleBarBackgroundColor);
handleBar.setBackground(handleBackground);
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(dip40, dip4);
handleParams.gravity = Gravity.CENTER_HORIZONTAL;
handleParams.setMargins(0, 0, 0, dip20);
handleBar.setLayoutParams(handleParams);
mainLayout.addView(handleBar);
// Create SpannableStringBuilder for formatted text.
SpannableStringBuilder spannableTitle = new SpannableStringBuilder();
String titlePart = str("video_quality_quick_menu_title");
String separatorPart = str("video_quality_title_seperator");
// Append title part with default foreground color.
spannableTitle.append(titlePart);
spannableTitle.setSpan(
new ForegroundColorSpan(Utils.getAppForegroundColor()),
0,
titlePart.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
);
spannableTitle.append(" "); // Space after title.
// Append separator part with adjusted title color.
int separatorStart = spannableTitle.length();
spannableTitle.append(separatorPart);
final int adjustedTitleForegroundColor = Utils.adjustColorBrightness(
Utils.getAppForegroundColor(), 1.6f, 0.6f);
spannableTitle.setSpan(
new ForegroundColorSpan(adjustedTitleForegroundColor),
separatorStart,
separatorStart + separatorPart.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
);
spannableTitle.append(" "); // Space after separator.
// Append quality label with adjusted title color.
final int qualityStart = spannableTitle.length();
spannableTitle.append(currentQuality.patch_getQualityName());
spannableTitle.setSpan(
new ForegroundColorSpan(adjustedTitleForegroundColor),
qualityStart,
qualityStart + currentQuality.patch_getQualityName().length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
);
// Add title with current quality.
TextView titleView = new TextView(context);
titleView.setText(spannableTitle);
titleView.setTextSize(16);
// Remove setTextColor since color is handled by SpannableStringBuilder.
LinearLayout.LayoutParams titleParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
titleParams.setMargins(dip8, 0, 0, dip20);
titleView.setLayoutParams(titleParams);
mainLayout.addView(titleView);
ListView listView = new ListView(context);
CustomQualityAdapter adapter = new CustomQualityAdapter(context, qualityLabels);
adapter.setSelectedPosition(listViewSelectedIndex);
listView.setAdapter(adapter);
listView.setDivider(null);
listView.setPadding(dip16, 0, 0, 0);
listView.setOnItemClickListener((parent, view, which, id) -> {
try {
final int originalIndex = which + 1; // Adjust for automatic.
VideoQuality selectedQuality = currentQualities[originalIndex];
RememberVideoQualityPatch.userChangedQuality(selectedQuality.patch_getResolution());
VideoInformation.changeQuality(selectedQuality);
dialog.dismiss();
} catch (Exception ex) {
Logger.printException(() -> "Video quality selection failure", ex);
}
});
LinearLayout.LayoutParams listViewParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
listViewParams.setMargins(0, 0, 0, dip5);
listView.setLayoutParams(listViewParams);
mainLayout.addView(listView);
LinearLayout wrapperLayout = new LinearLayout(context);
wrapperLayout.setOrientation(LinearLayout.VERTICAL);
wrapperLayout.setPadding(dip8, 0, dip8, 0);
wrapperLayout.addView(mainLayout);
dialog.setContentView(wrapperLayout);
Window window = dialog.getWindow();
if (window != null) {
WindowManager.LayoutParams params = window.getAttributes();
params.gravity = Gravity.BOTTOM;
params.y = dip6;
int portraitWidth = context.getResources().getDisplayMetrics().widthPixels;
if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
portraitWidth = Math.min(
portraitWidth,
context.getResources().getDisplayMetrics().heightPixels);
// Limit height in landscape mode.
params.height = Utils.percentageHeightToPixels(80);
} else {
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
}
params.width = portraitWidth;
window.setAttributes(params);
window.setBackgroundDrawable(null);
}
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
slideInABottomAnimation.setDuration(fadeDurationFast);
mainLayout.startAnimation(slideInABottomAnimation);
// noinspection ClickableViewAccessibility
mainLayout.setOnTouchListener(new View.OnTouchListener() {
final float dismissThreshold = dipToPixels(100);
float touchY;
float translationY;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touchY = event.getRawY();
translationY = mainLayout.getTranslationY();
return true;
case MotionEvent.ACTION_MOVE:
final float deltaY = event.getRawY() - touchY;
if (deltaY >= 0) {
mainLayout.setTranslationY(translationY + deltaY);
}
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mainLayout.getTranslationY() > dismissThreshold) {
//noinspection ExtractMethodRecommender
final float remainingDistance = context.getResources().getDisplayMetrics().heightPixels
- mainLayout.getTop();
TranslateAnimation slideOut = new TranslateAnimation(
0, 0, mainLayout.getTranslationY(), remainingDistance);
slideOut.setDuration(fadeDurationFast);
slideOut.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
dialog.dismiss();
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
mainLayout.startAnimation(slideOut);
} else {
TranslateAnimation slideBack = new TranslateAnimation(
0, 0, mainLayout.getTranslationY(), 0);
slideBack.setDuration(fadeDurationFast);
mainLayout.startAnimation(slideBack);
mainLayout.setTranslationY(0);
}
return true;
default:
return false;
}
}
});
dialog.show();
} catch (Exception ex) {
Logger.printException(() -> "showVideoQualityDialog failure", ex);
}
}
private static class CustomQualityAdapter extends ArrayAdapter<String> {
private static final int CUSTOM_LIST_ITEM_CHECKED_ID = Utils.getResourceIdentifier(
"revanced_custom_list_item_checked", "layout");
private static final int CHECK_ICON_ID = Utils.getResourceIdentifier(
"revanced_check_icon", "id");
private static final int CHECK_ICON_PLACEHOLDER_ID = Utils.getResourceIdentifier(
"revanced_check_icon_placeholder", "id");
private static final int ITEM_TEXT_ID = Utils.getResourceIdentifier(
"revanced_item_text", "id");
private int selectedPosition = -1;
public CustomQualityAdapter(@NonNull Context context, @NonNull List<String> objects) {
super(context, 0, objects);
}
private void setSelectedPosition(int position) {
this.selectedPosition = position;
notifyDataSetChanged();
}
@NonNull
@Override
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(
CUSTOM_LIST_ITEM_CHECKED_ID,
parent,
false
);
viewHolder = new ViewHolder();
viewHolder.checkIcon = convertView.findViewById(CHECK_ICON_ID);
viewHolder.placeholder = convertView.findViewById(CHECK_ICON_PLACEHOLDER_ID);
viewHolder.textView = convertView.findViewById(ITEM_TEXT_ID);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.textView.setText(getItem(position));
final boolean isSelected = position == selectedPosition;
viewHolder.checkIcon.setVisibility(isSelected ? View.VISIBLE : View.GONE);
viewHolder.placeholder.setVisibility(isSelected ? View.GONE : View.INVISIBLE);
return convertView;
}
private static class ViewHolder {
ImageView checkIcon;
View placeholder;
TextView textView;
}
}
}

View File

@@ -0,0 +1,8 @@
package com.google.android.libraries.youtube.innertube.model.media;
public abstract class VideoQuality implements Comparable<VideoQuality> {
public abstract String patch_getQualityName();
public abstract int patch_getResolution();
}

View File

@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
org.gradle.parallel = true
android.useAndroidX = true
kotlin.code.style = official
version = 5.33.0-dev.3
version = 5.34.0-dev.4

9
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"gradle-semantic-release-plugin": "^1.10.1",
"semantic-release": "^24.2.6"
"semantic-release": "^24.2.7"
}
},
"node_modules/@babel/code-frame": {
@@ -6889,10 +6889,11 @@
"license": "MIT"
},
"node_modules/semantic-release": {
"version": "24.2.6",
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.6.tgz",
"integrity": "sha512-D0cwjlO5RZzHHxAcsoF1HxiRLfC3ehw+ay+zntzFs6PNX6aV0JzKNG15mpxPipBYa/l4fHly88dHvgDyqwb1Ww==",
"version": "24.2.7",
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.7.tgz",
"integrity": "sha512-g7RssbTAbir1k/S7uSwSVZFfFXwpomUB9Oas0+xi9KStSCmeDXcA7rNhiskjLqvUe/Evhx8fVCT16OSa34eM5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@semantic-release/commit-analyzer": "^13.0.0-beta.1",
"@semantic-release/error": "^4.0.0",

View File

@@ -4,6 +4,6 @@
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/git": "^10.0.1",
"gradle-semantic-release-plugin": "^1.10.1",
"semantic-release": "^24.2.6"
"semantic-release": "^24.2.7"
}
}

View File

@@ -1660,6 +1660,10 @@ public final class app/revanced/patches/youtube/video/quality/RememberVideoQuali
public static final fun getRememberVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/video/quality/VideoQualityDialogButtonPatchKt {
public static final fun getVideoQualityDialogButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}
public final class app/revanced/patches/youtube/video/quality/VideoQualityPatchKt {
public static final fun getVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
}

View File

@@ -3,6 +3,7 @@ package app.revanced.patches.backdrops.misc.pro
import app.revanced.patcher.fingerprint
import com.android.tools.smali.dexlib2.Opcode
@Deprecated("Fingerprint no longer resolves and will soon be deleted.")
internal val proUnlockFingerprint = fingerprint {
opcodes(
Opcode.INVOKE_VIRTUAL,

View File

@@ -6,9 +6,8 @@ import app.revanced.patcher.patch.bytecodePatch
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@Suppress("unused")
val proUnlockPatch = bytecodePatch(
name = "Pro unlock",
) {
@Deprecated("This patch no longer works and will soon be deleted.")
val proUnlockPatch = bytecodePatch{
compatibleWith("com.backdrops.wallpapers")
execute {

View File

@@ -8,7 +8,7 @@ val signatureCheckPatch = bytecodePatch(
name = "Disable signature check",
description = "Disables the signature check that causes the app to crash on startup."
) {
compatibleWith("com.instagram.android"("378.0.0.52.68"))
compatibleWith("com.instagram.android")
execute {
isValidSignatureMethodFingerprint

View File

@@ -5,6 +5,5 @@ import app.revanced.patcher.fingerprint
internal val isFacebookButtonEnabledFingerprint = fingerprint {
parameters()
returns("Z")
strings("com.facebook.messaging.inbox.tab.plugins.core.tabtoolbarbutton." +
"facebookbutton.facebooktoolbarbutton.FacebookButtonTabButtonImplementation")
strings("FacebookButtonTabButtonImplementation")
}

View File

@@ -3,6 +3,7 @@ package app.revanced.patches.music.layout.compactheader
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.findFreeRegister
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@Suppress("unused")
@@ -17,13 +18,14 @@ val hideCategoryBar = bytecodePatch(
constructCategoryBarFingerprint.method.apply {
val insertIndex = constructCategoryBarFingerprint.patternMatch!!.startIndex
val register = getInstruction<OneRegisterInstruction>(insertIndex - 1).registerA
val freeRegister = findFreeRegister(insertIndex, register)
addInstructions(
insertIndex,
"""
const/16 v2, 0x8
invoke-virtual {v$register, v2}, Landroid/view/View;->setVisibility(I)V
""",
const/16 v$freeRegister, 0x8
invoke-virtual { v$register, v$freeRegister }, Landroid/view/View;->setVisibility(I)V
"""
)
}
}

View File

@@ -3,6 +3,7 @@ package app.revanced.patches.nfctoolsse.misc.pro
import com.android.tools.smali.dexlib2.AccessFlags
import app.revanced.patcher.fingerprint
@Deprecated("This patch no longer works and will soon be deleted.")
internal val isLicenseRegisteredFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC)
returns("Z")

View File

@@ -4,9 +4,8 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
@Suppress("unused")
val unlockProPatch = bytecodePatch(
name = "Unlock pro",
) {
@Deprecated("This patch no longer works and will soon be deleted.")
val unlockProPatch = bytecodePatch{
compatibleWith("com.wakdev.apps.nfctools.se")
execute {

View File

@@ -1,21 +1,15 @@
package app.revanced.patches.pixiv.ads
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.util.returnEarly
@Suppress("unused")
val hideAdsPatch = bytecodePatch(
name = "Hide ads",
) {
compatibleWith("jp.pxv.android")
compatibleWith("jp.pxv.android"("6.141.1"))
execute {
shouldShowAdsFingerprint.method.addInstructions(
0,
"""
const/4 v0, 0x0
return v0
""",
)
shouldShowAdsFingerprint.method.returnEarly(false)
}
}

View File

@@ -21,7 +21,7 @@ val audioAdsPatch = bytecodePatch(
addResourcesPatch,
)
compatibleWith("tv.twitch.android.app")
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
execute {
addResources("twitch", "ad.audio.audioAdsPatch")

View File

@@ -19,7 +19,7 @@ val embeddedAdsPatch = bytecodePatch(
settingsPatch,
)
compatibleWith("tv.twitch.android.app")
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
execute {
addResources("twitch", "ad.embedded.embeddedAdsPatch")

View File

@@ -155,7 +155,5 @@ val videoAdsPatch = bytecodePatch(
},
)
compatibleWith(
"tv.twitch.android.app",
)
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
}

View File

@@ -22,7 +22,7 @@ val showDeletedMessagesPatch = bytecodePatch(
addResourcesPatch,
)
compatibleWith("tv.twitch.android.app")
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
fun createSpoilerConditionInstructions(register: String = "v0") = """
invoke-static {}, Lapp/revanced/extension/twitch/patches/ShowDeletedMessagesPatch;->shouldUseSpoiler()Z

View File

@@ -20,7 +20,7 @@ val autoClaimChannelPointsPatch = bytecodePatch(
addResourcesPatch,
)
compatibleWith("tv.twitch.android.app")
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
execute {
addResources("twitch", "chat.autoclaim.autoClaimChannelPointsPatch")

View File

@@ -20,7 +20,7 @@ val debugModePatch = bytecodePatch(
addResourcesPatch,
)
compatibleWith("tv.twitch.android.app")
compatibleWith("tv.twitch.android.app"("16.9.1", "25.3.0"))
execute {
addResources("twitch", "debug.debugModePatch")

View File

@@ -48,7 +48,7 @@ val settingsPatch = bytecodePatch(
settingsPatch(preferences = preferences),
)
compatibleWith("tv.twitch.android.app")
compatibleWith("tv.twitch.android.app"("16.9.1"))
execute {
addResources("twitch", "misc.settings.settingsPatch")

View File

@@ -156,7 +156,7 @@ val lithoFilterPatch = bytecodePatch(
move-object/from16 v$freeRegister, p2
iget-object v$identifierRegister, v$freeRegister, $conversionContextIdentifierField
iget-object v$pathRegister, v$freeRegister, $conversionContextPathBuilderField
invoke-static { v$identifierRegister, v$pathRegister }, $EXTENSION_CLASS_DESCRIPTOR->shouldFilter(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
invoke-static { v$identifierRegister, v$pathRegister }, $EXTENSION_CLASS_DESCRIPTOR->isFiltered(Ljava/lang/String;Ljava/lang/StringBuilder;)Z
move-result v$freeRegister
if-eqz v$freeRegister, :unfiltered

View File

@@ -282,7 +282,7 @@ val playerControlsPatch = bytecodePatch(
// The change to support this is simple and only requires adding buttons to both layout files,
// but for now force this different layout off since it's still an experimental test.
if (is_19_35_or_greater) {
playerBottomControlsExploderFeatureFlagFingerprint.method.returnEarly()
playerBottomControlsExploderFeatureFlagFingerprint.method.returnLate(false)
}
// A/B test of new top overlay controls. Two different layouts can be used:

View File

@@ -11,7 +11,6 @@ val recyclerViewTreeHookPatch = bytecodePatch {
dependsOn(sharedExtensionPatch)
execute {
recyclerViewTreeObserverFingerprint.method.apply {
val insertIndex = recyclerViewTreeObserverFingerprint.patternMatch!!.startIndex + 1
val recyclerViewParameter = 2

View File

@@ -121,7 +121,7 @@ internal val subtitleButtonControllerFingerprint = fingerprint {
)
}
internal val newVideoQualityChangedFingerprint = fingerprint {
internal val videoQualityChangedFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("L")
parameters("L")

View File

@@ -1,23 +1,27 @@
package app.revanced.patches.youtube.video.audio
import app.revanced.patcher.fingerprint
import app.revanced.util.containsLiteralInstruction
import com.android.tools.smali.dexlib2.AccessFlags
internal val streamingModelBuilderFingerprint = fingerprint {
internal val formatStreamModelToStringFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("L")
strings("vprng")
returns("Ljava/lang/String;")
custom { method, classDef ->
method.name == "toString" && classDef.type ==
"Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;"
}
}
internal val menuItemAudioTrackFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
parameters("L")
returns("V")
strings("menu_item_audio_track")
internal const val AUDIO_STREAM_IGNORE_DEFAULT_FEATURE_FLAG = 45666189L
internal val selectAudioStreamFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC)
returns("L")
custom { method, _ ->
method.parameters.size > 2 // Method has a large number of parameters and may change.
&& method.parameters[1].type == "Lcom/google/android/libraries/youtube/innertube/model/media/PlayerConfigModel;"
&& method.containsLiteralInstruction(AUDIO_STREAM_IGNORE_DEFAULT_FEATURE_FLAG)
}
}
internal val audioStreamingTypeSelector = fingerprint {
accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL)
returns("L")
strings("raw") // String is not unique
}

View File

@@ -5,22 +5,22 @@ import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWith
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playservice.is_20_07_or_greater
import app.revanced.patches.youtube.misc.playservice.versionCheckPatch
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.util.getReference
import app.revanced.util.findMethodFromToString
import app.revanced.util.indexOfFirstInstructionOrThrow
import app.revanced.util.insertLiteralOverride
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
import com.android.tools.smali.dexlib2.iface.Method
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.immutable.ImmutableField
import com.android.tools.smali.dexlib2.immutable.ImmutableMethod
import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter
@@ -37,6 +37,7 @@ val forceOriginalAudioPatch = bytecodePatch(
sharedExtensionPatch,
settingsPatch,
addResourcesPatch,
versionCheckPatch
)
compatibleWith(
@@ -60,100 +61,93 @@ val forceOriginalAudioPatch = bytecodePatch(
)
)
fun Method.firstFormatStreamingModelCall(
returnType: String = "Ljava/lang/String;"
): MutableMethod {
val audioTrackIdIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>()
reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;"
&& reference.returnType == returnType
}
return navigate(this).to(audioTrackIdIndex).stop()
// Disable feature flag that ignores the default track flag
// and instead overrides to the user region language.
if (is_20_07_or_greater) {
selectAudioStreamFingerprint.method.insertLiteralOverride(
AUDIO_STREAM_IGNORE_DEFAULT_FEATURE_FLAG,
"$EXTENSION_CLASS_DESCRIPTOR->ignoreDefaultAudioStream(Z)Z"
)
}
// Accessor methods of FormatStreamModel have no string constants and
// opcodes are identical to other methods in the same class,
// so must walk from another class that use the methods.
val isDefaultMethod = streamingModelBuilderFingerprint.originalMethod.firstFormatStreamingModelCall("Z")
val audioTrackIdMethod = menuItemAudioTrackFingerprint.originalMethod.firstFormatStreamingModelCall()
val audioTrackDisplayNameMethod = audioStreamingTypeSelector.originalMethod.firstFormatStreamingModelCall()
val formatStreamModelClass = proxy(classes.first {
it.type == audioTrackIdMethod.definingClass
}).mutableClass
formatStreamModelToStringFingerprint.let {
val isDefaultAudioTrackMethod = it.originalMethod.findMethodFromToString("isDefaultAudioTrack=")
val audioTrackDisplayNameMethod = it.originalMethod.findMethodFromToString("audioTrackDisplayName=")
val audioTrackIdMethod = it.originalMethod.findMethodFromToString("audioTrackId=")
formatStreamModelClass.apply {
// Add a new field to store the override.
val helperFieldName = "isDefaultAudioTrackOverride"
fields.add(
ImmutableField(
type,
helperFieldName,
"Ljava/lang/Boolean;",
// Boolean is a 100% immutable class (all fields are final)
// and safe to write to a shared field without volatile/synchronization,
// but without volatile the field can show stale data
// and the same field is calculated more than once by different threads.
AccessFlags.PRIVATE.value or AccessFlags.VOLATILE.value,
it.classDef.apply {
// Add a new field to store the override.
val helperFieldName = "patch_isDefaultAudioTrackOverride"
fields.add(
ImmutableField(
type,
helperFieldName,
"Ljava/lang/Boolean;",
// Boolean is a 100% immutable class (all fields are final)
// and safe to write to a shared field without volatile/synchronization,
// but without volatile the field can show stale data
// and the same field is calculated more than once by different threads.
AccessFlags.PRIVATE.value or AccessFlags.VOLATILE.value,
null,
null,
null
).toMutable()
)
// Add a helper method because the isDefaultAudioTrack() has only 2 registers and 3 are needed.
val helperMethodClass = type
val helperMethodName = "patch_isDefaultAudioTrack"
val helperMethod = ImmutableMethod(
helperMethodClass,
helperMethodName,
listOf(ImmutableMethodParameter("Z", null, null)),
"Z",
AccessFlags.PRIVATE.value,
null,
null,
null
).toMutable()
)
MutableMethodImplementation(6),
).toMutable().apply {
addInstructionsWithLabels(
0,
"""
iget-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean;
if-eqz v0, :call_extension
invoke-virtual { v0 }, Ljava/lang/Boolean;->booleanValue()Z
move-result v3
return v3
:call_extension
invoke-virtual { p0 }, $audioTrackIdMethod
move-result-object v1
invoke-virtual { p0 }, $audioTrackDisplayNameMethod
move-result-object v2
invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->isDefaultAudioStream(ZLjava/lang/String;Ljava/lang/String;)Z
move-result v3
invoke-static { v3 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;
move-result-object v0
iput-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean;
return v3
"""
)
}
methods.add(helperMethod)
// Add a helper method because the isDefaultAudioTrack() has only 2 registers and 3 are needed.
val helperMethodClass = type
val helperMethodName = "extension_isDefaultAudioTrack"
val helperMethod = ImmutableMethod(
helperMethodClass,
helperMethodName,
listOf(ImmutableMethodParameter("Z", null, null)),
"Z",
AccessFlags.PRIVATE.value,
null,
null,
MutableMethodImplementation(6),
).toMutable().apply {
addInstructionsWithLabels(
0,
"""
iget-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean;
if-eqz v0, :call_extension
invoke-virtual { v0 }, Ljava/lang/Boolean;->booleanValue()Z
move-result v3
return v3
:call_extension
invoke-virtual { p0 }, $audioTrackIdMethod
move-result-object v1
invoke-virtual { p0 }, $audioTrackDisplayNameMethod
move-result-object v2
invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->isDefaultAudioStream(ZLjava/lang/String;Ljava/lang/String;)Z
move-result v3
invoke-static { v3 }, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean;
move-result-object v0
iput-object v0, p0, $helperMethodClass->$helperFieldName:Ljava/lang/Boolean;
return v3
"""
)
}
methods.add(helperMethod)
// Modify isDefaultAudioTrack() to call extension helper method.
isDefaultAudioTrackMethod.apply {
val index = indexOfFirstInstructionOrThrow(Opcode.RETURN)
val register = getInstruction<OneRegisterInstruction>(index).registerA
// Modify isDefaultAudioTrack() to call extension helper method.
isDefaultMethod.apply {
val index = indexOfFirstInstructionOrThrow(Opcode.RETURN)
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstructions(
index,
"""
invoke-direct { p0, v$register }, $helperMethodClass->$helperMethodName(Z)Z
move-result v$register
"""
)
addInstructions(
index,
"""
invoke-direct { p0, v$register }, $helperMethodClass->$helperMethodName(Z)Z
move-result v$register
"""
)
}
}
}
}

View File

@@ -63,7 +63,7 @@ val disableHdrPatch = bytecodePatch(
return v0
:useHdr
nop
"""
"""
)
}
}

View File

@@ -1,7 +1,7 @@
package app.revanced.patches.youtube.video.information
import app.revanced.patcher.fingerprint
import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint
import app.revanced.patches.youtube.shared.videoQualityChangedFingerprint
import app.revanced.util.getReference
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
@@ -110,7 +110,7 @@ internal val seekRelativeFingerprint = fingerprint {
}
/**
* Resolves with the class found in [newVideoQualityChangedFingerprint].
* Resolves with the class found in [videoQualityChangedFingerprint].
*/
internal val playbackSpeedMenuSpeedChangedFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
@@ -133,3 +133,46 @@ internal val playbackSpeedClassFingerprint = fingerprint {
)
strings("PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT")
}
internal const val YOUTUBE_VIDEO_QUALITY_CLASS_TYPE = "Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;"
internal val videoQualityFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR)
parameters(
"I", // Resolution.
"Ljava/lang/String;", // Human readable resolution: "480p", "1080p Premium", etc
"Z",
"L"
)
custom { _, classDef ->
classDef.type == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
}
}
internal val videoQualitySetterFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
returns("V")
parameters("[L", "I", "Z")
opcodes(
Opcode.IF_EQZ,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.IPUT_BOOLEAN,
)
strings("menu_item_video_quality")
}
/**
* Matches with the class found in [videoQualitySetterFingerprint].
*/
internal val setVideoQualityFingerprint = fingerprint {
returns("V")
parameters("L")
opcodes(
Opcode.IGET_OBJECT,
Opcode.IPUT_OBJECT,
Opcode.IGET_OBJECT,
)
}

View File

@@ -9,7 +9,7 @@ import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable
import app.revanced.patcher.util.smali.toInstructions
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint
import app.revanced.patches.youtube.shared.videoQualityChangedFingerprint
import app.revanced.patches.youtube.video.playerresponse.Hook
import app.revanced.patches.youtube.video.playerresponse.addPlayerResponseMethodHook
import app.revanced.patches.youtube.video.playerresponse.playerResponseMethodHookPatch
@@ -38,7 +38,9 @@ import com.android.tools.smali.dexlib2.util.MethodUtil
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/VideoInformation;"
private const val EXTENSION_PLAYER_INTERFACE =
"Lapp/revanced/extension/youtube/patches/VideoInformation${'$'}PlaybackController;"
"Lapp/revanced/extension/youtube/patches/VideoInformation\$PlaybackController;"
private const val EXTENSION_VIDEO_QUALITY_MENU_INTERFACE =
"Lapp/revanced/extension/youtube/patches/VideoInformation\$VideoQualityMenuInterface;"
private lateinit var playerInitMethod: MutableMethod
private var playerInitInsertIndex = -1
@@ -83,7 +85,6 @@ val videoInformationPatch = bytecodePatch(
)
execute {
playerInitMethod = playerInitFingerprint.classDef.methods.first { MethodUtil.isConstructor(it) }
// Find the location of the first invoke-direct call and extract the register storing the 'this' object reference.
@@ -93,9 +94,6 @@ val videoInformationPatch = bytecodePatch(
playerInitInsertRegister = playerInitMethod.getInstruction<FiveRegisterInstruction>(initThisIndex).registerC
playerInitInsertIndex = initThisIndex + 1
// Hook the player controller for use through the extension.
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "initialize")
val seekFingerprintResultMethod = seekFingerprint.match(playerInitFingerprint.originalClassDef).method
val seekRelativeFingerprintResultMethod =
seekRelativeFingerprint.match(playerInitFingerprint.originalClassDef).method
@@ -263,7 +261,7 @@ val videoInformationPatch = bytecodePatch(
// Handle new playback speed menu.
playbackSpeedMenuSpeedChangedFingerprint.match(
newVideoQualityChangedFingerprint.originalClassDef,
videoQualityChangedFingerprint.originalClassDef,
).method.apply {
val index = indexOfFirstInstructionOrThrow(Opcode.IGET)
@@ -272,6 +270,131 @@ val videoInformationPatch = bytecodePatch(
speedSelectionValueRegister = getInstruction<TwoRegisterInstruction>(index).registerA
}
videoQualityFingerprint.let {
// Fix bad data used by YouTube.
it.method.addInstructions(
0,
"""
invoke-static { p2, p1 }, $EXTENSION_CLASS_DESCRIPTOR->fixVideoQualityResolution(Ljava/lang/String;I)I
move-result p1
"""
)
// Add methods to access obfuscated quality fields.
it.classDef.apply {
methods.add(
ImmutableMethod(
type,
"patch_getQualityName",
listOf(),
"Ljava/lang/String;",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
// Only one string field.
val qualityNameField = fields.single { field ->
field.type == "Ljava/lang/String;"
}
addInstructions(
0,
"""
iget-object v0, p0, $qualityNameField
return-object v0
"""
)
}
)
methods.add(
ImmutableMethod(
type,
"patch_getResolution",
listOf(),
"I",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
val resolutionField = fields.single { field ->
field.type == "I"
}
addInstructions(
0,
"""
iget v0, p0, $resolutionField
return v0
"""
)
}
)
}
}
// Detect video quality changes and override the current quality.
setVideoQualityFingerprint.match(
videoQualitySetterFingerprint.originalClassDef
).let { match ->
// This instruction refers to the field with the type that contains the setQuality method.
val onItemClickListenerClassReference = match.method
.getInstruction<ReferenceInstruction>(0).reference
val setQualityFieldReference = match.method
.getInstruction<ReferenceInstruction>(1).reference as FieldReference
proxy(
classes.find { classDef ->
classDef.type == setQualityFieldReference.type
}!!
).mutableClass.apply {
// Add interface and helper methods to allow extension code to call obfuscated methods.
interfaces.add(EXTENSION_VIDEO_QUALITY_MENU_INTERFACE)
methods.add(
ImmutableMethod(
type,
"patch_setQuality",
listOf(
ImmutableMethodParameter(YOUTUBE_VIDEO_QUALITY_CLASS_TYPE, null, null)
),
"V",
AccessFlags.PUBLIC.value or AccessFlags.FINAL.value,
null,
null,
MutableMethodImplementation(2),
).toMutable().apply {
val setQualityMenuIndexMethod = methods.single { method ->
method.parameterTypes.firstOrNull() == YOUTUBE_VIDEO_QUALITY_CLASS_TYPE
}
addInstructions(
0,
"""
invoke-virtual { p0, p1 }, $setQualityMenuIndexMethod
return-void
"""
)
}
)
}
videoQualitySetterFingerprint.method.addInstructions(
0,
"""
# Get object instance to invoke setQuality method.
iget-object v0, p0, $onItemClickListenerClassReference
iget-object v0, v0, $setQualityFieldReference
invoke-static { p1, v0, p2 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([$YOUTUBE_VIDEO_QUALITY_CLASS_TYPE${EXTENSION_VIDEO_QUALITY_MENU_INTERFACE}I)I
move-result p2
"""
)
}
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "initialize")
videoSpeedChangedHook(EXTENSION_CLASS_DESCRIPTOR, "videoSpeedChanged")
userSelectedPlaybackSpeedHook(EXTENSION_CLASS_DESCRIPTOR, "userSelectedPlaybackSpeed")
}
@@ -282,8 +405,8 @@ private fun addSeekInterfaceMethods(targetClass: MutableClass, seekToMethod: Met
targetClass.interfaces.add(EXTENSION_PLAYER_INTERFACE)
arrayOf(
Triple(seekToMethod, "seekTo", true),
Triple(seekToRelativeMethod, "seekToRelative", false),
Triple(seekToMethod, "patch_seekTo", true),
Triple(seekToRelativeMethod, "patch_seekToRelative", false),
).forEach { (method, name, returnsBoolean) ->
// Add interface method.
// Get enum type for the seek helper method.

View File

@@ -68,7 +68,6 @@ internal val advancedVideoQualityMenuPatch = bytecodePatch {
// region Patch for the old type of the video quality menu.
// Used for regular videos when spoofing to old app version,
// and for the Shorts quality flyout on newer app versions.
videoQualityMenuViewInflateFingerprint.let {
it.method.apply {
val checkCastIndex = it.patternMatch!!.endIndex
@@ -77,7 +76,7 @@ internal val advancedVideoQualityMenuPatch = bytecodePatch {
addInstruction(
checkCastIndex + 1,
"invoke-static { v$listViewRegister }, $EXTENSION_CLASS_DESCRIPTOR->" +
"showAdvancedVideoQualityMenu(Landroid/widget/ListView;)V",
"addVideoQualityListMenuListener(Landroid/widget/ListView;)V",
)
}
}

View File

@@ -5,36 +5,25 @@ import app.revanced.util.literal
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
/**
* Matches with the class found in [videoQualitySetterFingerprint].
*/
internal val setQualityByIndexMethodClassFieldReferenceFingerprint = fingerprint {
returns("V")
parameters("L")
opcodes(
Opcode.IGET_OBJECT,
Opcode.IPUT_OBJECT,
Opcode.IGET_OBJECT,
)
}
internal val videoQualityItemOnClickParentFingerprint = fingerprint {
returns("V")
strings("VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT")
}
internal val videoQualitySetterFingerprint = fingerprint {
accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL)
/**
* Resolves to class found in [videoQualityItemOnClickFingerprint].
*/
internal val videoQualityItemOnClickFingerprint = fingerprint {
returns("V")
parameters("[L", "I", "Z")
opcodes(
Opcode.IF_EQZ,
Opcode.INVOKE_VIRTUAL,
Opcode.MOVE_RESULT_OBJECT,
Opcode.INVOKE_VIRTUAL,
Opcode.IPUT_BOOLEAN,
parameters(
"Landroid/widget/AdapterView;",
"Landroid/view/View;",
"I",
"J"
)
strings("menu_item_video_quality")
custom { method, _ ->
method.name == "onItemClick"
}
}

View File

@@ -1,9 +1,7 @@
package app.revanced.patches.youtube.video.quality
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
@@ -12,12 +10,10 @@ import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch
import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint
import app.revanced.patches.youtube.shared.videoQualityChangedFingerprint
import app.revanced.patches.youtube.video.information.onCreateHook
import app.revanced.patches.youtube.video.information.videoInformationPatch
import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch;"
@@ -61,81 +57,27 @@ val rememberVideoQualityPatch = bytecodePatch {
SwitchPreference("revanced_remember_video_quality_last_selected_toast")
))
/*
* The following code works by hooking the method which is called when the user selects a video quality
* to remember the last selected video quality.
*
* It also hooks the method which is called when the video quality to set is determined.
* Conveniently, at this point the video quality is overridden to the remembered playback speed.
*/
onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "newVideoStarted")
// Inject a call to set the remembered quality once a video loads.
setQualityByIndexMethodClassFieldReferenceFingerprint.match(
videoQualitySetterFingerprint.originalClassDef,
).let { match ->
// This instruction refers to the field with the type that contains the setQualityByIndex method.
val instructions = match.method.implementation!!.instructions
// Inject a call to remember the selected quality for Shorts.
videoQualityItemOnClickFingerprint.match(
videoQualityItemOnClickParentFingerprint.classDef
).method.addInstruction(
0,
"invoke-static { p3 }, $EXTENSION_CLASS_DESCRIPTOR->userChangedShortsQuality(I)V"
)
val getOnItemClickListenerClassReference =
(instructions.elementAt(0) as ReferenceInstruction).reference
val getSetQualityByIndexMethodClassFieldReference =
(instructions.elementAt(1) as ReferenceInstruction).reference
val setQualityByIndexMethodClassFieldReference =
getSetQualityByIndexMethodClassFieldReference as FieldReference
val setQualityByIndexMethodClass = classes
.find { classDef -> classDef.type == setQualityByIndexMethodClassFieldReference.type }!!
// Get the name of the setQualityByIndex method.
val setQualityByIndexMethod = setQualityByIndexMethodClass.methods
.find { method -> method.parameterTypes.first() == "I" }
?: throw PatchException("Could not find setQualityByIndex method")
videoQualitySetterFingerprint.method.addInstructions(
0,
"""
# Get the object instance to invoke the setQualityByIndex method on.
iget-object v0, p0, $getOnItemClickListenerClassReference
iget-object v0, v0, $getSetQualityByIndexMethodClassFieldReference
# Get the method name.
const-string v1, "${setQualityByIndexMethod.name}"
# Set the quality.
# The first parameter is the array list of video qualities.
# The second parameter is the index of the selected quality.
# The register v0 stores the object instance to invoke the setQualityByIndex method on.
# The register v1 stores the name of the setQualityByIndex method.
invoke-static { p1, p2, v0, v1 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([Ljava/lang/Object;ILjava/lang/Object;Ljava/lang/String;)I
move-result p2
""",
)
}
// Inject a call to remember the selected quality.
videoQualityItemOnClickParentFingerprint.classDef.methods.find { it.name == "onItemClick" }
?.apply {
val listItemIndexParameter = 3
// Inject a call to remember the user selected quality for regular videos.
videoQualityChangedFingerprint.let {
it.method.apply {
val index = it.patternMatch!!.startIndex
val register = getInstruction<TwoRegisterInstruction>(index).registerA
addInstruction(
0,
"invoke-static { p$listItemIndexParameter }, " +
"$EXTENSION_CLASS_DESCRIPTOR->userChangedQuality(I)V",
index + 1,
"invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->userChangedQuality(I)V",
)
} ?: throw PatchException("Failed to find onItemClick method")
// Remember video quality if not using old layout menu.
newVideoQualityChangedFingerprint.method.apply {
val index = newVideoQualityChangedFingerprint.patternMatch!!.startIndex
val qualityRegister = getInstruction<TwoRegisterInstruction>(index).registerA
addInstruction(
index + 1,
"invoke-static { v$qualityRegister }, " +
"$EXTENSION_CLASS_DESCRIPTOR->userChangedQualityInNewFlyout(I)V",
)
}
}
}
}

View File

@@ -0,0 +1,56 @@
package app.revanced.patches.youtube.video.quality
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.misc.settings.preference.SwitchPreference
import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playercontrols.*
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.util.ResourceGroup
import app.revanced.util.copyResources
private val videoQualityButtonResourcePatch = resourcePatch {
dependsOn(playerControlsResourcePatch)
execute {
copyResources(
"qualitybutton",
ResourceGroup(
"drawable",
"revanced_video_quality_dialog_button_rectangle.xml",
),
)
addBottomControl("qualitybutton")
}
}
private const val QUALITY_BUTTON_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/videoplayer/VideoQualityDialogButton;"
val videoQualityDialogButtonPatch = bytecodePatch(
description = "Adds the option to display video quality dialog button in the video player.",
) {
dependsOn(
sharedExtensionPatch,
settingsPatch,
addResourcesPatch,
rememberVideoQualityPatch,
videoQualityButtonResourcePatch,
playerControlsPatch,
)
execute {
addResources("youtube", "video.quality.button.videoQualityDialogButtonPatch")
PreferenceScreen.PLAYER.addPreferences(
SwitchPreference("revanced_video_quality_dialog_button"),
)
initializeBottomControl(QUALITY_BUTTON_CLASS_DESCRIPTOR)
injectVisibilityCheckCall(QUALITY_BUTTON_CLASS_DESCRIPTOR)
}
}

View File

@@ -19,6 +19,7 @@ val videoQualityPatch = bytecodePatch(
dependsOn(
rememberVideoQualityPatch,
advancedVideoQualityMenuPatch,
videoQualityDialogButtonPatch,
)
compatibleWith(

View File

@@ -9,6 +9,9 @@ import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch
import app.revanced.patches.youtube.misc.playercontrols.*
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.patches.youtube.misc.settings.settingsPatch
import app.revanced.patches.youtube.video.information.userSelectedPlaybackSpeedHook
import app.revanced.patches.youtube.video.information.videoInformationPatch
import app.revanced.patches.youtube.video.information.videoSpeedChangedHook
import app.revanced.patches.youtube.video.speed.custom.customPlaybackSpeedPatch
import app.revanced.util.ResourceGroup
import app.revanced.util.copyResources
@@ -21,8 +24,8 @@ private val playbackSpeedButtonResourcePatch = resourcePatch {
"speedbutton",
ResourceGroup(
"drawable",
"revanced_playback_speed_dialog_button.xml",
),
"revanced_playback_speed_dialog_button_rectangle.xml"
)
)
addBottomControl("speedbutton")
@@ -42,6 +45,7 @@ val playbackSpeedButtonPatch = bytecodePatch(
customPlaybackSpeedPatch,
playbackSpeedButtonResourcePatch,
playerControlsPatch,
videoInformationPatch,
)
execute {
@@ -53,5 +57,8 @@ val playbackSpeedButtonPatch = bytecodePatch(
initializeBottomControl(SPEED_BUTTON_CLASS_DESCRIPTOR)
injectVisibilityCheckCall(SPEED_BUTTON_CLASS_DESCRIPTOR)
videoSpeedChangedHook(SPEED_BUTTON_CLASS_DESCRIPTOR, "videoSpeedChanged")
userSelectedPlaybackSpeedHook(SPEED_BUTTON_CLASS_DESCRIPTOR, "videoSpeedChanged")
}
}

View File

@@ -32,7 +32,10 @@ import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstructio
import com.android.tools.smali.dexlib2.iface.instruction.ThreeRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import com.android.tools.smali.dexlib2.iface.reference.Reference
import com.android.tools.smali.dexlib2.iface.reference.StringReference
import com.android.tools.smali.dexlib2.immutable.ImmutableField
import com.android.tools.smali.dexlib2.util.MethodUtil
import java.util.EnumSet
@@ -171,6 +174,79 @@ internal val Instruction.isBranchInstruction: Boolean
internal val Instruction.isReturnInstruction: Boolean
get() = this.opcode in returnOpcodes
/**
* Find the instruction index used for a toString() StringBuilder write of a given String name.
*
* @param fieldName The name of the field to find. Partial matches are allowed.
*/
private fun Method.findInstructionIndexFromToString(fieldName: String) : Int {
val stringIndex = indexOfFirstInstruction {
val reference = getReference<StringReference>()
reference?.string?.contains(fieldName) == true
}
if (stringIndex < 0) {
throw IllegalArgumentException("Could not find usage of string: '$fieldName'")
}
val stringRegister = getInstruction<OneRegisterInstruction>(stringIndex).registerA
// Find use of the string with a StringBuilder.
val stringUsageIndex = indexOfFirstInstruction(stringIndex) {
val reference = getReference<MethodReference>()
reference?.definingClass == "Ljava/lang/StringBuilder;" &&
(this as? FiveRegisterInstruction)?.registerD == stringRegister
}
if (stringUsageIndex < 0) {
throw IllegalArgumentException("Could not find StringBuilder usage in: $this")
}
// Find the next usage of StringBuilder, which should be the desired field.
val fieldUsageIndex = indexOfFirstInstruction(stringUsageIndex + 1) {
val reference = getReference<MethodReference>()
reference?.definingClass == "Ljava/lang/StringBuilder;" && reference.name == "append"
}
if (fieldUsageIndex < 0) {
// Should never happen.
throw IllegalArgumentException("Could not find StringBuilder append usage in: $this")
}
val fieldUsageRegister = getInstruction<FiveRegisterInstruction>(fieldUsageIndex).registerD
// Look backwards up the method to find the instruction that sets the register.
var fieldSetIndex = indexOfFirstInstructionReversedOrThrow(fieldUsageIndex - 1) {
fieldUsageRegister == writeRegister
}
// If the field is a method call, then adjust from MOVE_RESULT to the method call.
val fieldSetOpcode = getInstruction(fieldSetIndex).opcode
if (fieldSetOpcode == MOVE_RESULT ||
fieldSetOpcode == MOVE_RESULT_WIDE ||
fieldSetOpcode == MOVE_RESULT_OBJECT) {
fieldSetIndex--
}
return fieldSetIndex
}
/**
* Find the method used for a toString() StringBuilder write of a given String name.
*
* @param fieldName The name of the field to find. Partial matches are allowed.
*/
context(BytecodePatchContext)
internal fun Method.findMethodFromToString(fieldName: String) : MutableMethod {
val methodUsageIndex = findInstructionIndexFromToString(fieldName)
return navigate(this).to(methodUsageIndex).stop()
}
/**
* Find the field used for a toString() StringBuilder write of a given String name.
*
* @param fieldName The name of the field to find. Partial matches are allowed.
*/
internal fun Method.findFieldFromToString(fieldName: String) : FieldReference {
val methodUsageIndex = findInstructionIndexFromToString(fieldName)
return getInstruction<ReferenceInstruction>(methodUsageIndex).getReference<FieldReference>()!!
}
/**
* Adds public [AccessFlags] and removes private and protected flags (if present).
*/
@@ -594,7 +670,7 @@ fun Method.indexOfFirstInstructionReversed(targetOpcode: Opcode): Int = indexOfF
/**
* Get the index of matching instruction,
* starting from and [startIndex] and searching down.
* starting from [startIndex] and searching down.
*
* @param startIndex Optional starting index to search down from. Searching includes the start index.
* @return The index of the instruction.
@@ -617,7 +693,7 @@ fun Method.indexOfFirstInstructionReversedOrThrow(targetOpcode: Opcode): Int = i
/**
* Get the index of matching instruction,
* starting from and [startIndex] and searching down.
* starting from [startIndex] and searching down.
*
* @param startIndex Optional starting index to search down from. Searching includes the start index.
* @return The index of the instruction.

View File

@@ -222,6 +222,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -222,6 +222,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -1465,6 +1465,11 @@ Second \"item\" text"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">الزر معروض. انقر مع الاستمرار لإعادة ضبط سرعة التشغيل إلى الوضع الافتراضي</string>
<string name="revanced_playback_speed_dialog_button_summary_off">لا يتم عرض الزر</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">عرض زر جودة الفيديو</string>
<string name="revanced_video_quality_dialog_button_summary_on">الزر معروض. انقر مع الاستمرار لإعادة تعيين الجودة إلى الافتراضي</string>
<string name="revanced_video_quality_dialog_button_summary_off">الزر غير معروض</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">قائمة سرعة التشغيل المخصصة</string>
<string name="revanced_custom_speed_menu_summary_on">يتم عرض قائمة سرعة التشغيل المخصصة</string>

View File

@@ -224,6 +224,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -1464,10 +1464,18 @@ Bunu aktivləşdirmə daha yüksək video keyfiyyətləri əngəlin silə bilər
<string name="revanced_playback_speed_dialog_button_summary_on">Düymə göstərilir. Oynatma sürətin standart olaraq qaytarmaq üçün toxunub saxla</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Düymə göstərilmir</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Video keyfiyyəti düyməsini göstər</string>
<string name="revanced_video_quality_dialog_button_summary_on">Düymə görünür. Keyfiyyəti ilkin vəziyyətinə qaytarmaq üçün toxunub saxlayın</string>
<string name="revanced_video_quality_dialog_button_summary_off">Düymə görünmür</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Fərdi oynatma sürəti siyahısı</string>
<string name="revanced_custom_speed_menu_summary_on">Fərdi sürət siyahısı göstərilir</string>
<string name="revanced_custom_speed_menu_summary_off">Fərdi sürət siyahısı göstərilmir</string>
<string name="revanced_restore_old_speed_menu_title">Köhnə oynatma sürəti menyusunu bərpa et</string>
<string name="revanced_restore_old_speed_menu_summary_on">Köhnə sürət menyusu göstərilir</string>
<string name="revanced_restore_old_speed_menu_summary_off">Müasir sürət menyusu göstərilir</string>
<string name="revanced_custom_playback_speeds_title">Fərdi oynatma sürəti</string>
<string name="revanced_custom_playback_speeds_summary">Fərdi oynatma sürətlərini əlavə et və ya dəyiş</string>
<string name="revanced_custom_playback_speeds_invalid">Fərdi sürətlər %s dəyərindən az olmalıdır</string>

View File

@@ -1466,6 +1466,11 @@ Second \"item\" text"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Кнопка паказана. Націсніце і ўтрымлівайце, каб скінуць хуткасць прайгравання да стандартнай</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Кнопка не паказваецца</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Паказаць кнопку якасці відэа</string>
<string name="revanced_video_quality_dialog_button_summary_on">Кнопка паказана. Націсніце і ўтрымлівайце, каб скінуць якасць да па змаўчанні</string>
<string name="revanced_video_quality_dialog_button_summary_off">Кнопка не паказваецца</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Меню пользовательской скорости воспроизведения</string>
<string name="revanced_custom_speed_menu_summary_on">Меню пользовательской скорости отображается</string>

View File

@@ -1465,6 +1465,11 @@ Second \"item\" text"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Бутонът е показан. Докоснете и задръжте, за да върнете скоростта на възпроизвеждане към стойността по подразбиране</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Бутонът не е показан</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Покажи бутона за качество на видеото</string>
<string name="revanced_video_quality_dialog_button_summary_on">Бутонът е показан. Докоснете и задръжте, за да възстановите качеството до подразбиране</string>
<string name="revanced_video_quality_dialog_button_summary_off">Бутонът не е показан</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Менюто за потребителска скорост</string>
<string name="revanced_custom_speed_menu_summary_on">Менюто за потребителска скорост се показва</string>

View File

@@ -1461,6 +1461,11 @@ DeArrow সম্পর্কে আরও জানতে এখানে ট
<string name="revanced_playback_speed_dialog_button_summary_on">বোতামটি দেখানো হয়েছে। প্লেব্যাক স্পীড ডিফল্টে রিসেট করতে ট্যাপ করে ধরে রাখুন।</string>
<string name="revanced_playback_speed_dialog_button_summary_off">বোতাম প্রদর্শিত হয়নি</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">ভিডিও গুণমান বোতাম দেখান</string>
<string name="revanced_video_quality_dialog_button_summary_on">বোতামটি দেখানো হয়েছে। গুণমান ডিফল্টে রিসেট করতে ট্যাপ করে ধরে রাখুন।</string>
<string name="revanced_video_quality_dialog_button_summary_off">বোতামটি দেখানো হয়নি।</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">কাস্টম প্লেব্যাক গতি মেনু</string>
<string name="revanced_custom_speed_menu_summary_on">কাস্টম স্পিড মেনু দেখানো হচ্ছে</string>

View File

@@ -222,6 +222,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -222,6 +222,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -1465,6 +1465,11 @@ Povolením této funkce lze odemknout vyšší kvality videa"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Tlačítko je zobrazeno. Klepnutím a podržením obnovíte výchozí rychlost přehrávání</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Tlačítko se nezobrazuje</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Zobrazit tlačítko kvality videa</string>
<string name="revanced_video_quality_dialog_button_summary_on">Tlačítko je zobrazeno. Klepnutím a podržením obnovíte kvalitu na výchozí</string>
<string name="revanced_video_quality_dialog_button_summary_off">Tlačítko není zobrazeno.</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Menu vlastní rychlosti přehrávání</string>
<string name="revanced_custom_speed_menu_summary_on">Menu vlastní rychlosti se zobrazuje</string>

View File

@@ -1467,6 +1467,11 @@ Aktivering af dette kan låse op for højere videokvalitet"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Knappen vises. Tryk og hold for at nulstille afspilningshastigheden til standard.</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Knap vises ikke</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Vis videokvalitetsknap</string>
<string name="revanced_video_quality_dialog_button_summary_on">Knap vises. Tryk og hold nede for at nulstille kvaliteten til standard</string>
<string name="revanced_video_quality_dialog_button_summary_off">Knappen vises ikke</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Tilpasset afspilningshastighed menu</string>
<string name="revanced_custom_speed_menu_summary_on">Tilpasset hastighed menu er vist</string>

View File

@@ -1460,6 +1460,11 @@ Durch Aktivieren dieser Option können höhere Videoqualitäten freigeschaltet w
<string name="revanced_playback_speed_dialog_button_summary_on">Die Schaltfläche wird angezeigt. Tippen und halten, um die Wiedergabegeschwindigkeit auf die Standardeinstellung zurückzusetzen.</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Button wird nicht angezeigt</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Videoqualität-Schaltfläche anzeigen</string>
<string name="revanced_video_quality_dialog_button_summary_on">Schaltfläche wird angezeigt. Tippen und halten, um die Qualität auf Standard zurückzusetzen</string>
<string name="revanced_video_quality_dialog_button_summary_off">Schaltfläche wird nicht angezeigt</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Benutzerdefiniertes Wiedergabegeschwindigkeitsmenü</string>
<string name="revanced_custom_speed_menu_summary_on">Benutzerdefiniertes Geschwindigkeitsmenü wird angezeigt</string>

View File

@@ -1464,6 +1464,11 @@ Second \"item\" text"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Το κουμπί εμφανίζεται. Πατήστε παρατεταμένα για επαναφορά της ταχύτητας αναπαραγωγής στην προεπιλογή</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Το κουμπί δεν εμφανίζεται</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Εμφάνιση κουμπιού αλλαγής ποιότητας βίντεο</string>
<string name="revanced_video_quality_dialog_button_summary_on">Το κουμπί εμφανίζεται. Πατήστε παρατεταμένα για επαναφορά της ποιότητας στην προεπιλογή</string>
<string name="revanced_video_quality_dialog_button_summary_off">Το κουμπί δεν εμφανίζεται</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Μενού προσαρμοσμένης ταχύτητας αναπαραγωγής</string>
<string name="revanced_custom_speed_menu_summary_on">Το μενού προσαρμοσμένης ταχύτητας αναπαραγωγής εμφανίζεται</string>

View File

@@ -1456,6 +1456,11 @@ Habilitar esto puede desbloquear calidades de vídeo más altas"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Se muestra el botón. Mantén pulsado para restablecer la velocidad de reproducción predeterminada</string>
<string name="revanced_playback_speed_dialog_button_summary_off">El botón no se muestra</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Mostrar botón de calidad de video</string>
<string name="revanced_video_quality_dialog_button_summary_on">Botón visible. Toca y mantén para restablecer la calidad a los valores predeterminados</string>
<string name="revanced_video_quality_dialog_button_summary_off">Botón no visible</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Menú de velocidad de reproducción personalizada</string>
<string name="revanced_custom_speed_menu_summary_on">Menú de velocidad personalizado se muestra</string>

View File

@@ -1465,6 +1465,11 @@ Selle lubamine võib avada kõrgema video kvaliteedi"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Nupp on nähtaval. Puudutage ja hoidke all, et taastada taasesituse kiirus vaikeväärtusele</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Nuppi ei kuvata</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Näita video kvaliteedi nuppu</string>
<string name="revanced_video_quality_dialog_button_summary_on">Nupp on nähtaval. Kvaliteedi lähtestamiseks vaikeseadeteks puudutage ja hoidke</string>
<string name="revanced_video_quality_dialog_button_summary_off">Nuppu ei kuvata</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Kohandatud taasesituse kiiruse menüü</string>
<string name="revanced_custom_speed_menu_summary_on">Kohandatud kiiruse menüü kuvatakse</string>

View File

@@ -222,6 +222,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -272,6 +272,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -1465,10 +1465,18 @@ Tämä voi avata korkealaatuisemmat videot"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Painike näytetään. Napauta ja pidä pohjassa palauttaaksesi soiton nopeuden oletukseksi</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Painiketta ei näytetä</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Näytä videolaatupainike</string>
<string name="revanced_video_quality_dialog_button_summary_on">Painike näkyy. Paina pitkään palauttaaksesi laadun oletukseksi</string>
<string name="revanced_video_quality_dialog_button_summary_off">Painiketta ei näytetä</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Mukautettu toistonopeusvalikko</string>
<string name="revanced_custom_speed_menu_summary_on">Mukautettu nopeusvalikko näytetään</string>
<string name="revanced_custom_speed_menu_summary_off">Mukautettua nopeusvalikkoa ei näytetä</string>
<string name="revanced_restore_old_speed_menu_title">Palauta vanha toistonopeusvalikko</string>
<string name="revanced_restore_old_speed_menu_summary_on">Vanha nopeusvalikko näytetään</string>
<string name="revanced_restore_old_speed_menu_summary_off">Moderni nopeusvalikko näytetään</string>
<string name="revanced_custom_playback_speeds_title">Mukautetut toistonopeudet</string>
<string name="revanced_custom_playback_speeds_summary">Lisää tai muuta mukautettuja toistonopeuksia</string>
<string name="revanced_custom_playback_speeds_invalid">Mukautettujen nopeuksien tulee olla alle %s</string>

View File

@@ -1463,6 +1463,11 @@ Ang pagpapagana nito ay maaaring magbukas ng mas mataas na kalidad ng video"</st
<string name="revanced_playback_speed_dialog_button_summary_on">Ipinapakita ang button. I-tap at i-hold para i-reset ang bilis ng pag-playback sa default</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Hindi ipinapakita ang button</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Ipakita ang button ng kalidad ng video</string>
<string name="revanced_video_quality_dialog_button_summary_on">Nakalabas ang button. Pindutin nang matagal para ibalik sa default ang kalidad</string>
<string name="revanced_video_quality_dialog_button_summary_off">Hindi ipinapakita ang buton</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Menu ng pasadyang bilis ng pag-playback</string>
<string name="revanced_custom_speed_menu_summary_on">Ipinapakita ang menu ng pasadyang bilis</string>

View File

@@ -1466,6 +1466,11 @@ Activer cette option peut déverrouiller des qualités vidéo supérieures"</str
<string name="revanced_playback_speed_dialog_button_summary_on">Le bouton est affiché. Appuyez longuement dessus pour rétablir la vitesse de lecture par défaut.</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Le bouton n\'est pas affiché</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Afficher le bouton de qualité vidéo</string>
<string name="revanced_video_quality_dialog_button_summary_on">Le bouton est affiché. Appuyez longuement pour rétablir la qualité par défaut.</string>
<string name="revanced_video_quality_dialog_button_summary_off">Le bouton n\'est pas affiché</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Menu de vitesse de lecture personnalisée</string>
<string name="revanced_custom_speed_menu_summary_on">Le menu de vitesse personnalisée est affiché</string>

View File

@@ -1465,6 +1465,11 @@ Is féidir le seo caighdeáin físeáin níos airde a dhíghlasáil"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Taispeántar an cnaipe. Tapáil agus coinnigh chun luas athsheinm a athshocrú go réamhshocrú</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Ní thaispeántar an cnaipe</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Taispeáin cnaipe cáilíochta físeáin</string>
<string name="revanced_video_quality_dialog_button_summary_on">Tá cnaipe le feiceáil. Tapáil agus coinnigh chun cáilíocht a athshocrú mar réamhshocrú</string>
<string name="revanced_video_quality_dialog_button_summary_off">Níl cnaipe le feiceáil</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Roghchlár luas athsheinm saincheaptha</string>
<string name="revanced_custom_speed_menu_summary_on">Taispeántar roghchlár luais saincheaptha</string>

View File

@@ -222,6 +222,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -222,6 +222,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -222,6 +222,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -222,6 +222,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -1462,6 +1462,11 @@ Ez a beállítás lehetővé teszi a magasabb videóminőségek feloldását"</s
<string name="revanced_playback_speed_dialog_button_summary_on">A gomb látható. Tartsa lenyomva a lejátszási sebesség alapértelmezettre állításához</string>
<string name="revanced_playback_speed_dialog_button_summary_off">A gomb nem látható</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Videóminőség gomb megjelenítése</string>
<string name="revanced_video_quality_dialog_button_summary_on">A gomb látható. Tartsa lenyomva a minőséget az alapértelmezettre visszaállításához</string>
<string name="revanced_video_quality_dialog_button_summary_off">A gomb nem látható</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Egyedi lejátszási sebesség menü</string>
<string name="revanced_custom_speed_menu_summary_on">Megjelenik az egyéni sebesség menü</string>

View File

@@ -1466,6 +1466,11 @@ Mini-player-ը կարող է գրավվել էկրանից դուրս՝ դեպի
<string name="revanced_playback_speed_dialog_button_summary_on">Կոճակը ցուցադրվում է: Հպեք և պահեք՝ նվագարկման արագությունը լռելյայնի վերականգնելու համար</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Կոճակը չի ցուցադրվում</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Ցուցադրել տեսանյութի որակի կոճակը</string>
<string name="revanced_video_quality_dialog_button_summary_on">Կոճակը ցուցադրված է։ Հպեք և պահեք՝ որակը լռելյայնին վերականգնելու համար</string>
<string name="revanced_video_quality_dialog_button_summary_off">Կոճակը ցուցադրված չէ։</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Տեսանյութի արագության հարմարեցված մենյու</string>
<string name="revanced_custom_speed_menu_summary_on">Հարմարեցված արագության մենյուը ցուցադրվում է</string>

View File

@@ -1464,6 +1464,11 @@ Mengaktifkan ini dapat membuka kualitas video yang lebih tinggi"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Tombol ditampilkan. Ketuk dan tahan untuk mengatur ulang kecepatan pemutaran ke bawaan</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Tombol tidak ditampilkan</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Tampilkan tombol kualitas video</string>
<string name="revanced_video_quality_dialog_button_summary_on">Tombol ditampilkan. Ketuk dan tahan untuk mengatur ulang kualitas ke bawaan</string>
<string name="revanced_video_quality_dialog_button_summary_off">Tombol tidak ditampilkan</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Menu kecepatan pemutaran khusus</string>
<string name="revanced_custom_speed_menu_summary_on">Menu kecepatan khusus ditampilkan</string>

View File

@@ -222,6 +222,8 @@ Second \"item\" text"</string>
</patch>
<patch id="video.speed.button.playbackSpeedButtonPatch">
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
</patch>
<patch id="video.speed.remember.rememberPlaybackSpeedPatch">

View File

@@ -1464,6 +1464,11 @@ Abilitare questa opzione può sbloccare qualità video più elevate"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">Il pulsante è visualizzato. Tieni premuto per ripristinare la velocità di riproduzione predefinita</string>
<string name="revanced_playback_speed_dialog_button_summary_off">Il pulsante non è visibile</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">Mostra il pulsante qualità video</string>
<string name="revanced_video_quality_dialog_button_summary_on">Il pulsante è visualizzato. Tocca e tieni premuto per ripristinare la qualità predefinita</string>
<string name="revanced_video_quality_dialog_button_summary_off">Il pulsante non è visibile.</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">Menu di velocità di riproduzione personalizzato</string>
<string name="revanced_custom_speed_menu_summary_on">Il menu di velocità personalizzato è visibile</string>

View File

@@ -1467,6 +1467,11 @@ Second \"item\" text"</string>
<string name="revanced_playback_speed_dialog_button_summary_on">הלחצן מוצג. יש להקיש ולהחזיק כדי לאפס את מהירות ההפעלה לברירת מחדל</string>
<string name="revanced_playback_speed_dialog_button_summary_off">הלחצן אינו מוצג</string>
</patch>
<patch id="video.quality.button.videoQualityButtonPatch">
<string name="revanced_video_quality_dialog_button_title">הצג כפתור איכות וידאו</string>
<string name="revanced_video_quality_dialog_button_summary_on">הלחצן מוצג. גע והחזק כדי לאפס את האיכות לברירת מחדל</string>
<string name="revanced_video_quality_dialog_button_summary_off">הלחצן אינו מוצג</string>
</patch>
<patch id="video.speed.custom.customPlaybackSpeedPatch">
<string name="revanced_custom_speed_menu_title">תפריט מהירות הפעלה מותאם אישית</string>
<string name="revanced_custom_speed_menu_summary_on">תפריט מהירות מותאם אישית מוצג</string>

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