mirror of
https://github.com/revanced/revanced-patches.git
synced 2025-12-07 18:03:55 +01:00
Compare commits
95 Commits
v5.23.0-de
...
v5.26.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18954a0285 | ||
|
|
ce5385b28e | ||
|
|
3f4cdf6f83 | ||
|
|
094b4a1ea8 | ||
|
|
a320e35c32 | ||
|
|
5bf5a2d2db | ||
|
|
ff903ba9ac | ||
|
|
1079a54dbe | ||
|
|
2b0e3b4553 | ||
|
|
0265a7791b | ||
|
|
49ae0df224 | ||
|
|
e279491724 | ||
|
|
495260fe2b | ||
|
|
40f069fff7 | ||
|
|
de263c1061 | ||
|
|
bf1f26d8bb | ||
|
|
0ee2ed72d4 | ||
|
|
02373b0bd2 | ||
|
|
97c8e2489d | ||
|
|
08b2b2e104 | ||
|
|
6b386b67d2 | ||
|
|
f8343ae9f6 | ||
|
|
3ba791ac7d | ||
|
|
443b54bf09 | ||
|
|
53587f190d | ||
|
|
83c148addc | ||
|
|
5c8ed05727 | ||
|
|
33833d7a1e | ||
|
|
b712f38017 | ||
|
|
517368eda7 | ||
|
|
2093c0c175 | ||
|
|
a7cfd80bfe | ||
|
|
2990dc6d4e | ||
|
|
c0e52bb6b3 | ||
|
|
93fdd6f538 | ||
|
|
decd249f20 | ||
|
|
d79cb3eea8 | ||
|
|
584b00fd87 | ||
|
|
795016abce | ||
|
|
dc1dbd50a8 | ||
|
|
2984d7362d | ||
|
|
627aed4010 | ||
|
|
4ab1f0cfa9 | ||
|
|
86e8e61ab2 | ||
|
|
e286dab74e | ||
|
|
712a82439f | ||
|
|
4449546c85 | ||
|
|
8d61ba90c3 | ||
|
|
689be79f71 | ||
|
|
b6047fa6b3 | ||
|
|
82bbd603ac | ||
|
|
bc0c3c452d | ||
|
|
fe864d8331 | ||
|
|
4f686935c3 | ||
|
|
798596fd83 | ||
|
|
38b37f182a | ||
|
|
52b9dc5c9f | ||
|
|
dea7108c45 | ||
|
|
24b4579cb9 | ||
|
|
0b52f3d192 | ||
|
|
18c374a81e | ||
|
|
092303e431 | ||
|
|
6bf5bf9d45 | ||
|
|
b2b09a2025 | ||
|
|
4a3a7f1674 | ||
|
|
e59c9e9b3c | ||
|
|
dfb552b01a | ||
|
|
94999c56b1 | ||
|
|
c4fd1f0146 | ||
|
|
4cd0ae9b92 | ||
|
|
9548d581c1 | ||
|
|
a2fe3af6be | ||
|
|
6ef6504d41 | ||
|
|
e58290839f | ||
|
|
e18260bd65 | ||
|
|
b2fcd5a846 | ||
|
|
e68cd70f66 | ||
|
|
14a8f4fb96 | ||
|
|
2593c004f4 | ||
|
|
db68c41d5e | ||
|
|
a4f9cb3cef | ||
|
|
9aec1999bb | ||
|
|
26ecbe646e | ||
|
|
46ba0d8a2e | ||
|
|
f454183646 | ||
|
|
d2b440d800 | ||
|
|
494c5f04a4 | ||
|
|
48d5fdf7e1 | ||
|
|
887c9f0d75 | ||
|
|
7de4c9d41d | ||
|
|
7d3b8d9c42 | ||
|
|
25e1a965d6 | ||
|
|
b29c01cee1 | ||
|
|
639850471b | ||
|
|
796c118fe1 |
6
.github/workflows/build_pull_request.yml
vendored
6
.github/workflows/build_pull_request.yml
vendored
@@ -19,11 +19,11 @@ jobs:
|
|||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
distribution: 'temurin'
|
||||||
java-version: "17"
|
java-version: '17'
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: burrunan/gradle-cache-action@v1
|
uses: burrunan/gradle-cache-action@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
|
|||||||
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@@ -13,24 +13,23 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
packages: write
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
attestations: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
# Make sure the release step uses its own credentials:
|
|
||||||
# https://github.com/cycjimmy/semantic-release-action#private-packages
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
with:
|
with:
|
||||||
distribution: "temurin"
|
distribution: 'temurin'
|
||||||
java-version: "17"
|
java-version: '17'
|
||||||
|
|
||||||
- name: Cache Gradle
|
- name: Cache Gradle
|
||||||
uses: burrunan/gradle-cache-action@v1
|
uses: burrunan/gradle-cache-action@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
@@ -40,7 +39,7 @@ jobs:
|
|||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: 'lts/*'
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -54,6 +53,14 @@ jobs:
|
|||||||
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
fingerprint: ${{ vars.GPG_FINGERPRINT }}
|
||||||
|
|
||||||
- name: Release
|
- name: Release
|
||||||
|
uses: cycjimmy/semantic-release-action@v4
|
||||||
|
id: release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: npm exec semantic-release
|
|
||||||
|
- name: Attest
|
||||||
|
if: steps.release.outputs.new_release_published == 'true'
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-name: 'ReVanced Patches ${{ steps.release.outputs.new_release_git_tag }}'
|
||||||
|
subject-path: patches/build/libs/patches-*.rvp
|
||||||
|
|||||||
10
.releaserc
10
.releaserc
@@ -22,7 +22,7 @@
|
|||||||
{
|
{
|
||||||
"assets": [
|
"assets": [
|
||||||
"CHANGELOG.md",
|
"CHANGELOG.md",
|
||||||
"gradle.properties",
|
"gradle.properties"
|
||||||
],
|
],
|
||||||
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
"message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
||||||
}
|
}
|
||||||
@@ -33,16 +33,16 @@
|
|||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
|
"path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)"
|
||||||
},
|
}
|
||||||
],
|
],
|
||||||
successComment: false
|
"successComment": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"@saithodev/semantic-release-backmerge",
|
"@saithodev/semantic-release-backmerge",
|
||||||
{
|
{
|
||||||
backmergeBranches: [{"from": "main", "to": "dev"}],
|
"backmergeBranches": [{"from": "main", "to": "dev"}],
|
||||||
clearWorkspace: true
|
"clearWorkspace": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|||||||
290
CHANGELOG.md
290
CHANGELOG.md
@@ -1,3 +1,293 @@
|
|||||||
|
# [5.26.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.3...v5.26.0-dev.4) (2025-06-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Spotify:** Add `Hide Create button` patch ([#5062](https://github.com/ReVanced/revanced-patches/issues/5062)) ([3201681](https://github.com/ReVanced/revanced-patches/commit/32016819d2adbdfdd5e028941d56feda36d20b00))
|
||||||
|
|
||||||
|
# [5.26.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.2...v5.26.0-dev.3) (2025-06-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Playback Speed:** Use modern custom speed dialog ([#5069](https://github.com/ReVanced/revanced-patches/issues/5069)) ([9a1e6ca](https://github.com/ReVanced/revanced-patches/commit/9a1e6ca178d9833ee2c681fb130b9290a4e89cd8))
|
||||||
|
|
||||||
|
# [5.26.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.26.0-dev.1...v5.26.0-dev.2) (2025-06-01)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube:** Support A/B Shorts layout for RYD and component hiding ([#5081](https://github.com/ReVanced/revanced-patches/issues/5081)) ([8ecacaa](https://github.com/ReVanced/revanced-patches/commit/8ecacaad27162d9380014a9a13ac9220b12257b2))
|
||||||
|
|
||||||
|
# [5.26.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.25.0...v5.26.0-dev.1) (2025-05-30)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Proton Mail:** Add `Remove free accounts limit` patch ([#4970](https://github.com/ReVanced/revanced-patches/issues/4970)) ([b0440ad](https://github.com/ReVanced/revanced-patches/commit/b0440ad6af0e190e516974ce896dcc54c8d2e122))
|
||||||
|
|
||||||
|
# [5.25.0](https://github.com/ReVanced/revanced-patches/compare/v5.24.0...v5.25.0) (2025-05-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Disable Pairip license check:** Change patch to default off ([74b6a94](https://github.com/ReVanced/revanced-patches/commit/74b6a94577ac3f73b04bd0cce98fb7011a6607fd))
|
||||||
|
* **Hide ADB status:** Resolve app crash on startup ([#5029](https://github.com/ReVanced/revanced-patches/issues/5029)) ([1abebd5](https://github.com/ReVanced/revanced-patches/commit/1abebd5f3b73250c6638d2d8a274b92ea8268924))
|
||||||
|
* **Messenger:** Remove outdated `Disable switching emoji to sticker` patch ([#5044](https://github.com/ReVanced/revanced-patches/issues/5044)) ([7b182ca](https://github.com/ReVanced/revanced-patches/commit/7b182cab825ee3a4a3ca528c744c9d2a351c7cf8))
|
||||||
|
* **Spotify Lite:** Remove obsolete `Enable on demand` patch ([#5046](https://github.com/ReVanced/revanced-patches/issues/5046)) ([4886d47](https://github.com/ReVanced/revanced-patches/commit/4886d47506c94b03c1f190ecc4947d3d91df6a47))
|
||||||
|
* **YouTube - GmsCore support:** Restore patch functionality from prior merge ([7686bbe](https://github.com/ReVanced/revanced-patches/commit/7686bbe975644e1e582fa52f166879da5694ed93))
|
||||||
|
* **YouTube - Hide ads:** Hide new type of general ad ([#5004](https://github.com/ReVanced/revanced-patches/issues/5004)) ([37e59d2](https://github.com/ReVanced/revanced-patches/commit/37e59d2771528c631dc13e73dac095fec95c6485))
|
||||||
|
* **YouTube - Open Shorts in regular player:** Do not exit app when pressing back button in regular player ([#5020](https://github.com/ReVanced/revanced-patches/issues/5020)) ([3384f8d](https://github.com/ReVanced/revanced-patches/commit/3384f8dd0ff2a345f2e387f4ed1570079a83ccb6))
|
||||||
|
* **YouTube:** Better handle incorrect duplicate translations ([20abac5](https://github.com/ReVanced/revanced-patches/commit/20abac52121fbecb65d87d0982f3380e1cf4e20e))
|
||||||
|
* **Yuka - Unlock premium:** Remove broken patch that is no longer supported ([#5018](https://github.com/ReVanced/revanced-patches/issues/5018)) ([fac6e59](https://github.com/ReVanced/revanced-patches/commit/fac6e59d281e21e57abdcfc899cd1aeb18e5c2b8))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add `Disable pairip license check` patch ([#4927](https://github.com/ReVanced/revanced-patches/issues/4927)) ([42d2c27](https://github.com/ReVanced/revanced-patches/commit/42d2c277982ef63e6ad42d85e46f13c3ab50243c))
|
||||||
|
* **Messenger:** Add `Remove Meta AI` patch ([#4945](https://github.com/ReVanced/revanced-patches/issues/4945)) ([012dff7](https://github.com/ReVanced/revanced-patches/commit/012dff7b6511b9e519ccac96f6713cf1a1b327b4))
|
||||||
|
* **Prime Video:** Add `Rename shared permissions` patch ([#5049](https://github.com/ReVanced/revanced-patches/issues/5049)) ([80f1fc6](https://github.com/ReVanced/revanced-patches/commit/80f1fc629e30e391bd5877f07dbdf4b6613bd1cf))
|
||||||
|
* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5))
|
||||||
|
* **Threads:** Hide Ads ([#5064](https://github.com/ReVanced/revanced-patches/issues/5064)) ([3c4cecb](https://github.com/ReVanced/revanced-patches/commit/3c4cecb966c2f99bfde99552686dda19ade5f67e))
|
||||||
|
* **YouTube - Enable debugging:** Add settings menu to share debug logs ([#5021](https://github.com/ReVanced/revanced-patches/issues/5021)) ([1ec4a88](https://github.com/ReVanced/revanced-patches/commit/1ec4a88464a2a2810c02cf072950b618d183779a))
|
||||||
|
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16))
|
||||||
|
* **YouTube - Swipe controls:** Add separate color settings for the brightness and volume bars ([#5043](https://github.com/ReVanced/revanced-patches/issues/5043)) ([80f50e8](https://github.com/ReVanced/revanced-patches/commit/80f50e8c50ca6a8366b7fd7b01459fb16fa1074a))
|
||||||
|
* **YouTube:** Add `Disable haptic feedback` patch ([#5033](https://github.com/ReVanced/revanced-patches/issues/5033)) ([bbe7974](https://github.com/ReVanced/revanced-patches/commit/bbe79744a513c96f9016476e8435f999e94c45d7))
|
||||||
|
|
||||||
|
# [5.25.0-dev.14](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.13...v5.25.0-dev.14) (2025-05-29)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Threads:** Hide Ads ([#5064](https://github.com/ReVanced/revanced-patches/issues/5064)) ([3c4cecb](https://github.com/ReVanced/revanced-patches/commit/3c4cecb966c2f99bfde99552686dda19ade5f67e))
|
||||||
|
|
||||||
|
# [5.25.0-dev.13](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.12...v5.25.0-dev.13) (2025-05-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Prime Video:** Add `Rename shared permissions` patch ([#5049](https://github.com/ReVanced/revanced-patches/issues/5049)) ([80f1fc6](https://github.com/ReVanced/revanced-patches/commit/80f1fc629e30e391bd5877f07dbdf4b6613bd1cf))
|
||||||
|
|
||||||
|
# [5.25.0-dev.12](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.11...v5.25.0-dev.12) (2025-05-28)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Swipe controls:** Add separate color settings for the brightness and volume bars ([#5043](https://github.com/ReVanced/revanced-patches/issues/5043)) ([80f50e8](https://github.com/ReVanced/revanced-patches/commit/80f50e8c50ca6a8366b7fd7b01459fb16fa1074a))
|
||||||
|
|
||||||
|
# [5.25.0-dev.11](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.10...v5.25.0-dev.11) (2025-05-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Enable debugging:** Add settings menu to share debug logs ([#5021](https://github.com/ReVanced/revanced-patches/issues/5021)) ([1ec4a88](https://github.com/ReVanced/revanced-patches/commit/1ec4a88464a2a2810c02cf072950b618d183779a))
|
||||||
|
* **YouTube:** Add `Disable haptic feedback` patch ([#5033](https://github.com/ReVanced/revanced-patches/issues/5033)) ([bbe7974](https://github.com/ReVanced/revanced-patches/commit/bbe79744a513c96f9016476e8435f999e94c45d7))
|
||||||
|
|
||||||
|
# [5.25.0-dev.10](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.9...v5.25.0-dev.10) (2025-05-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Messenger:** Remove outdated `Disable switching emoji to sticker` patch ([#5044](https://github.com/ReVanced/revanced-patches/issues/5044)) ([7b182ca](https://github.com/ReVanced/revanced-patches/commit/7b182cab825ee3a4a3ca528c744c9d2a351c7cf8))
|
||||||
|
* **Spotify Lite:** Remove obsolete `Enable on demand` patch ([#5046](https://github.com/ReVanced/revanced-patches/issues/5046)) ([4886d47](https://github.com/ReVanced/revanced-patches/commit/4886d47506c94b03c1f190ecc4947d3d91df6a47))
|
||||||
|
|
||||||
|
# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5))
|
||||||
|
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16))
|
||||||
|
|
||||||
|
# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5))
|
||||||
|
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16))
|
||||||
|
|
||||||
|
# [5.25.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.8...v5.25.0-dev.9) (2025-05-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Spotify:** Add `Fix Facebook login` patch ([#5023](https://github.com/ReVanced/revanced-patches/issues/5023)) ([34932dc](https://github.com/ReVanced/revanced-patches/commit/34932dc43933d346a5a3adadc62c0dbd38a633b5))
|
||||||
|
* **YouTube - Settings:** Add a color picker ([#4981](https://github.com/ReVanced/revanced-patches/issues/4981)) ([1e0e398](https://github.com/ReVanced/revanced-patches/commit/1e0e398574329173aff11a4dc9acfc3fcdeabe16))
|
||||||
|
|
||||||
|
# [5.25.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.7...v5.25.0-dev.8) (2025-05-25)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Hide ADB status:** Resolve app crash on startup ([#5029](https://github.com/ReVanced/revanced-patches/issues/5029)) ([1abebd5](https://github.com/ReVanced/revanced-patches/commit/1abebd5f3b73250c6638d2d8a274b92ea8268924))
|
||||||
|
|
||||||
|
# [5.25.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.6...v5.25.0-dev.7) (2025-05-24)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Open Shorts in regular player:** Do not exit app when pressing back button in regular player ([#5020](https://github.com/ReVanced/revanced-patches/issues/5020)) ([3384f8d](https://github.com/ReVanced/revanced-patches/commit/3384f8dd0ff2a345f2e387f4ed1570079a83ccb6))
|
||||||
|
|
||||||
|
# [5.25.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.5...v5.25.0-dev.6) (2025-05-23)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Yuka - Unlock premium:** Remove broken patch that is no longer supported ([#5018](https://github.com/ReVanced/revanced-patches/issues/5018)) ([fac6e59](https://github.com/ReVanced/revanced-patches/commit/fac6e59d281e21e57abdcfc899cd1aeb18e5c2b8))
|
||||||
|
|
||||||
|
# [5.25.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.4...v5.25.0-dev.5) (2025-05-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube:** Better handle incorrect duplicate translations ([20abac5](https://github.com/ReVanced/revanced-patches/commit/20abac52121fbecb65d87d0982f3380e1cf4e20e))
|
||||||
|
|
||||||
|
# [5.25.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.3...v5.25.0-dev.4) (2025-05-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - GmsCore support:** Restore patch functionality from prior merge ([7686bbe](https://github.com/ReVanced/revanced-patches/commit/7686bbe975644e1e582fa52f166879da5694ed93))
|
||||||
|
|
||||||
|
# [5.25.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.2...v5.25.0-dev.3) (2025-05-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Hide ads:** Hide new type of general ad ([#5004](https://github.com/ReVanced/revanced-patches/issues/5004)) ([37e59d2](https://github.com/ReVanced/revanced-patches/commit/37e59d2771528c631dc13e73dac095fec95c6485))
|
||||||
|
|
||||||
|
# [5.25.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.25.0-dev.1...v5.25.0-dev.2) (2025-05-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Disable Pairip license check:** Change patch to default off ([74b6a94](https://github.com/ReVanced/revanced-patches/commit/74b6a94577ac3f73b04bd0cce98fb7011a6607fd))
|
||||||
|
|
||||||
|
# [5.25.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.24.0...v5.25.0-dev.1) (2025-05-22)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Add `Disable pairip license check` patch ([#4927](https://github.com/ReVanced/revanced-patches/issues/4927)) ([42d2c27](https://github.com/ReVanced/revanced-patches/commit/42d2c277982ef63e6ad42d85e46f13c3ab50243c))
|
||||||
|
* **Messenger:** Add `Remove Meta AI` patch ([#4945](https://github.com/ReVanced/revanced-patches/issues/4945)) ([012dff7](https://github.com/ReVanced/revanced-patches/commit/012dff7b6511b9e519ccac96f6713cf1a1b327b4))
|
||||||
|
|
||||||
|
# [5.24.0](https://github.com/ReVanced/revanced-patches/compare/v5.23.0...v5.24.0) (2025-05-19)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Spotify - Fix third party launchers widgets:** Add missing compatibility annotation ([0493f80](https://github.com/ReVanced/revanced-patches/commit/0493f8035b26b90c5f8e42be2e2a5ce73d8685a5))
|
||||||
|
* **YouTube - Hide layout components:** Fix `Hide video recommendation labels` ([#4956](https://github.com/ReVanced/revanced-patches/issues/4956)) ([ae05ac3](https://github.com/ReVanced/revanced-patches/commit/ae05ac38151ebd3197953af97ca0dd847a04cc2d))
|
||||||
|
* **YouTube - Settings:** Correctly show summary text if search box is closed before searching ([d0ae835](https://github.com/ReVanced/revanced-patches/commit/d0ae835d3381fc659c9bb4a2d130d4db8a1499cf))
|
||||||
|
* **YouTube - SponsorBlock:** Fix segment category summary not showing category description ([06934a6](https://github.com/ReVanced/revanced-patches/commit/06934a60d91b40a5cdf7f4cd92deae4a136c149b))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **GmsCore support:** Open vendor specific DontKillMyApp if available ([#4952](https://github.com/ReVanced/revanced-patches/issues/4952)) ([b89927a](https://github.com/ReVanced/revanced-patches/commit/b89927a10e3b909a3c37fbb75c16a7abbce44560))
|
||||||
|
* **NU.nl:** Support version `11.3.0` ([#4925](https://github.com/ReVanced/revanced-patches/issues/4925)) ([bedde60](https://github.com/ReVanced/revanced-patches/commit/bedde60fc1a52b0fd491174b3b5b887435eb621a))
|
||||||
|
* **Spotify:** Add `Fix third party launchers widgets` patch ([#4893](https://github.com/ReVanced/revanced-patches/issues/4893)) ([23bfdc9](https://github.com/ReVanced/revanced-patches/commit/23bfdc98fbbcc8ecf0ffbf8704f58dd2272e4af2))
|
||||||
|
* **YouTube - Hide description components:** Add `Hide Ask` ([#4972](https://github.com/ReVanced/revanced-patches/issues/4972)) ([ebc94a5](https://github.com/ReVanced/revanced-patches/commit/ebc94a5da6214b67399c9c01515689bd4b20547c))
|
||||||
|
* **YouTube - Hide layout components:** Add `Hide ticket shelf` ([#4969](https://github.com/ReVanced/revanced-patches/issues/4969)) ([6436af7](https://github.com/ReVanced/revanced-patches/commit/6436af7e77c77d2034dfceba8bc51132ad7632be))
|
||||||
|
* **YouTube - Hide player components:** Hide related video overlay in fullscreen ([#4938](https://github.com/ReVanced/revanced-patches/issues/4938)) ([ac9be97](https://github.com/ReVanced/revanced-patches/commit/ac9be9760c9965e54df196b227a310d64ead4bf5))
|
||||||
|
* **YouTube - Settings:** Add ability to search in settings ([#4881](https://github.com/ReVanced/revanced-patches/issues/4881)) ([aca8b20](https://github.com/ReVanced/revanced-patches/commit/aca8b207c15f254bcc9ad94bc7dfb895f21d4058))
|
||||||
|
|
||||||
|
# [5.24.0-dev.9](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.8...v5.24.0-dev.9) (2025-05-18)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - SponsorBlock:** Fix segment category summary not showing category description ([06934a6](https://github.com/ReVanced/revanced-patches/commit/06934a60d91b40a5cdf7f4cd92deae4a136c149b))
|
||||||
|
|
||||||
|
# [5.24.0-dev.8](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.7...v5.24.0-dev.8) (2025-05-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Settings:** Correctly show summary text if search box is closed before searching ([d0ae835](https://github.com/ReVanced/revanced-patches/commit/d0ae835d3381fc659c9bb4a2d130d4db8a1499cf))
|
||||||
|
|
||||||
|
# [5.24.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.6...v5.24.0-dev.7) (2025-05-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Add `Hide ticket shelf` ([#4969](https://github.com/ReVanced/revanced-patches/issues/4969)) ([6436af7](https://github.com/ReVanced/revanced-patches/commit/6436af7e77c77d2034dfceba8bc51132ad7632be))
|
||||||
|
|
||||||
|
# [5.24.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.5...v5.24.0-dev.6) (2025-05-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Hide description components:** Add `Hide Ask` ([#4972](https://github.com/ReVanced/revanced-patches/issues/4972)) ([ebc94a5](https://github.com/ReVanced/revanced-patches/commit/ebc94a5da6214b67399c9c01515689bd4b20547c))
|
||||||
|
|
||||||
|
# [5.24.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.4...v5.24.0-dev.5) (2025-05-17)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **Spotify - Fix third party launchers widgets:** Add missing compatibility annotation ([0493f80](https://github.com/ReVanced/revanced-patches/commit/0493f8035b26b90c5f8e42be2e2a5ce73d8685a5))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **YouTube - Settings:** Add ability to search in settings ([#4881](https://github.com/ReVanced/revanced-patches/issues/4881)) ([aca8b20](https://github.com/ReVanced/revanced-patches/commit/aca8b207c15f254bcc9ad94bc7dfb895f21d4058))
|
||||||
|
|
||||||
|
# [5.24.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.3...v5.24.0-dev.4) (2025-05-16)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Spotify:** Add `Fix third party launchers widgets` patch ([#4893](https://github.com/ReVanced/revanced-patches/issues/4893)) ([23bfdc9](https://github.com/ReVanced/revanced-patches/commit/23bfdc98fbbcc8ecf0ffbf8704f58dd2272e4af2))
|
||||||
|
|
||||||
|
# [5.24.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.2...v5.24.0-dev.3) (2025-05-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **YouTube - Hide layout components:** Fix `Hide video recommendation labels` ([#4956](https://github.com/ReVanced/revanced-patches/issues/4956)) ([ae05ac3](https://github.com/ReVanced/revanced-patches/commit/ae05ac38151ebd3197953af97ca0dd847a04cc2d))
|
||||||
|
|
||||||
|
# [5.24.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.24.0-dev.1...v5.24.0-dev.2) (2025-05-14)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **GmsCore support:** Open vendor specific DontKillMyApp if available ([#4952](https://github.com/ReVanced/revanced-patches/issues/4952)) ([b89927a](https://github.com/ReVanced/revanced-patches/commit/b89927a10e3b909a3c37fbb75c16a7abbce44560))
|
||||||
|
* **YouTube - Hide player components:** Hide related video overlay in fullscreen ([#4938](https://github.com/ReVanced/revanced-patches/issues/4938)) ([ac9be97](https://github.com/ReVanced/revanced-patches/commit/ac9be9760c9965e54df196b227a310d64ead4bf5))
|
||||||
|
|
||||||
|
# [5.24.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v5.23.0...v5.24.0-dev.1) (2025-05-12)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **NU.nl:** Support version `11.3.0` ([#4925](https://github.com/ReVanced/revanced-patches/issues/4925)) ([bedde60](https://github.com/ReVanced/revanced-patches/commit/bedde60fc1a52b0fd491174b3b5b887435eb621a))
|
||||||
|
|
||||||
|
# [5.23.0](https://github.com/ReVanced/revanced-patches/compare/v5.22.0...v5.23.0) (2025-05-10)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Correct incorrect fingerprint ([c3bab89](https://github.com/ReVanced/revanced-patches/commit/c3bab89fc4189e38c10eee0caa36289de7e29dfa))
|
||||||
|
* Fix incorrect fingerprints ([#4917](https://github.com/ReVanced/revanced-patches/issues/4917)) ([49ca329](https://github.com/ReVanced/revanced-patches/commit/49ca3290a726cdba7bc9b62ffcd8d46e6f04778e))
|
||||||
|
* **Spotify - Unlock Spotify Premium:** Remove pop up premium ads ([#4842](https://github.com/ReVanced/revanced-patches/issues/4842)) ([00aa200](https://github.com/ReVanced/revanced-patches/commit/00aa2000ba2eef15a0dd827c2bd84c2e85c412e0))
|
||||||
|
* **YouTube:** Improve litho filtering performance ([#4904](https://github.com/ReVanced/revanced-patches/issues/4904)) ([7b43986](https://github.com/ReVanced/revanced-patches/commit/7b43986871a68e5cb43331d2fb2fdb9ef67438ad))
|
||||||
|
* **YouTube:** Simplify litho filtering patch ([#4910](https://github.com/ReVanced/revanced-patches/issues/4910)) ([bd53955](https://github.com/ReVanced/revanced-patches/commit/bd53955df738bb7b819eb91a3e776e9d2ca5c74a))
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **Lightroom:** Constrain patches to last working version ([efef03b](https://github.com/ReVanced/revanced-patches/commit/efef03b80da21552d0d8be6913faba64e4fb5ed1))
|
||||||
|
* **Pandora:** Add `Disable audio ads` and `Unlimited skips` patch ([#4841](https://github.com/ReVanced/revanced-patches/issues/4841)) ([0cf7a4c](https://github.com/ReVanced/revanced-patches/commit/0cf7a4c6be615ed0a52a6bacf87592f5f43ff575))
|
||||||
|
* **Prime Video:** Add `Skip ads` patch ([#4824](https://github.com/ReVanced/revanced-patches/issues/4824)) ([bb672c4](https://github.com/ReVanced/revanced-patches/commit/bb672c4674ddc201b8b2648c3906cfc31ef43f10))
|
||||||
|
* **Spotify:** Add `Sanitize sharing links` patch ([#4829](https://github.com/ReVanced/revanced-patches/issues/4829)) ([2e3511d](https://github.com/ReVanced/revanced-patches/commit/2e3511d03c8198bbdb9336888df038a33fb3ab8c))
|
||||||
|
|
||||||
|
# [5.23.0-dev.7](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.6...v5.23.0-dev.7) (2025-05-06)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* Fix incorrect fingerprints ([#4917](https://github.com/ReVanced/revanced-patches/issues/4917)) ([49ca329](https://github.com/ReVanced/revanced-patches/commit/49ca3290a726cdba7bc9b62ffcd8d46e6f04778e))
|
||||||
|
|
||||||
# [5.23.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.5...v5.23.0-dev.6) (2025-05-06)
|
# [5.23.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v5.23.0-dev.5...v5.23.0-dev.6) (2025-05-06)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
3
extensions/messenger/build.gradle.kts
Normal file
3
extensions/messenger/build.gradle.kts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
dependencies {
|
||||||
|
compileOnly(project(":extensions:shared:library"))
|
||||||
|
}
|
||||||
1
extensions/messenger/src/main/AndroidManifest.xml
Normal file
1
extensions/messenger/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest/>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package app.revanced.extension.messenger.metaai;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class RemoveMetaAIPatch {
|
||||||
|
public static boolean overrideConfigBool(long id, boolean value) {
|
||||||
|
// It seems like all configs starting with 363219 are related to Meta AI.
|
||||||
|
// A list of specific ones that need disabling would probably be better,
|
||||||
|
// but these config numbers seem to change slightly with each update.
|
||||||
|
// These first 6 digits don't though.
|
||||||
|
if (Long.toString(id).startsWith("363219"))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ public class HideAdsPatch {
|
|||||||
|
|
||||||
// Filter HeaderBlock with known ads until next HeaderBlock.
|
// Filter HeaderBlock with known ads until next HeaderBlock.
|
||||||
if (currentBlock instanceof HeaderBlock headerBlock) {
|
if (currentBlock instanceof HeaderBlock headerBlock) {
|
||||||
StyledText headerText = headerBlock.component20();
|
StyledText headerText = headerBlock.getTitle();
|
||||||
if (headerText != null) {
|
if (headerText != null) {
|
||||||
skipFullHeader = false;
|
skipFullHeader = false;
|
||||||
for (String blockedHeaderBlock : blockedHeaderBlocks) {
|
for (String blockedHeaderBlock : blockedHeaderBlocks) {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ package nl.nu.performance.api.client.objects;
|
|||||||
import nl.nu.performance.api.client.interfaces.Block;
|
import nl.nu.performance.api.client.interfaces.Block;
|
||||||
|
|
||||||
public class HeaderBlock extends Block {
|
public class HeaderBlock extends Block {
|
||||||
// returns title
|
public final StyledText getTitle() {
|
||||||
public final StyledText component20() {
|
|
||||||
throw new UnsupportedOperationException("Stub");
|
throw new UnsupportedOperationException("Stub");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.shared;
|
package app.revanced.extension.shared;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.requests.Route.Method.GET;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@@ -15,10 +16,16 @@ import android.os.Build;
|
|||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
|
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.requests.Requester;
|
||||||
|
import app.revanced.extension.shared.requests.Route;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class GmsCoreSupport {
|
public class GmsCoreSupport {
|
||||||
@@ -29,10 +36,24 @@ public class GmsCoreSupport {
|
|||||||
= getGmsCoreVendorGroupId() + ".android.gms";
|
= getGmsCoreVendorGroupId() + ".android.gms";
|
||||||
private static final Uri GMS_CORE_PROVIDER
|
private static final Uri GMS_CORE_PROVIDER
|
||||||
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
|
= Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix");
|
||||||
private static final String DONT_KILL_MY_APP_LINK
|
private static final String DONT_KILL_MY_APP_URL
|
||||||
= "https://dontkillmyapp.com";
|
= "https://dontkillmyapp.com/";
|
||||||
|
private static final Route DONT_KILL_MY_APP_MANUFACTURER_API
|
||||||
|
= new Route(GET, "/api/v2/{manufacturer}.json");
|
||||||
|
private static final String DONT_KILL_MY_APP_NAME_PARAMETER
|
||||||
|
= "?app=MicroG";
|
||||||
|
private static final String BUILD_MANUFACTURER
|
||||||
|
= Build.MANUFACTURER.toLowerCase(Locale.ROOT).replace(" ", "-");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a manufacturer specific page exists on DontKillMyApp.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
private static volatile Boolean DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||||
|
|
||||||
private static void open(String queryOrLink) {
|
private static void open(String queryOrLink) {
|
||||||
|
Logger.printInfo(() -> "Opening link: " + queryOrLink);
|
||||||
|
|
||||||
Intent intent;
|
Intent intent;
|
||||||
try {
|
try {
|
||||||
// Check if queryOrLink is a valid URL.
|
// Check if queryOrLink is a valid URL.
|
||||||
@@ -86,7 +107,7 @@ public class GmsCoreSupport {
|
|||||||
|
|
||||||
// Do not exit. If the app exits before launch completes (and without
|
// Do not exit. If the app exits before launch completes (and without
|
||||||
// opening another activity), then on some devices such as Pixel phone Android 10
|
// opening another activity), then on some devices such as Pixel phone Android 10
|
||||||
// no toast will be shown and the app will continually be relaunched
|
// no toast will be shown and the app will continually relaunch
|
||||||
// with the appearance of a hung app.
|
// with the appearance of a hung app.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,11 +143,12 @@ public class GmsCoreSupport {
|
|||||||
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
Logger.printInfo(() -> "GmsCore is not running in the background");
|
Logger.printInfo(() -> "GmsCore is not running in the background");
|
||||||
|
checkIfDontKillMyAppSupportsManufacturer();
|
||||||
|
|
||||||
showBatteryOptimizationDialog(context,
|
showBatteryOptimizationDialog(context,
|
||||||
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
"gms_core_dialog_not_whitelisted_not_allowed_in_background_message",
|
||||||
"gms_core_dialog_open_website_text",
|
"gms_core_dialog_open_website_text",
|
||||||
(dialog, id) -> open(DONT_KILL_MY_APP_LINK));
|
(dialog, id) -> openDontKillMyApp());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
@@ -141,6 +163,48 @@ public class GmsCoreSupport {
|
|||||||
activity.startActivityForResult(intent, 0);
|
activity.startActivityForResult(intent, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void checkIfDontKillMyAppSupportsManufacturer() {
|
||||||
|
Utils.runOnBackgroundThread(() -> {
|
||||||
|
try {
|
||||||
|
final long start = System.currentTimeMillis();
|
||||||
|
HttpURLConnection connection = Requester.getConnectionFromRoute(
|
||||||
|
DONT_KILL_MY_APP_URL, DONT_KILL_MY_APP_MANUFACTURER_API, BUILD_MANUFACTURER);
|
||||||
|
connection.setConnectTimeout(5000);
|
||||||
|
connection.setReadTimeout(5000);
|
||||||
|
|
||||||
|
final boolean supported = connection.getResponseCode() == 200;
|
||||||
|
Logger.printInfo(() -> "Manufacturer is " + (supported ? "" : "NOT ")
|
||||||
|
+ "listed on DontKillMyApp: " + BUILD_MANUFACTURER
|
||||||
|
+ " fetch took: " + (System.currentTimeMillis() - start) + "ms");
|
||||||
|
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = supported;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printInfo(() -> "Could not check if manufacturer is listed on DontKillMyApp: "
|
||||||
|
+ BUILD_MANUFACTURER, ex);
|
||||||
|
DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void openDontKillMyApp() {
|
||||||
|
final Boolean manufacturerSupported = DONT_KILL_MY_APP_MANUFACTURER_SUPPORTED;
|
||||||
|
|
||||||
|
String manufacturerPageToOpen;
|
||||||
|
if (manufacturerSupported == null) {
|
||||||
|
// Fetch has not completed yet. Only happens on extremely slow internet connections
|
||||||
|
// and the user spends less than 1 second reading what's on screen.
|
||||||
|
// Instead of waiting for the fetch (which may timeout),
|
||||||
|
// open the website without a vendor.
|
||||||
|
manufacturerPageToOpen = "";
|
||||||
|
} else if (manufacturerSupported) {
|
||||||
|
manufacturerPageToOpen = BUILD_MANUFACTURER;
|
||||||
|
} else {
|
||||||
|
// No manufacturer specific page exists. Open the general page.
|
||||||
|
manufacturerPageToOpen = "general";
|
||||||
|
}
|
||||||
|
|
||||||
|
open(DONT_KILL_MY_APP_URL + manufacturerPageToOpen + DONT_KILL_MY_APP_NAME_PARAMETER);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return If GmsCore is not whitelisted from battery optimizations.
|
* @return If GmsCore is not whitelisted from battery optimizations.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,15 +1,26 @@
|
|||||||
package app.revanced.extension.shared;
|
package app.revanced.extension.shared;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG;
|
||||||
|
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_STACKTRACE;
|
||||||
|
import static app.revanced.extension.shared.settings.BaseSettings.DEBUG_TOAST_ON_ERROR;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
|
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.settings.BaseSettings.*;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReVanced specific logger. Logging is done to standard device log (accessible thru ADB),
|
||||||
|
* and additionally accessible thru {@link LogBufferManager}.
|
||||||
|
*
|
||||||
|
* All methods are thread safe.
|
||||||
|
*/
|
||||||
public class Logger {
|
public class Logger {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,99 +28,159 @@ public class Logger {
|
|||||||
*/
|
*/
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface LogMessage {
|
public interface LogMessage {
|
||||||
|
/**
|
||||||
|
* @return Logger string message. This method is only called if logging is enabled.
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
String buildMessageString();
|
String buildMessageString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
private enum LogLevel {
|
||||||
* @return For outer classes, this returns {@link Class#getSimpleName()}.
|
DEBUG,
|
||||||
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
|
INFO,
|
||||||
* <br>
|
ERROR
|
||||||
* For example, each of these classes return 'SomethingView':
|
}
|
||||||
* <code>
|
|
||||||
* com.company.SomethingView
|
|
||||||
* com.company.SomethingView$StaticClass
|
|
||||||
* com.company.SomethingView$1
|
|
||||||
* </code>
|
|
||||||
*/
|
|
||||||
private String findOuterClassSimpleName() {
|
|
||||||
var selfClass = this.getClass();
|
|
||||||
|
|
||||||
String fullClassName = selfClass.getName();
|
/**
|
||||||
final int dollarSignIndex = fullClassName.indexOf('$');
|
* Log tag prefix. Only used for system logging.
|
||||||
if (dollarSignIndex < 0) {
|
*/
|
||||||
return selfClass.getSimpleName(); // Already an outer class.
|
private static final String REVANCED_LOG_TAG_PREFIX = "revanced: ";
|
||||||
|
|
||||||
|
private static final String LOGGER_CLASS_NAME = Logger.class.getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return For outer classes, this returns {@link Class#getSimpleName()}.
|
||||||
|
* For static, inner, or anonymous classes, this returns the simple name of the enclosing class.
|
||||||
|
* <br>
|
||||||
|
* For example, each of these classes returns 'SomethingView':
|
||||||
|
* <code>
|
||||||
|
* com.company.SomethingView
|
||||||
|
* com.company.SomethingView$StaticClass
|
||||||
|
* com.company.SomethingView$1
|
||||||
|
* </code>
|
||||||
|
*/
|
||||||
|
private static String getOuterClassSimpleName(Object obj) {
|
||||||
|
Class<?> logClass = obj.getClass();
|
||||||
|
String fullClassName = logClass.getName();
|
||||||
|
final int dollarSignIndex = fullClassName.indexOf('$');
|
||||||
|
if (dollarSignIndex < 0) {
|
||||||
|
return logClass.getSimpleName(); // Already an outer class.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Class is inner, static, or anonymous.
|
||||||
|
// Parse the simple name full name.
|
||||||
|
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
|
||||||
|
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
|
||||||
|
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal method to handle logging to Android Log and {@link LogBufferManager}.
|
||||||
|
* Appends the log message, stack trace (if enabled), and exception (if present) to logBuffer
|
||||||
|
* with class name but without 'revanced:' prefix.
|
||||||
|
*
|
||||||
|
* @param logLevel The log level.
|
||||||
|
* @param message Log message object.
|
||||||
|
* @param ex Optional exception.
|
||||||
|
* @param includeStackTrace If the current stack should be included.
|
||||||
|
* @param showToast If a toast is to be shown.
|
||||||
|
*/
|
||||||
|
private static void logInternal(LogLevel logLevel, LogMessage message, @Nullable Throwable ex,
|
||||||
|
boolean includeStackTrace, boolean showToast) {
|
||||||
|
// It's very important that no Settings are used in this method,
|
||||||
|
// as this code is used when a context is not set and thus referencing
|
||||||
|
// a setting will crash the app.
|
||||||
|
String messageString = message.buildMessageString();
|
||||||
|
String className = getOuterClassSimpleName(message);
|
||||||
|
|
||||||
|
String logText = messageString;
|
||||||
|
|
||||||
|
// Append exception message if present.
|
||||||
|
if (ex != null) {
|
||||||
|
var exceptionMessage = ex.getMessage();
|
||||||
|
if (exceptionMessage != null) {
|
||||||
|
logText += "\nException: " + exceptionMessage;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Class is inner, static, or anonymous.
|
if (includeStackTrace) {
|
||||||
// Parse the simple name full name.
|
var sw = new StringWriter();
|
||||||
// A class with no package returns index of -1, but incrementing gives index zero which is correct.
|
new Throwable().printStackTrace(new PrintWriter(sw));
|
||||||
final int simpleClassNameStartIndex = fullClassName.lastIndexOf('.') + 1;
|
String stackTrace = sw.toString();
|
||||||
return fullClassName.substring(simpleClassNameStartIndex, dollarSignIndex);
|
// Remove the stacktrace elements of this class.
|
||||||
|
final int loggerIndex = stackTrace.lastIndexOf(LOGGER_CLASS_NAME);
|
||||||
|
final int loggerBegins = stackTrace.indexOf('\n', loggerIndex);
|
||||||
|
logText += stackTrace.substring(loggerBegins);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not include "revanced:" prefix in clipboard logs.
|
||||||
|
String managerToastString = className + ": " + logText;
|
||||||
|
LogBufferManager.appendToLogBuffer(managerToastString);
|
||||||
|
|
||||||
|
String logTag = REVANCED_LOG_TAG_PREFIX + className;
|
||||||
|
switch (logLevel) {
|
||||||
|
case DEBUG:
|
||||||
|
if (ex == null) Log.d(logTag, logText);
|
||||||
|
else Log.d(logTag, logText, ex);
|
||||||
|
break;
|
||||||
|
case INFO:
|
||||||
|
if (ex == null) Log.i(logTag, logText);
|
||||||
|
else Log.i(logTag, logText, ex);
|
||||||
|
break;
|
||||||
|
case ERROR:
|
||||||
|
if (ex == null) Log.e(logTag, logText);
|
||||||
|
else Log.e(logTag, logText, ex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showToast) {
|
||||||
|
Utils.showToastLong(managerToastString);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String REVANCED_LOG_PREFIX = "revanced: ";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs debug messages under the outer class name of the code calling this method.
|
* Logs debug messages under the outer class name of the code calling this method.
|
||||||
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
|
* <p>
|
||||||
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
* Whenever possible, the log string should be constructed entirely inside
|
||||||
|
* {@link LogMessage#buildMessageString()} so the performance cost of
|
||||||
|
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||||
*/
|
*/
|
||||||
public static void printDebug(@NonNull LogMessage message) {
|
public static void printDebug(LogMessage message) {
|
||||||
printDebug(message, null);
|
printDebug(message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs debug messages under the outer class name of the code calling this method.
|
* Logs debug messages under the outer class name of the code calling this method.
|
||||||
* Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()}
|
* <p>
|
||||||
* so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
* Whenever possible, the log string should be constructed entirely inside
|
||||||
|
* {@link LogMessage#buildMessageString()} so the performance cost of
|
||||||
|
* building strings is paid only if {@link BaseSettings#DEBUG} is enabled.
|
||||||
*/
|
*/
|
||||||
public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) {
|
public static void printDebug(LogMessage message, @Nullable Exception ex) {
|
||||||
if (DEBUG.get()) {
|
if (DEBUG.get()) {
|
||||||
String logMessage = message.buildMessageString();
|
logInternal(LogLevel.DEBUG, message, ex, DEBUG_STACKTRACE.get(), false);
|
||||||
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
|
|
||||||
|
|
||||||
if (DEBUG_STACKTRACE.get()) {
|
|
||||||
var builder = new StringBuilder(logMessage);
|
|
||||||
var sw = new StringWriter();
|
|
||||||
new Throwable().printStackTrace(new PrintWriter(sw));
|
|
||||||
|
|
||||||
builder.append('\n').append(sw);
|
|
||||||
logMessage = builder.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ex == null) {
|
|
||||||
Log.d(logTag, logMessage);
|
|
||||||
} else {
|
|
||||||
Log.d(logTag, logMessage, ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs information messages using the outer class name of the code calling this method.
|
* Logs information messages using the outer class name of the code calling this method.
|
||||||
*/
|
*/
|
||||||
public static void printInfo(@NonNull LogMessage message) {
|
public static void printInfo(LogMessage message) {
|
||||||
printInfo(message, null);
|
printInfo(message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs information messages using the outer class name of the code calling this method.
|
* Logs information messages using the outer class name of the code calling this method.
|
||||||
*/
|
*/
|
||||||
public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) {
|
public static void printInfo(LogMessage message, @Nullable Exception ex) {
|
||||||
String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName();
|
logInternal(LogLevel.INFO, message, ex, DEBUG_STACKTRACE.get(), false);
|
||||||
String logMessage = message.buildMessageString();
|
|
||||||
if (ex == null) {
|
|
||||||
Log.i(logTag, logMessage);
|
|
||||||
} else {
|
|
||||||
Log.i(logTag, logMessage, ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs exceptions under the outer class name of the code calling this method.
|
* Logs exceptions under the outer class name of the code calling this method.
|
||||||
|
* Appends the log message, exception (if present), and toast message (if enabled) to logBuffer.
|
||||||
*/
|
*/
|
||||||
public static void printException(@NonNull LogMessage message) {
|
public static void printException(LogMessage message) {
|
||||||
printException(message, null);
|
printException(message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,35 +193,23 @@ public class Logger {
|
|||||||
* @param message log message
|
* @param message log message
|
||||||
* @param ex exception (optional)
|
* @param ex exception (optional)
|
||||||
*/
|
*/
|
||||||
public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) {
|
public static void printException(LogMessage message, @Nullable Throwable ex) {
|
||||||
String messageString = message.buildMessageString();
|
logInternal(LogLevel.ERROR, message, ex, DEBUG_STACKTRACE.get(), DEBUG_TOAST_ON_ERROR.get());
|
||||||
String outerClassSimpleName = message.findOuterClassSimpleName();
|
|
||||||
String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName;
|
|
||||||
if (ex == null) {
|
|
||||||
Log.e(logMessage, messageString);
|
|
||||||
} else {
|
|
||||||
Log.e(logMessage, messageString, ex);
|
|
||||||
}
|
|
||||||
if (DEBUG_TOAST_ON_ERROR.get()) {
|
|
||||||
Utils.showToastLong(outerClassSimpleName + ": " + messageString);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
||||||
* Normally this method should not be used.
|
* Normally this method should not be used.
|
||||||
*/
|
*/
|
||||||
public static void initializationInfo(@NonNull Class<?> callingClass, @NonNull String message) {
|
public static void initializationInfo(LogMessage message) {
|
||||||
Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message);
|
logInternal(LogLevel.INFO, message, null, false, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
* Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized.
|
||||||
* Normally this method should not be used.
|
* Normally this method should not be used.
|
||||||
*/
|
*/
|
||||||
public static void initializationException(@NonNull Class<?> callingClass, @NonNull String message,
|
public static void initializationException(LogMessage message, @Nullable Exception ex) {
|
||||||
@Nullable Exception ex) {
|
logInternal(LogLevel.ERROR, message, ex, false, false);
|
||||||
Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package app.revanced.extension.shared;
|
package app.revanced.extension.shared;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.*;
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.app.DialogFragment;
|
||||||
|
import android.app.Fragment;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.pm.ApplicationInfo;
|
import android.content.pm.ApplicationInfo;
|
||||||
@@ -18,6 +22,8 @@ import android.os.Looper;
|
|||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
import android.preference.PreferenceGroup;
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
|
import android.util.Pair;
|
||||||
|
import android.util.TypedValue;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewParent;
|
import android.view.ViewParent;
|
||||||
@@ -357,28 +363,31 @@ public class Utils {
|
|||||||
|
|
||||||
public static Context getContext() {
|
public static Context getContext() {
|
||||||
if (context == null) {
|
if (context == null) {
|
||||||
Logger.initializationException(Utils.class, "Context is not set by extension hook, returning null", null);
|
Logger.initializationException(() -> "Context is not set by extension hook, returning null", null);
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setContext(Context appContext) {
|
public static void setContext(Context appContext) {
|
||||||
|
// Intentionally use logger before context is set,
|
||||||
|
// to expose any bugs in the 'no context available' logger method.
|
||||||
|
Logger.initializationInfo(() -> "Set context: " + appContext);
|
||||||
// Must initially set context to check the app language.
|
// Must initially set context to check the app language.
|
||||||
context = appContext;
|
context = appContext;
|
||||||
Logger.initializationInfo(Utils.class, "Set context: " + appContext);
|
|
||||||
|
|
||||||
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
AppLanguage language = BaseSettings.REVANCED_LANGUAGE.get();
|
||||||
if (language != AppLanguage.DEFAULT) {
|
if (language != AppLanguage.DEFAULT) {
|
||||||
// Create a new context with the desired language.
|
// Create a new context with the desired language.
|
||||||
Logger.printDebug(() -> "Using app language: " + language);
|
Logger.printDebug(() -> "Using app language: " + language);
|
||||||
Configuration config = appContext.getResources().getConfiguration();
|
Configuration config = new Configuration(appContext.getResources().getConfiguration());
|
||||||
config.setLocale(language.getLocale());
|
config.setLocale(language.getLocale());
|
||||||
context = appContext.createConfigurationContext(config);
|
context = appContext.createConfigurationContext(config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setClipboard(@NonNull String text) {
|
public static void setClipboard(CharSequence text) {
|
||||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context
|
||||||
|
.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||||
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
|
android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text);
|
||||||
clipboard.setPrimaryClip(clip);
|
clipboard.setPrimaryClip(clip);
|
||||||
}
|
}
|
||||||
@@ -391,16 +400,47 @@ public class Utils {
|
|||||||
private static Boolean isRightToLeftTextLayout;
|
private static Boolean isRightToLeftTextLayout;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the device language uses right to left text layout (hebrew, arabic, etc)
|
* @return If the device language uses right to left text layout (Hebrew, Arabic, etc).
|
||||||
|
* If this should match any ReVanced language override then instead use
|
||||||
|
* {@link #isRightToLeftLocale(Locale)} with {@link BaseSettings#REVANCED_LANGUAGE}.
|
||||||
|
* This is the default locale of the device, which may differ if
|
||||||
|
* {@link BaseSettings#REVANCED_LANGUAGE} is set to a different language.
|
||||||
*/
|
*/
|
||||||
public static boolean isRightToLeftTextLayout() {
|
public static boolean isRightToLeftLocale() {
|
||||||
if (isRightToLeftTextLayout == null) {
|
if (isRightToLeftTextLayout == null) {
|
||||||
String displayLanguage = Locale.getDefault().getDisplayLanguage();
|
isRightToLeftTextLayout = isRightToLeftLocale(Locale.getDefault());
|
||||||
isRightToLeftTextLayout = new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
|
|
||||||
}
|
}
|
||||||
return isRightToLeftTextLayout;
|
return isRightToLeftTextLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return If the locale uses right to left text layout (Hebrew, Arabic, etc).
|
||||||
|
*/
|
||||||
|
public static boolean isRightToLeftLocale(Locale locale) {
|
||||||
|
String displayLanguage = locale.getDisplayLanguage();
|
||||||
|
return new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return A UTF8 string containing a left-to-right or right-to-left
|
||||||
|
* character of the device locale. If this should match any ReVanced language
|
||||||
|
* override then instead use {@link #getTextDirectionString(Locale)} with
|
||||||
|
* {@link BaseSettings#REVANCED_LANGUAGE}.
|
||||||
|
*/
|
||||||
|
public static String getTextDirectionString() {
|
||||||
|
return getTextDirectionString(isRightToLeftLocale());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTextDirectionString(Locale locale) {
|
||||||
|
return getTextDirectionString(isRightToLeftLocale(locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTextDirectionString(boolean isRightToLeft) {
|
||||||
|
return isRightToLeft
|
||||||
|
? "\u200F" // u200F = right to left character.
|
||||||
|
: "\u200E"; // u200E = left to right character.
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the text contains at least 1 number character,
|
* @return if the text contains at least 1 number character,
|
||||||
* including any unicode numbers such as Arabic.
|
* including any unicode numbers such as Arabic.
|
||||||
@@ -511,24 +551,25 @@ public class Utils {
|
|||||||
private static void showToast(@NonNull String messageToToast, int toastDuration) {
|
private static void showToast(@NonNull String messageToToast, int toastDuration) {
|
||||||
Objects.requireNonNull(messageToToast);
|
Objects.requireNonNull(messageToToast);
|
||||||
runOnMainThreadNowOrLater(() -> {
|
runOnMainThreadNowOrLater(() -> {
|
||||||
if (context == null) {
|
Context currentContext = context;
|
||||||
Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null);
|
|
||||||
} else {
|
if (currentContext == null) {
|
||||||
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
Logger.initializationException(() -> "Cannot show toast (context is null): " + messageToToast, null);
|
||||||
Toast.makeText(context, messageToToast, toastDuration).show();
|
} else {
|
||||||
}
|
Logger.printDebug(() -> "Showing toast: " + messageToToast);
|
||||||
}
|
Toast.makeText(currentContext, messageToToast, toastDuration).show();
|
||||||
);
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isDarkModeEnabled(Context context) {
|
public static boolean isDarkModeEnabled() {
|
||||||
Configuration config = context.getResources().getConfiguration();
|
Configuration config = Resources.getSystem().getConfiguration();
|
||||||
final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||||
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
return currentNightMode == Configuration.UI_MODE_NIGHT_YES;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean isLandscapeOrientation() {
|
public static boolean isLandscapeOrientation() {
|
||||||
final int orientation = context.getResources().getConfiguration().orientation;
|
final int orientation = Resources.getSystem().getConfiguration().orientation;
|
||||||
return orientation == Configuration.ORIENTATION_LANDSCAPE;
|
return orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,7 +583,7 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Automatically logs any exceptions the runnable throws
|
* Automatically logs any exceptions the runnable throws.
|
||||||
*/
|
*/
|
||||||
public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
|
public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) {
|
||||||
Runnable loggingRunnable = () -> {
|
Runnable loggingRunnable = () -> {
|
||||||
@@ -568,14 +609,14 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return if the calling thread is on the main thread
|
* @return if the calling thread is on the main thread.
|
||||||
*/
|
*/
|
||||||
public static boolean isCurrentlyOnMainThread() {
|
public static boolean isCurrentlyOnMainThread() {
|
||||||
return Looper.getMainLooper().isCurrentThread();
|
return Looper.getMainLooper().isCurrentThread();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws IllegalStateException if the calling thread is _off_ the main thread
|
* @throws IllegalStateException if the calling thread is _off_ the main thread.
|
||||||
*/
|
*/
|
||||||
public static void verifyOnMainThread() throws IllegalStateException {
|
public static void verifyOnMainThread() throws IllegalStateException {
|
||||||
if (!isCurrentlyOnMainThread()) {
|
if (!isCurrentlyOnMainThread()) {
|
||||||
@@ -584,7 +625,7 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws IllegalStateException if the calling thread is _on_ the main thread
|
* @throws IllegalStateException if the calling thread is _on_ the main thread.
|
||||||
*/
|
*/
|
||||||
public static void verifyOffMainThread() throws IllegalStateException {
|
public static void verifyOffMainThread() throws IllegalStateException {
|
||||||
if (isCurrentlyOnMainThread()) {
|
if (isCurrentlyOnMainThread()) {
|
||||||
@@ -598,13 +639,23 @@ public class Utils {
|
|||||||
OTHER,
|
OTHER,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calling extension code must ensure the un-patched app has the permission
|
||||||
|
* <code>android.permission.ACCESS_NETWORK_STATE</code>, otherwise the app will crash
|
||||||
|
* if this method is used.
|
||||||
|
*/
|
||||||
public static boolean isNetworkConnected() {
|
public static boolean isNetworkConnected() {
|
||||||
NetworkType networkType = getNetworkType();
|
NetworkType networkType = getNetworkType();
|
||||||
return networkType == NetworkType.MOBILE
|
return networkType == NetworkType.MOBILE
|
||||||
|| networkType == NetworkType.OTHER;
|
|| networkType == NetworkType.OTHER;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint({"MissingPermission", "deprecation"}) // Permission already included in YouTube.
|
/**
|
||||||
|
* Calling extension code must ensure the un-patched app has the permission
|
||||||
|
* <code>android.permission.ACCESS_NETWORK_STATE</code>, otherwise the app will crash
|
||||||
|
* if this method is used.
|
||||||
|
*/
|
||||||
|
@SuppressLint({"MissingPermission", "deprecation"})
|
||||||
public static NetworkType getNetworkType() {
|
public static NetworkType getNetworkType() {
|
||||||
Context networkContext = getContext();
|
Context networkContext = getContext();
|
||||||
if (networkContext == null) {
|
if (networkContext == null) {
|
||||||
@@ -692,9 +743,10 @@ public class Utils {
|
|||||||
/**
|
/**
|
||||||
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
|
* Strips all punctuation and converts to lower case. A null parameter returns an empty string.
|
||||||
*/
|
*/
|
||||||
public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) {
|
public static String removePunctuationToLowercase(@Nullable CharSequence original) {
|
||||||
if (original == null) return "";
|
if (original == null) return "";
|
||||||
return punctuationPattern.matcher(original).replaceAll("").toLowerCase();
|
return punctuationPattern.matcher(original).replaceAll("")
|
||||||
|
.toLowerCase(BaseSettings.REVANCED_LANGUAGE.get().getLocale());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -706,9 +758,9 @@ public class Utils {
|
|||||||
* then the preferences are left unsorted.
|
* then the preferences are left unsorted.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public static void sortPreferenceGroups(@NonNull PreferenceGroup group) {
|
public static void sortPreferenceGroups(PreferenceGroup group) {
|
||||||
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
|
Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED);
|
||||||
SortedMap<String, Preference> preferences = new TreeMap<>();
|
List<Pair<String, Preference>> preferences = new ArrayList<>();
|
||||||
|
|
||||||
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) {
|
||||||
Preference preference = group.getPreference(i);
|
Preference preference = group.getPreference(i);
|
||||||
@@ -726,7 +778,7 @@ public class Utils {
|
|||||||
final String sortValue;
|
final String sortValue;
|
||||||
switch (preferenceSort) {
|
switch (preferenceSort) {
|
||||||
case BY_TITLE:
|
case BY_TITLE:
|
||||||
sortValue = removePunctuationConvertToLowercase(preference.getTitle());
|
sortValue = removePunctuationToLowercase(preference.getTitle());
|
||||||
break;
|
break;
|
||||||
case BY_KEY:
|
case BY_KEY:
|
||||||
sortValue = preference.getKey();
|
sortValue = preference.getKey();
|
||||||
@@ -737,17 +789,22 @@ public class Utils {
|
|||||||
throw new IllegalStateException();
|
throw new IllegalStateException();
|
||||||
}
|
}
|
||||||
|
|
||||||
preferences.put(sortValue, preference);
|
preferences.add(new Pair<>(sortValue, preference));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//noinspection ComparatorCombinators
|
||||||
|
Collections.sort(preferences, (pair1, pair2)
|
||||||
|
-> pair1.first.compareTo(pair2.first));
|
||||||
|
|
||||||
int index = 0;
|
int index = 0;
|
||||||
for (Preference pref : preferences.values()) {
|
for (Pair<String, Preference> pair : preferences) {
|
||||||
int order = index++;
|
int order = index++;
|
||||||
|
Preference pref = pair.second;
|
||||||
|
|
||||||
// Move any screens, intents, and the one off About preference to the top.
|
// Move any screens, intents, and the one off About preference to the top.
|
||||||
if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference
|
if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference
|
||||||
|| pref.getIntent() != null) {
|
|| pref.getIntent() != null) {
|
||||||
// Arbitrary high number.
|
// Any arbitrary large number.
|
||||||
order -= 1000;
|
order -= 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -810,4 +867,26 @@ public class Utils {
|
|||||||
}
|
}
|
||||||
return getResourceColor(colorString);
|
return getResourceColor(colorString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts dip value to actual device pixels.
|
||||||
|
*
|
||||||
|
* @param dip The density-independent pixels value
|
||||||
|
* @return The device pixel value
|
||||||
|
*/
|
||||||
|
public static int dipToPixels(float dip) {
|
||||||
|
return (int) TypedValue.applyDimension(
|
||||||
|
TypedValue.COMPLEX_UNIT_DIP,
|
||||||
|
dip,
|
||||||
|
Resources.getSystem().getDisplayMetrics()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int clamp(int value, int lower, int upper) {
|
||||||
|
return Math.max(lower, Math.min(value, upper));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float clamp(float value, float lower, float upper) {
|
||||||
|
return Math.max(lower, Math.min(value, upper));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,9 +89,11 @@ public enum AppLanguage {
|
|||||||
ZU;
|
ZU;
|
||||||
|
|
||||||
private final String language;
|
private final String language;
|
||||||
|
private final Locale locale;
|
||||||
|
|
||||||
AppLanguage() {
|
AppLanguage() {
|
||||||
language = name().toLowerCase(Locale.US);
|
language = name().toLowerCase(Locale.US);
|
||||||
|
locale = Locale.forLanguageTag(language);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,6 +114,6 @@ public enum AppLanguage {
|
|||||||
return Locale.getDefault();
|
return Locale.getDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
return Locale.forLanguageTag(language);
|
return locale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,442 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.preference.EditTextPreference;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.InputType;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
import android.text.style.RelativeSizeSpan;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom preference for selecting a color via a hexadecimal code or a color picker dialog.
|
||||||
|
* Extends {@link EditTextPreference} to display a colored dot in the widget area,
|
||||||
|
* reflecting the currently selected color. The dot is dimmed when the preference is disabled.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class ColorPickerPreference extends EditTextPreference {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character to show the color appearance.
|
||||||
|
*/
|
||||||
|
public static final String COLOR_DOT_STRING = "⬤";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Length of a valid color string of format #RRGGBB.
|
||||||
|
*/
|
||||||
|
public static final int COLOR_STRING_LENGTH = 7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches everything that is not a hex number/letter.
|
||||||
|
*/
|
||||||
|
private static final Pattern PATTERN_NOT_HEX = Pattern.compile("[^0-9A-Fa-f]");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alpha for dimming when the preference is disabled.
|
||||||
|
*/
|
||||||
|
private static final float DISABLED_ALPHA = 0.5f; // 50%
|
||||||
|
|
||||||
|
/**
|
||||||
|
* View displaying a colored dot in the widget area.
|
||||||
|
*/
|
||||||
|
private View widgetColorDot;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current color in RGB format (without alpha).
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
|
private int currentColor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Associated setting for storing the color value.
|
||||||
|
*/
|
||||||
|
private StringSetting colorSetting;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog TextWatcher for the EditText to monitor color input changes.
|
||||||
|
*/
|
||||||
|
private TextWatcher colorTextWatcher;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog TextView displaying a colored dot for the selected color preview in the dialog.
|
||||||
|
*/
|
||||||
|
private TextView dialogColorPreview;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog color picker view.
|
||||||
|
*/
|
||||||
|
private ColorPickerView dialogColorPickerView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes non valid hex characters, converts to all uppercase,
|
||||||
|
* and adds # character to the start if not present.
|
||||||
|
*/
|
||||||
|
public static String cleanupColorCodeString(String colorString) {
|
||||||
|
// Remove non-hex chars, convert to uppercase, and ensure correct length
|
||||||
|
String result = "#" + PATTERN_NOT_HEX.matcher(colorString)
|
||||||
|
.replaceAll("").toUpperCase(Locale.ROOT);
|
||||||
|
|
||||||
|
if (result.length() < COLOR_STRING_LENGTH) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.substring(0, COLOR_STRING_LENGTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param color RGB color, without an alpha channel.
|
||||||
|
* @return #RRGGBB hex color string
|
||||||
|
*/
|
||||||
|
public static String getColorString(@ColorInt int color) {
|
||||||
|
String colorString = String.format("#%06X", color);
|
||||||
|
if ((color & 0xFF000000) != 0) {
|
||||||
|
// Likely a bug somewhere.
|
||||||
|
Logger.printException(() -> "getColorString: color has alpha channel: " + colorString);
|
||||||
|
}
|
||||||
|
return colorString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a Spanned object for a colored dot using SpannableString.
|
||||||
|
*
|
||||||
|
* @param color The RGB color (without alpha).
|
||||||
|
* @return A Spanned object with the colored dot.
|
||||||
|
*/
|
||||||
|
public static Spanned getColorDot(@ColorInt int color) {
|
||||||
|
SpannableString spannable = new SpannableString(COLOR_DOT_STRING);
|
||||||
|
spannable.setSpan(new ForegroundColorSpan(color | 0xFF000000), 0, COLOR_DOT_STRING.length(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
spannable.setSpan(new RelativeSizeSpan(1.5f), 0, 1,
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
return spannable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the preference by setting up the EditText, loading the color, and set the widget layout.
|
||||||
|
*/
|
||||||
|
private void init() {
|
||||||
|
colorSetting = (StringSetting) Setting.getSettingFromPath(getKey());
|
||||||
|
if (colorSetting == null) {
|
||||||
|
Logger.printException(() -> "Could not find color setting for: " + getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
EditText editText = getEditText();
|
||||||
|
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
||||||
|
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
editText.setAutofillHints((String) null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the widget layout to a custom layout containing the colored dot.
|
||||||
|
setWidgetLayoutResource(getResourceIdentifier("revanced_color_dot_widget", "layout"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected color and updates the UI and settings.
|
||||||
|
*
|
||||||
|
* @param colorString The color in hexadecimal format (e.g., "#RRGGBB").
|
||||||
|
* @throws IllegalArgumentException If the color string is invalid.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public final void setText(String colorString) {
|
||||||
|
try {
|
||||||
|
Logger.printDebug(() -> "setText: " + colorString);
|
||||||
|
super.setText(colorString);
|
||||||
|
|
||||||
|
currentColor = Color.parseColor(colorString) & 0x00FFFFFF;
|
||||||
|
if (colorSetting != null) {
|
||||||
|
colorSetting.save(getColorString(currentColor));
|
||||||
|
}
|
||||||
|
updateColorPreview();
|
||||||
|
updateWidgetColorDot();
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
// This code is reached if the user pastes settings json with an invalid color
|
||||||
|
// since this preference is updated with the new setting text.
|
||||||
|
Logger.printDebug(() -> "Parse color error: " + colorString, ex);
|
||||||
|
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||||
|
setText(colorSetting.resetToDefault());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "setText failure: " + colorString, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onBindView(View view) {
|
||||||
|
super.onBindView(view);
|
||||||
|
|
||||||
|
widgetColorDot = view.findViewById(getResourceIdentifier(
|
||||||
|
"revanced_color_dot_widget", "id"));
|
||||||
|
widgetColorDot.setBackgroundResource(getResourceIdentifier(
|
||||||
|
"revanced_settings_circle_background", "drawable"));
|
||||||
|
widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
|
||||||
|
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a layout with a color preview and EditText for hex color input.
|
||||||
|
*
|
||||||
|
* @param context The context for creating the layout.
|
||||||
|
* @return A LinearLayout containing the color preview and EditText.
|
||||||
|
*/
|
||||||
|
private LinearLayout createDialogLayout(Context context) {
|
||||||
|
LinearLayout layout = new LinearLayout(context);
|
||||||
|
layout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
layout.setPadding(70, 0, 70, 0);
|
||||||
|
|
||||||
|
// Inflate color picker.
|
||||||
|
View colorPicker = LayoutInflater.from(context).inflate(
|
||||||
|
getResourceIdentifier("revanced_color_picker", "layout"), null);
|
||||||
|
dialogColorPickerView = colorPicker.findViewById(
|
||||||
|
getResourceIdentifier("color_picker_view", "id"));
|
||||||
|
dialogColorPickerView.setColor(currentColor);
|
||||||
|
layout.addView(colorPicker);
|
||||||
|
|
||||||
|
// Horizontal layout for preview and EditText.
|
||||||
|
LinearLayout inputLayout = new LinearLayout(context);
|
||||||
|
inputLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
inputLayout.setPadding(0, 20, 0, 0);
|
||||||
|
|
||||||
|
dialogColorPreview = new TextView(context);
|
||||||
|
inputLayout.addView(dialogColorPreview);
|
||||||
|
updateColorPreview();
|
||||||
|
|
||||||
|
EditText editText = getEditText();
|
||||||
|
ViewParent parent = editText.getParent();
|
||||||
|
if (parent instanceof ViewGroup parentViewGroup) {
|
||||||
|
parentViewGroup.removeView(editText);
|
||||||
|
}
|
||||||
|
editText.setLayoutParams(new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||||
|
));
|
||||||
|
String currentColorString = getColorString(currentColor);
|
||||||
|
editText.setText(currentColorString);
|
||||||
|
editText.setSelection(currentColorString.length());
|
||||||
|
editText.setTypeface(Typeface.MONOSPACE);
|
||||||
|
colorTextWatcher = createColorTextWatcher(dialogColorPickerView);
|
||||||
|
editText.addTextChangedListener(colorTextWatcher);
|
||||||
|
inputLayout.addView(editText);
|
||||||
|
|
||||||
|
// Add a dummy view to take up remaining horizontal space,
|
||||||
|
// otherwise it will show an oversize underlined text view.
|
||||||
|
View paddingView = new View(context);
|
||||||
|
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||||
|
0,
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
1f
|
||||||
|
);
|
||||||
|
paddingView.setLayoutParams(params);
|
||||||
|
inputLayout.addView(paddingView);
|
||||||
|
|
||||||
|
layout.addView(inputLayout);
|
||||||
|
|
||||||
|
// Set up color picker listener with debouncing.
|
||||||
|
// Add listener last to prevent callbacks from set calls above.
|
||||||
|
dialogColorPickerView.setOnColorChangedListener(color -> {
|
||||||
|
// Check if it actually changed, since this callback
|
||||||
|
// can be caused by updates in afterTextChanged().
|
||||||
|
if (currentColor == color) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String updatedColorString = getColorString(color);
|
||||||
|
Logger.printDebug(() -> "onColorChanged: " + updatedColorString);
|
||||||
|
currentColor = color;
|
||||||
|
editText.setText(updatedColorString);
|
||||||
|
editText.setSelection(updatedColorString.length());
|
||||||
|
|
||||||
|
updateColorPreview();
|
||||||
|
updateWidgetColorDot();
|
||||||
|
});
|
||||||
|
|
||||||
|
return layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the color preview TextView with a colored dot.
|
||||||
|
*/
|
||||||
|
private void updateColorPreview() {
|
||||||
|
if (dialogColorPreview != null) {
|
||||||
|
dialogColorPreview.setText(getColorDot(currentColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateWidgetColorDot() {
|
||||||
|
if (widgetColorDot != null) {
|
||||||
|
widgetColorDot.getBackground().setTint(currentColor | 0xFF000000);
|
||||||
|
widgetColorDot.setAlpha(isEnabled() ? 1.0f : DISABLED_ALPHA);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a TextWatcher to monitor changes in the EditText for color input.
|
||||||
|
*
|
||||||
|
* @return A TextWatcher that updates the color preview on valid input.
|
||||||
|
*/
|
||||||
|
private TextWatcher createColorTextWatcher(ColorPickerView colorPickerView) {
|
||||||
|
return new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable edit) {
|
||||||
|
try {
|
||||||
|
String colorString = edit.toString();
|
||||||
|
|
||||||
|
String sanitizedColorString = cleanupColorCodeString(colorString);
|
||||||
|
if (!sanitizedColorString.equals(colorString)) {
|
||||||
|
edit.replace(0, colorString.length(), sanitizedColorString);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sanitizedColorString.length() != COLOR_STRING_LENGTH) {
|
||||||
|
// User is still typing out the color.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int newColor = Color.parseColor(colorString);
|
||||||
|
if (currentColor != newColor) {
|
||||||
|
Logger.printDebug(() -> "afterTextChanged: " + sanitizedColorString);
|
||||||
|
currentColor = newColor;
|
||||||
|
updateColorPreview();
|
||||||
|
updateWidgetColorDot();
|
||||||
|
colorPickerView.setColor(newColor);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Should never be reached since input is validated before using.
|
||||||
|
Logger.printException(() -> "afterTextChanged failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares the dialog builder with a custom view and reset button.
|
||||||
|
*
|
||||||
|
* @param builder The AlertDialog.Builder to configure.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||||
|
Utils.setEditTextDialogTheme(builder);
|
||||||
|
LinearLayout dialogLayout = createDialogLayout(builder.getContext());
|
||||||
|
builder.setView(dialogLayout);
|
||||||
|
final int originalColor = currentColor;
|
||||||
|
|
||||||
|
builder.setNeutralButton(str("revanced_settings_reset_color"), null);
|
||||||
|
|
||||||
|
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||||
|
try {
|
||||||
|
String colorString = getEditText().getText().toString();
|
||||||
|
|
||||||
|
if (colorString.length() != COLOR_STRING_LENGTH) {
|
||||||
|
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||||
|
setText(getColorString(originalColor));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setText(colorString);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Should never happen due to a bad color string,
|
||||||
|
// since the text is validated and fixed while the user types.
|
||||||
|
Logger.printException(() -> "setPositiveButton failure", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||||
|
try {
|
||||||
|
// Restore the original color.
|
||||||
|
setText(getColorString(originalColor));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "setNegativeButton failure", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void showDialog(Bundle state) {
|
||||||
|
super.showDialog(state);
|
||||||
|
|
||||||
|
AlertDialog dialog = (AlertDialog) getDialog();
|
||||||
|
dialog.setCanceledOnTouchOutside(false);
|
||||||
|
|
||||||
|
// Do not close dialog when reset is pressed.
|
||||||
|
Button button = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||||
|
button.setOnClickListener(view -> {
|
||||||
|
try {
|
||||||
|
final int defaultColor = Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
|
||||||
|
// Setting view color causes listener callback into this class.
|
||||||
|
dialogColorPickerView.setColor(defaultColor);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "setOnClickListener failure", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDialogClosed(boolean positiveResult) {
|
||||||
|
super.onDialogClosed(positiveResult);
|
||||||
|
|
||||||
|
if (colorTextWatcher != null) {
|
||||||
|
getEditText().removeTextChangedListener(colorTextWatcher);
|
||||||
|
colorTextWatcher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogColorPreview = null;
|
||||||
|
dialogColorPickerView = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setEnabled(boolean enabled) {
|
||||||
|
super.setEnabled(enabled);
|
||||||
|
updateWidgetColorDot();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.ComposeShader;
|
||||||
|
import android.graphics.LinearGradient;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.Shader;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom color picker view that allows the user to select a color using a hue slider and a saturation-value selector.
|
||||||
|
* This implementation is density-independent and responsive across different screen sizes and DPIs.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This view displays two main components for color selection:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>Hue Bar:</b> A vertical bar on the right that allows the user to select the hue component of the color.
|
||||||
|
* <li><b>Saturation-Value Selector:</b> A rectangular area that allows the user to select the saturation and value (brightness)
|
||||||
|
* components of the color based on the selected hue.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The view uses {@link LinearGradient} and {@link ComposeShader} to create the color gradients for the hue bar and the
|
||||||
|
* saturation-value selector. It also uses {@link Paint} to draw the selectors (draggable handles).
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The selected color can be retrieved using {@link #getColor()} and can be set using {@link #setColor(int)}.
|
||||||
|
* An {@link OnColorChangedListener} can be registered to receive notifications when the selected color changes.
|
||||||
|
*/
|
||||||
|
public class ColorPickerView extends View {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface definition for a callback to be invoked when the selected color changes.
|
||||||
|
*/
|
||||||
|
public interface OnColorChangedListener {
|
||||||
|
/**
|
||||||
|
* Called when the selected color has changed.
|
||||||
|
*
|
||||||
|
* Important: Callback color uses RGB format with zero alpha channel.
|
||||||
|
*
|
||||||
|
* @param color The new selected color.
|
||||||
|
*/
|
||||||
|
void onColorChanged(@ColorInt int color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Expanded touch area for the hue bar to increase the touch-sensitive area. */
|
||||||
|
public static final float TOUCH_EXPANSION = dipToPixels(20f);
|
||||||
|
|
||||||
|
private static final float MARGIN_BETWEEN_AREAS = dipToPixels(24);
|
||||||
|
private static final float VIEW_PADDING = dipToPixels(16);
|
||||||
|
private static final float HUE_BAR_WIDTH = dipToPixels(12);
|
||||||
|
private static final float HUE_CORNER_RADIUS = dipToPixels(6);
|
||||||
|
private static final float SELECTOR_RADIUS = dipToPixels(12);
|
||||||
|
private static final float SELECTOR_STROKE_WIDTH = 8;
|
||||||
|
/**
|
||||||
|
* Hue fill radius. Use slightly smaller radius for the selector handle fill,
|
||||||
|
* otherwise the anti-aliasing causes the fill color to bleed past the selector outline.
|
||||||
|
*/
|
||||||
|
private static final float SELECTOR_FILL_RADIUS = SELECTOR_RADIUS - SELECTOR_STROKE_WIDTH / 2;
|
||||||
|
/** Thin dark outline stroke width for the selector rings. */
|
||||||
|
private static final float SELECTOR_EDGE_STROKE_WIDTH = 1;
|
||||||
|
public static final float SELECTOR_EDGE_RADIUS =
|
||||||
|
SELECTOR_RADIUS + SELECTOR_STROKE_WIDTH / 2 + SELECTOR_EDGE_STROKE_WIDTH / 2;
|
||||||
|
|
||||||
|
/** Selector outline inner color. */
|
||||||
|
@ColorInt
|
||||||
|
private static final int SELECTOR_OUTLINE_COLOR = Color.WHITE;
|
||||||
|
|
||||||
|
/** Dark edge color for the selector rings. */
|
||||||
|
@ColorInt
|
||||||
|
private static final int SELECTOR_EDGE_COLOR = Color.parseColor("#CFCFCF");
|
||||||
|
|
||||||
|
private static final int[] HUE_COLORS = new int[361];
|
||||||
|
static {
|
||||||
|
for (int i = 0; i < 361; i++) {
|
||||||
|
HUE_COLORS[i] = Color.HSVToColor(new float[]{i, 1, 1});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Hue bar. */
|
||||||
|
private final Paint huePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
/** Saturation-value selector. */
|
||||||
|
private final Paint saturationValuePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
/** Draggable selector. */
|
||||||
|
private final Paint selectorPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
{
|
||||||
|
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bounds of the hue bar. */
|
||||||
|
private final RectF hueRect = new RectF();
|
||||||
|
/** Bounds of the saturation-value selector. */
|
||||||
|
private final RectF saturationValueRect = new RectF();
|
||||||
|
|
||||||
|
/** HSV color calculations to avoid allocations during drawing. */
|
||||||
|
private final float[] hsvArray = {1, 1, 1};
|
||||||
|
|
||||||
|
/** Current hue value (0-360). */
|
||||||
|
private float hue = 0f;
|
||||||
|
/** Current saturation value (0-1). */
|
||||||
|
private float saturation = 1f;
|
||||||
|
/** Current value (brightness) value (0-1). */
|
||||||
|
private float value = 1f;
|
||||||
|
|
||||||
|
/** The currently selected color in RGB format with no alpha channel. */
|
||||||
|
@ColorInt
|
||||||
|
private int selectedColor;
|
||||||
|
|
||||||
|
private OnColorChangedListener colorChangedListener;
|
||||||
|
|
||||||
|
/** Track if we're currently dragging the hue or saturation handle. */
|
||||||
|
private boolean isDraggingHue;
|
||||||
|
private boolean isDraggingSaturation;
|
||||||
|
|
||||||
|
public ColorPickerView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ColorPickerView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
final float DESIRED_ASPECT_RATIO = 0.8f; // height = width * 0.8
|
||||||
|
|
||||||
|
final int minWidth = Utils.dipToPixels(250);
|
||||||
|
final int minHeight = (int) (minWidth * DESIRED_ASPECT_RATIO);
|
||||||
|
|
||||||
|
int width = resolveSize(minWidth, widthMeasureSpec);
|
||||||
|
int height = resolveSize(minHeight, heightMeasureSpec);
|
||||||
|
|
||||||
|
// Ensure minimum dimensions for usability
|
||||||
|
width = Math.max(width, minWidth);
|
||||||
|
height = Math.max(height, minHeight);
|
||||||
|
|
||||||
|
// Adjust height to maintain desired aspect ratio if possible
|
||||||
|
final int desiredHeight = (int) (width * DESIRED_ASPECT_RATIO);
|
||||||
|
if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY) {
|
||||||
|
height = desiredHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMeasuredDimension(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the size of the view changes.
|
||||||
|
* This method calculates and sets the bounds of the hue bar and saturation-value selector.
|
||||||
|
* It also creates the necessary shaders for the gradients.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
|
||||||
|
super.onSizeChanged(width, height, oldWidth, oldHeight);
|
||||||
|
|
||||||
|
// Calculate bounds with hue bar on the right
|
||||||
|
final float effectiveWidth = width - (2 * VIEW_PADDING);
|
||||||
|
final float selectorWidth = effectiveWidth - HUE_BAR_WIDTH - MARGIN_BETWEEN_AREAS;
|
||||||
|
|
||||||
|
// Adjust rectangles to account for padding and density-independent dimensions
|
||||||
|
saturationValueRect.set(
|
||||||
|
VIEW_PADDING,
|
||||||
|
VIEW_PADDING,
|
||||||
|
VIEW_PADDING + selectorWidth,
|
||||||
|
height - VIEW_PADDING
|
||||||
|
);
|
||||||
|
|
||||||
|
hueRect.set(
|
||||||
|
width - VIEW_PADDING - HUE_BAR_WIDTH,
|
||||||
|
VIEW_PADDING,
|
||||||
|
width - VIEW_PADDING,
|
||||||
|
height - VIEW_PADDING
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update the shaders.
|
||||||
|
updateHueShader();
|
||||||
|
updateSaturationValueShader();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the hue full spectrum (0-360 degrees).
|
||||||
|
*/
|
||||||
|
private void updateHueShader() {
|
||||||
|
LinearGradient hueShader = new LinearGradient(
|
||||||
|
hueRect.left, hueRect.top,
|
||||||
|
hueRect.left, hueRect.bottom,
|
||||||
|
HUE_COLORS,
|
||||||
|
null,
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
huePaint.setShader(hueShader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the shader for the saturation-value selector based on the currently selected hue.
|
||||||
|
* This method creates a combined shader that blends a saturation gradient with a value gradient.
|
||||||
|
*/
|
||||||
|
private void updateSaturationValueShader() {
|
||||||
|
// Create a saturation-value gradient based on the current hue.
|
||||||
|
// Calculate the start color (white with the selected hue) for the saturation gradient.
|
||||||
|
final int startColor = Color.HSVToColor(new float[]{hue, 0f, 1f});
|
||||||
|
|
||||||
|
// Calculate the middle color (fully saturated color with the selected hue) for the saturation gradient.
|
||||||
|
final int midColor = Color.HSVToColor(new float[]{hue, 1f, 1f});
|
||||||
|
|
||||||
|
// Create a linear gradient for the saturation from startColor to midColor (horizontal).
|
||||||
|
LinearGradient satShader = new LinearGradient(
|
||||||
|
saturationValueRect.left, saturationValueRect.top,
|
||||||
|
saturationValueRect.right, saturationValueRect.top,
|
||||||
|
startColor,
|
||||||
|
midColor,
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a linear gradient for the value (brightness) from white to black (vertical).
|
||||||
|
//noinspection ExtractMethodRecommender
|
||||||
|
LinearGradient valShader = new LinearGradient(
|
||||||
|
saturationValueRect.left, saturationValueRect.top,
|
||||||
|
saturationValueRect.left, saturationValueRect.bottom,
|
||||||
|
Color.WHITE,
|
||||||
|
Color.BLACK,
|
||||||
|
Shader.TileMode.CLAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
// Combine the saturation and value shaders using PorterDuff.Mode.MULTIPLY to create the final color.
|
||||||
|
ComposeShader combinedShader = new ComposeShader(satShader, valShader, PorterDuff.Mode.MULTIPLY);
|
||||||
|
|
||||||
|
// Set the combined shader for the saturation-value paint.
|
||||||
|
saturationValuePaint.setShader(combinedShader);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the color picker view on the canvas.
|
||||||
|
* This method draws the saturation-value selector, the hue bar with rounded corners,
|
||||||
|
* and the draggable handles.
|
||||||
|
*
|
||||||
|
* @param canvas The canvas on which to draw.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
// Draw the saturation-value selector rectangle.
|
||||||
|
canvas.drawRect(saturationValueRect, saturationValuePaint);
|
||||||
|
|
||||||
|
// Draw the hue bar.
|
||||||
|
canvas.drawRoundRect(hueRect, HUE_CORNER_RADIUS, HUE_CORNER_RADIUS, huePaint);
|
||||||
|
|
||||||
|
final float hueSelectorX = hueRect.centerX();
|
||||||
|
final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height();
|
||||||
|
|
||||||
|
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||||
|
final float satSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||||
|
|
||||||
|
// Draw the saturation and hue selector handle filled with the selected color.
|
||||||
|
hsvArray[0] = hue;
|
||||||
|
final int hueHandleColor = Color.HSVToColor(0xFF, hsvArray);
|
||||||
|
selectorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
|
||||||
|
|
||||||
|
selectorPaint.setColor(hueHandleColor);
|
||||||
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||||
|
|
||||||
|
selectorPaint.setColor(selectedColor | 0xFF000000);
|
||||||
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_FILL_RADIUS, selectorPaint);
|
||||||
|
|
||||||
|
// Draw white outlines for the handles.
|
||||||
|
selectorPaint.setColor(SELECTOR_OUTLINE_COLOR);
|
||||||
|
selectorPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
selectorPaint.setStrokeWidth(SELECTOR_STROKE_WIDTH);
|
||||||
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||||
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_RADIUS, selectorPaint);
|
||||||
|
|
||||||
|
// Draw thin dark outlines for the handles at the outer edge of the white outline.
|
||||||
|
selectorPaint.setColor(SELECTOR_EDGE_COLOR);
|
||||||
|
selectorPaint.setStrokeWidth(SELECTOR_EDGE_STROKE_WIDTH);
|
||||||
|
canvas.drawCircle(hueSelectorX, hueSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||||
|
canvas.drawCircle(satSelectorX, satSelectorY, SELECTOR_EDGE_RADIUS, selectorPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles touch events on the view.
|
||||||
|
* This method determines whether the touch event occurred within the hue bar or the saturation-value selector,
|
||||||
|
* updates the corresponding values (hue, saturation, value), and invalidates the view to trigger a redraw.
|
||||||
|
* <p>
|
||||||
|
* In addition to testing if the touch is within the strict rectangles, an expanded hit area (by selectorRadius)
|
||||||
|
* is used so that the draggable handles remain active even when half of the handle is outside the drawn bounds.
|
||||||
|
*
|
||||||
|
* @param event The motion event.
|
||||||
|
* @return True if the event was handled, false otherwise.
|
||||||
|
*/
|
||||||
|
@SuppressLint("ClickableViewAccessibility") // performClick is not overridden, but not needed in this case.
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent event) {
|
||||||
|
try {
|
||||||
|
final float x = event.getX();
|
||||||
|
final float y = event.getY();
|
||||||
|
final int action = event.getAction();
|
||||||
|
Logger.printDebug(() -> "onTouchEvent action: " + action + " x: " + x + " y: " + y);
|
||||||
|
|
||||||
|
// Define touch expansion for the hue bar.
|
||||||
|
RectF expandedHueRect = new RectF(
|
||||||
|
hueRect.left - TOUCH_EXPANSION,
|
||||||
|
hueRect.top,
|
||||||
|
hueRect.right + TOUCH_EXPANSION,
|
||||||
|
hueRect.bottom
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
// Calculate current handle positions.
|
||||||
|
final float hueSelectorX = hueRect.centerX();
|
||||||
|
final float hueSelectorY = hueRect.top + (hue / 360f) * hueRect.height();
|
||||||
|
|
||||||
|
final float satSelectorX = saturationValueRect.left + saturation * saturationValueRect.width();
|
||||||
|
final float valSelectorY = saturationValueRect.top + (1 - value) * saturationValueRect.height();
|
||||||
|
|
||||||
|
// Create hit areas for both handles.
|
||||||
|
RectF hueHitRect = new RectF(
|
||||||
|
hueSelectorX - SELECTOR_RADIUS,
|
||||||
|
hueSelectorY - SELECTOR_RADIUS,
|
||||||
|
hueSelectorX + SELECTOR_RADIUS,
|
||||||
|
hueSelectorY + SELECTOR_RADIUS
|
||||||
|
);
|
||||||
|
RectF satValHitRect = new RectF(
|
||||||
|
satSelectorX - SELECTOR_RADIUS,
|
||||||
|
valSelectorY - SELECTOR_RADIUS,
|
||||||
|
satSelectorX + SELECTOR_RADIUS,
|
||||||
|
valSelectorY + SELECTOR_RADIUS
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if the touch started on a handle or within the expanded hue bar area.
|
||||||
|
if (hueHitRect.contains(x, y)) {
|
||||||
|
isDraggingHue = true;
|
||||||
|
updateHueFromTouch(y);
|
||||||
|
} else if (satValHitRect.contains(x, y)) {
|
||||||
|
isDraggingSaturation = true;
|
||||||
|
updateSaturationValueFromTouch(x, y);
|
||||||
|
} else if (expandedHueRect.contains(x, y)) {
|
||||||
|
// Handle touch within the expanded hue bar area.
|
||||||
|
isDraggingHue = true;
|
||||||
|
updateHueFromTouch(y);
|
||||||
|
} else if (saturationValueRect.contains(x, y)) {
|
||||||
|
isDraggingSaturation = true;
|
||||||
|
updateSaturationValueFromTouch(x, y);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
// Continue updating values even if touch moves outside the view.
|
||||||
|
if (isDraggingHue) {
|
||||||
|
updateHueFromTouch(y);
|
||||||
|
} else if (isDraggingSaturation) {
|
||||||
|
updateSaturationValueFromTouch(x, y);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
isDraggingHue = false;
|
||||||
|
isDraggingSaturation = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onTouchEvent failure", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the hue value based on touch position, clamping to valid range.
|
||||||
|
*
|
||||||
|
* @param y The y-coordinate of the touch position.
|
||||||
|
*/
|
||||||
|
private void updateHueFromTouch(float y) {
|
||||||
|
// Clamp y to the hue rectangle bounds.
|
||||||
|
final float clampedY = Utils.clamp(y, hueRect.top, hueRect.bottom);
|
||||||
|
final float updatedHue = ((clampedY - hueRect.top) / hueRect.height()) * 360f;
|
||||||
|
if (hue == updatedHue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
hue = updatedHue;
|
||||||
|
updateSaturationValueShader();
|
||||||
|
updateSelectedColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates saturation and value based on touch position, clamping to valid range.
|
||||||
|
*
|
||||||
|
* @param x The x-coordinate of the touch position.
|
||||||
|
* @param y The y-coordinate of the touch position.
|
||||||
|
*/
|
||||||
|
private void updateSaturationValueFromTouch(float x, float y) {
|
||||||
|
// Clamp x and y to the saturation-value rectangle bounds.
|
||||||
|
final float clampedX = Utils.clamp(x, saturationValueRect.left, saturationValueRect.right);
|
||||||
|
final float clampedY = Utils.clamp(y, saturationValueRect.top, saturationValueRect.bottom);
|
||||||
|
|
||||||
|
final float updatedSaturation = (clampedX - saturationValueRect.left) / saturationValueRect.width();
|
||||||
|
final float updatedValue = 1 - ((clampedY - saturationValueRect.top) / saturationValueRect.height());
|
||||||
|
|
||||||
|
if (saturation == updatedSaturation && value == updatedValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
saturation = updatedSaturation;
|
||||||
|
value = updatedValue;
|
||||||
|
updateSelectedColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the selected color and notifies listeners.
|
||||||
|
*/
|
||||||
|
private void updateSelectedColor() {
|
||||||
|
final int updatedColor = Color.HSVToColor(0, new float[]{hue, saturation, value});
|
||||||
|
|
||||||
|
if (selectedColor != updatedColor) {
|
||||||
|
selectedColor = updatedColor;
|
||||||
|
|
||||||
|
if (colorChangedListener != null) {
|
||||||
|
colorChangedListener.onColorChanged(updatedColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must always redraw, otherwise if saturation is pure grey or black
|
||||||
|
// then the hue slider cannot be changed.
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the currently selected color.
|
||||||
|
*
|
||||||
|
* @param color The color to set in either ARGB or RGB format.
|
||||||
|
*/
|
||||||
|
public void setColor(@ColorInt int color) {
|
||||||
|
color &= 0x00FFFFFF;
|
||||||
|
if (selectedColor == color) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the selected color.
|
||||||
|
selectedColor = color;
|
||||||
|
Logger.printDebug(() -> "setColor: " + getColorString(selectedColor));
|
||||||
|
|
||||||
|
// Convert the ARGB color to HSV values.
|
||||||
|
float[] hsv = new float[3];
|
||||||
|
Color.colorToHSV(color, hsv);
|
||||||
|
|
||||||
|
// Update the hue, saturation, and value.
|
||||||
|
hue = hsv[0];
|
||||||
|
saturation = hsv[1];
|
||||||
|
value = hsv[2];
|
||||||
|
|
||||||
|
// Update the saturation-value shader based on the new hue.
|
||||||
|
updateSaturationValueShader();
|
||||||
|
|
||||||
|
// Notify the listener if it's set.
|
||||||
|
if (colorChangedListener != null) {
|
||||||
|
colorChangedListener.onColorChanged(selectedColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate the view to trigger a redraw.
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the currently selected color.
|
||||||
|
*
|
||||||
|
* @return The selected color in RGB format with no alpha channel.
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
|
public int getColor() {
|
||||||
|
return selectedColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the listener to be notified when the selected color changes.
|
||||||
|
*
|
||||||
|
* @param listener The listener to set.
|
||||||
|
*/
|
||||||
|
public void setOnColorChangedListener(OnColorChangedListener listener) {
|
||||||
|
colorChangedListener = listener;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,7 +70,7 @@ public class ImportExportPreference extends EditTextPreference implements Prefer
|
|||||||
|
|
||||||
// Show the user the settings in JSON format.
|
// Show the user the settings in JSON format.
|
||||||
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
|
builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> {
|
||||||
Utils.setClipboard(getEditText().getText().toString());
|
Utils.setClipboard(getEditText().getText());
|
||||||
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
|
}).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> {
|
||||||
importSettings(builder.getContext(), getEditText().getText().toString());
|
importSettings(builder.getContext(), getEditText().getText().toString());
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages a buffer for storing debug logs from {@link Logger}.
|
||||||
|
* Stores just under 1MB of the most recent log data.
|
||||||
|
*
|
||||||
|
* All methods are thread-safe.
|
||||||
|
*/
|
||||||
|
public final class LogBufferManager {
|
||||||
|
/** Maximum byte size of all buffer entries. Must be less than Android's 1 MB Binder transaction limit. */
|
||||||
|
private static final int BUFFER_MAX_BYTES = 900_000;
|
||||||
|
/** Limit number of log lines. */
|
||||||
|
private static final int BUFFER_MAX_SIZE = 10_000;
|
||||||
|
|
||||||
|
private static final Deque<String> logBuffer = new ConcurrentLinkedDeque<>();
|
||||||
|
private static final AtomicInteger logBufferByteSize = new AtomicInteger();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends a log message to the internal buffer if debugging is enabled.
|
||||||
|
* The buffer is limited to approximately {@link #BUFFER_MAX_BYTES} or {@link #BUFFER_MAX_SIZE}
|
||||||
|
* to prevent excessive memory usage.
|
||||||
|
*
|
||||||
|
* @param message The log message to append.
|
||||||
|
*/
|
||||||
|
public static void appendToLogBuffer(String message) {
|
||||||
|
Objects.requireNonNull(message);
|
||||||
|
|
||||||
|
// It's very important that no Settings are used in this method,
|
||||||
|
// as this code is used when a context is not set and thus referencing
|
||||||
|
// a setting will crash the app.
|
||||||
|
logBuffer.addLast(message);
|
||||||
|
int newSize = logBufferByteSize.addAndGet(message.length());
|
||||||
|
|
||||||
|
// Remove oldest entries if over the log size limits.
|
||||||
|
while (newSize > BUFFER_MAX_BYTES || logBuffer.size() > BUFFER_MAX_SIZE) {
|
||||||
|
String removed = logBuffer.pollFirst();
|
||||||
|
if (removed == null) {
|
||||||
|
// Thread race of two different calls to this method, and the other thread won.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
newSize = logBufferByteSize.addAndGet(-removed.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports all logs from the internal buffer to the clipboard.
|
||||||
|
* Displays a toast with the result.
|
||||||
|
*/
|
||||||
|
public static void exportToClipboard() {
|
||||||
|
try {
|
||||||
|
if (!BaseSettings.DEBUG.get()) {
|
||||||
|
Utils.showToastShort(str("revanced_debug_logs_disabled"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logBuffer.isEmpty()) {
|
||||||
|
Utils.showToastShort(str("revanced_debug_logs_none_found"));
|
||||||
|
clearLogBufferData(); // Clear toast log entry that was just created.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Most (but not all) Android 13+ devices always show a "copied to clipboard" toast
|
||||||
|
// and there is no way to programmatically detect if a toast will show or not.
|
||||||
|
// Show a toast even if using Android 13+, but show ReVanced toast first (before copying to clipboard).
|
||||||
|
Utils.showToastShort(str("revanced_debug_logs_copied_to_clipboard"));
|
||||||
|
|
||||||
|
Utils.setClipboard(String.join("\n", logBuffer));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Handle security exception if clipboard access is denied.
|
||||||
|
String errorMessage = String.format(str("revanced_debug_logs_failed_to_export"), ex.getMessage());
|
||||||
|
Utils.showToastLong(errorMessage);
|
||||||
|
Logger.printDebug(() -> errorMessage, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void clearLogBufferData() {
|
||||||
|
// Cannot simply clear the log buffer because there is no
|
||||||
|
// write lock for both the deque and the atomic int.
|
||||||
|
// Instead pop off log entries and decrement the size one by one.
|
||||||
|
while (!logBuffer.isEmpty()) {
|
||||||
|
String removed = logBuffer.pollFirst();
|
||||||
|
if (removed != null) {
|
||||||
|
logBufferByteSize.addAndGet(-removed.length());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the internal log buffer and displays a toast with the result.
|
||||||
|
*/
|
||||||
|
public static void clearLogBuffer() {
|
||||||
|
if (!BaseSettings.DEBUG.get()) {
|
||||||
|
Utils.showToastShort(str("revanced_debug_logs_disabled"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show toast before clearing, otherwise toast log will still remain.
|
||||||
|
Utils.showToastShort(str("revanced_debug_logs_clear_toast"));
|
||||||
|
clearLogBufferData();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,10 @@ public class NoTitlePreferenceCategory extends PreferenceCategory {
|
|||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NoTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
public NoTitlePreferenceCategory(Context context) {
|
public NoTitlePreferenceCategory(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import android.app.Dialog;
|
|||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -54,7 +53,7 @@ public class ReVancedAboutPreference extends Preference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isDarkModeEnabled() {
|
protected boolean isDarkModeEnabled() {
|
||||||
return Utils.isDarkModeEnabled(getContext());
|
return Utils.isDarkModeEnabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package app.revanced.extension.shared.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.ListPreference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Pair;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PreferenceList that sorts itself.
|
||||||
|
* By default the first entry is preserved in its original position,
|
||||||
|
* and all other entries are sorted alphabetically.
|
||||||
|
*
|
||||||
|
* Ideally the 'keep first entries to preserve' is an xml parameter,
|
||||||
|
* but currently that's not so simple since Extensions code cannot use
|
||||||
|
* generated code from the Patches repo (which is required for custom xml parameters).
|
||||||
|
*
|
||||||
|
* If any class wants to use a different getFirstEntriesToPreserve value,
|
||||||
|
* it needs to subclass this preference and override {@link #getFirstEntriesToPreserve}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class SortedListPreference extends ListPreference {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sorts the current list entries.
|
||||||
|
*
|
||||||
|
* @param firstEntriesToPreserve The number of entries to preserve in their original position.
|
||||||
|
*/
|
||||||
|
public void sortEntryAndValues(int firstEntriesToPreserve) {
|
||||||
|
CharSequence[] entries = getEntries();
|
||||||
|
CharSequence[] entryValues = getEntryValues();
|
||||||
|
if (entries == null || entryValues == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int entrySize = entries.length;
|
||||||
|
if (entrySize != entryValues.length) {
|
||||||
|
// Xml array declaration has a missing/extra entry.
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Pair<CharSequence, CharSequence>> firstEntries = new ArrayList<>(firstEntriesToPreserve);
|
||||||
|
|
||||||
|
// Android does not have a triple class like Kotlin, So instead use a nested pair.
|
||||||
|
// Cannot easily use a SortedMap, because if two entries incorrectly have
|
||||||
|
// identical names then the duplicates entries are not preserved.
|
||||||
|
List<Pair<String, Pair<CharSequence, CharSequence>>> lastEntries = new ArrayList<>();
|
||||||
|
|
||||||
|
for (int i = 0; i < entrySize; i++) {
|
||||||
|
Pair<CharSequence, CharSequence> pair = new Pair<>(entries[i], entryValues[i]);
|
||||||
|
if (i < firstEntriesToPreserve) {
|
||||||
|
firstEntries.add(pair);
|
||||||
|
} else {
|
||||||
|
lastEntries.add(new Pair<>(Utils.removePunctuationToLowercase(pair.first), pair));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//noinspection ComparatorCombinators
|
||||||
|
Collections.sort(lastEntries, (pair1, pair2)
|
||||||
|
-> pair1.first.compareTo(pair2.first));
|
||||||
|
|
||||||
|
CharSequence[] sortedEntries = new CharSequence[entrySize];
|
||||||
|
CharSequence[] sortedEntryValues = new CharSequence[entrySize];
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (Pair<CharSequence, CharSequence> pair : firstEntries) {
|
||||||
|
sortedEntries[i] = pair.first;
|
||||||
|
sortedEntryValues[i] = pair.second;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (Pair<String, Pair<CharSequence, CharSequence>> outer : lastEntries) {
|
||||||
|
Pair<CharSequence, CharSequence> inner = outer.second;
|
||||||
|
sortedEntries[i] = inner.first;
|
||||||
|
sortedEntryValues[i] = inner.second;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
super.setEntries(sortedEntries);
|
||||||
|
super.setEntryValues(sortedEntryValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getFirstEntriesToPreserve() {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SortedListPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
|
||||||
|
sortEntryAndValues(getFirstEntriesToPreserve());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -71,9 +71,7 @@ final class PlayerRoutes {
|
|||||||
return innerTubeBody.toString();
|
return innerTubeBody.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@SuppressWarnings("SameParameterValue")
|
||||||
* @noinspection SameParameterValue
|
|
||||||
*/
|
|
||||||
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
|
static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException {
|
||||||
var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
|
var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route);
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_11
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_11
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package app.revanced.extension.spotify.layout.hide.createbutton;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class HideCreateButtonPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of ids of resources which contain the Create button title.
|
||||||
|
*/
|
||||||
|
private static final List<String> CREATE_BUTTON_TITLE_RES_ID_LIST = List.of(
|
||||||
|
Integer.toString(Utils.getResourceIdentifier("navigationbar_musicappitems_create_title", "string"))
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The old id of the resource which contained the Create button title. Used in older versions of the app.
|
||||||
|
*/
|
||||||
|
private static final int OLD_CREATE_BUTTON_TITLE_RES_ID =
|
||||||
|
Utils.getResourceIdentifier("bottom_navigation_bar_create_tab_title", "string");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point. This method is called on every navigation bar item to check whether it is the Create button.
|
||||||
|
* If the navigation bar item is the Create button, it returns null to erase it.
|
||||||
|
* The method fingerprint used to patch ensures we can safely return null here.
|
||||||
|
*/
|
||||||
|
public static Object returnNullIfIsCreateButton(Object navigationBarItem) {
|
||||||
|
if (navigationBarItem == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String stringifiedNavigationBarItem = navigationBarItem.toString();
|
||||||
|
boolean isCreateButton = CREATE_BUTTON_TITLE_RES_ID_LIST.stream()
|
||||||
|
.anyMatch(stringifiedNavigationBarItem::contains);
|
||||||
|
|
||||||
|
if (isCreateButton) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return navigationBarItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point. Called in older versions of the app. Returns whether the old navigation bar item is the old
|
||||||
|
* Create button.
|
||||||
|
*/
|
||||||
|
public static boolean isOldCreateButton(int oldNavigationBarItemTitleResId) {
|
||||||
|
return oldNavigationBarItemTitleResId == OLD_CREATE_BUTTON_TITLE_RES_ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,11 +7,11 @@ android {
|
|||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = 26
|
minSdk = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_17
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package app.revanced.extension.tiktok;
|
|||||||
|
|
||||||
import static app.revanced.extension.shared.Utils.isDarkModeEnabled;
|
import static app.revanced.extension.shared.Utils.isDarkModeEnabled;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -43,8 +42,8 @@ public class Utils {
|
|||||||
private static final @ColorInt int TEXT_LIGHT_MODE_SUMMARY
|
private static final @ColorInt int TEXT_LIGHT_MODE_SUMMARY
|
||||||
= Color.argb(255, 80, 80, 80);
|
= Color.argb(255, 80, 80, 80);
|
||||||
|
|
||||||
public static void setTitleAndSummaryColor(Context context, View view) {
|
public static void setTitleAndSummaryColor(View view) {
|
||||||
final boolean darkModeEnabled = isDarkModeEnabled(context);
|
final boolean darkModeEnabled = isDarkModeEnabled();
|
||||||
|
|
||||||
TextView title = view.findViewById(android.R.id.title);
|
TextView title = view.findViewById(android.R.id.title);
|
||||||
title.setTextColor(darkModeEnabled
|
title.setTextColor(darkModeEnabled
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ public class DownloadPathPreference extends DialogPreference {
|
|||||||
protected void onBindView(View view) {
|
protected void onBindView(View view) {
|
||||||
super.onBindView(view);
|
super.onBindView(view);
|
||||||
|
|
||||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
Utils.setTitleAndSummaryColor(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ public class InputTextPreference extends EditTextPreference {
|
|||||||
protected void onBindView(View view) {
|
protected void onBindView(View view) {
|
||||||
super.onBindView(view);
|
super.onBindView(view);
|
||||||
|
|
||||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
Utils.setTitleAndSummaryColor(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ public class RangeValuePreference extends DialogPreference {
|
|||||||
protected void onBindView(View view) {
|
protected void onBindView(View view) {
|
||||||
super.onBindView(view);
|
super.onBindView(view);
|
||||||
|
|
||||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
Utils.setTitleAndSummaryColor(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -48,6 +48,6 @@ public class ReVancedTikTokAboutPreference extends ReVancedAboutPreference {
|
|||||||
protected void onBindView(View view) {
|
protected void onBindView(View view) {
|
||||||
super.onBindView(view);
|
super.onBindView(view);
|
||||||
|
|
||||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
Utils.setTitleAndSummaryColor(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,6 @@ public class TogglePreference extends SwitchPreference {
|
|||||||
protected void onBindView(View view) {
|
protected void onBindView(View view) {
|
||||||
super.onBindView(view);
|
super.onBindView(view);
|
||||||
|
|
||||||
Utils.setTitleAndSummaryColor(getContext(), view);
|
Utils.setTitleAndSummaryColor(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ public class SpoofSimPatch {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.initializationException(SpoofSimPatch.class,
|
Logger.initializationException(() -> "Context is not yet set, cannot spoof: " + fieldSpoofed, null);
|
||||||
"Context is not yet set, cannot spoof: " + fieldSpoofed, null);
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
package app.revanced.extension.youtube;
|
package app.revanced.extension.youtube;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.Utils.clamp;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.text.style.ReplacementSpan;
|
||||||
|
import android.text.TextPaint;
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
@@ -97,6 +105,14 @@ public class ThemeHelper {
|
|||||||
return isDarkTheme() ? getLightThemeColor() : getDarkThemeColor();
|
return isDarkTheme() ? getLightThemeColor() : getDarkThemeColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int getDialogBackgroundColor() {
|
||||||
|
final String colorName = isDarkTheme()
|
||||||
|
? "yt_black1"
|
||||||
|
: "yt_white1";
|
||||||
|
|
||||||
|
return Utils.getColorFromString(colorName);
|
||||||
|
}
|
||||||
|
|
||||||
public static int getToolbarBackgroundColor() {
|
public static int getToolbarBackgroundColor() {
|
||||||
final String colorName = isDarkTheme()
|
final String colorName = isDarkTheme()
|
||||||
? "yt_black3"
|
? "yt_black3"
|
||||||
@@ -121,4 +137,43 @@ public class ThemeHelper {
|
|||||||
window.setNavigationBarContrastEnforced(true);
|
window.setNavigationBarContrastEnforced(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts the brightness of a color by lightening or darkening it based on the given factor.
|
||||||
|
* <p>
|
||||||
|
* If the factor is greater than 1, the color is lightened by interpolating toward white (#FFFFFF).
|
||||||
|
* If the factor is less than or equal to 1, the color is darkened by scaling its RGB components toward black (#000000).
|
||||||
|
* The alpha channel remains unchanged.
|
||||||
|
*
|
||||||
|
* @param color The input color to adjust, in ARGB format.
|
||||||
|
* @param factor The adjustment factor. Use values > 1.0f to lighten (e.g., 1.11f for slight lightening)
|
||||||
|
* or values <= 1.0f to darken (e.g., 0.95f for slight darkening).
|
||||||
|
* @return The adjusted color in ARGB format.
|
||||||
|
*/
|
||||||
|
public static int adjustColorBrightness(int color, float factor) {
|
||||||
|
final int alpha = Color.alpha(color);
|
||||||
|
int red = Color.red(color);
|
||||||
|
int green = Color.green(color);
|
||||||
|
int blue = Color.blue(color);
|
||||||
|
|
||||||
|
if (factor > 1.0f) {
|
||||||
|
// Lighten: Interpolate toward white (255)
|
||||||
|
final float t = 1.0f - (1.0f / factor); // Interpolation parameter
|
||||||
|
red = Math.round(red + (255 - red) * t);
|
||||||
|
green = Math.round(green + (255 - green) * t);
|
||||||
|
blue = Math.round(blue + (255 - blue) * t);
|
||||||
|
} else {
|
||||||
|
// Darken or no change: Scale toward black
|
||||||
|
red = (int) (red * factor);
|
||||||
|
green = (int) (green * factor);
|
||||||
|
blue = (int) (blue * factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure values are within [0, 255]
|
||||||
|
red = clamp(red, 0, 255);
|
||||||
|
green = clamp(green, 0, 255);
|
||||||
|
blue = clamp(blue, 0, 255);
|
||||||
|
|
||||||
|
return Color.argb(alpha, red, green, blue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -686,7 +686,7 @@ public final class AlternativeThumbnailsPatch {
|
|||||||
? "" : fullUrl.substring(imageExtensionEndIndex);
|
? "" : fullUrl.substring(imageExtensionEndIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @noinspection SameParameterValue */
|
@SuppressWarnings("SameParameterValue")
|
||||||
String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) {
|
String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) {
|
||||||
// Images could be upgraded to webp if they are not already, but this fails quite often,
|
// Images could be upgraded to webp if they are not already, but this fails quite often,
|
||||||
// especially for new videos uploaded in the last hour.
|
// especially for new videos uploaded in the last hour.
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class DisableHapticFeedbackPatch {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean disableChapterVibrate() {
|
||||||
|
return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean disableSeekUndoVibrate() {
|
||||||
|
return Settings.DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean disablePreciseSeekingVibrate() {
|
||||||
|
return Settings.DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean disableZoomVibrate() {
|
||||||
|
return Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public final class HideRelatedVideoOverlayPatch {
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean hideRelatedVideoOverlay() {
|
||||||
|
return Settings.HIDE_RELATED_VIDEO_OVERLAY.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -95,7 +95,7 @@ public final class NavigationButtonsPatch {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Utils.isDarkModeEnabled(Utils.getContext())
|
return Utils.isDarkModeEnabled()
|
||||||
? !DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
|
? !DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK
|
||||||
: !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT;
|
: !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ public class OpenShortsInRegularPlayerPatch {
|
|||||||
|
|
||||||
private static WeakReference<Activity> mainActivityRef = new WeakReference<>(null);
|
private static WeakReference<Activity> mainActivityRef = new WeakReference<>(null);
|
||||||
|
|
||||||
|
private static volatile boolean overrideBackPressToExit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@@ -38,6 +40,18 @@ public class OpenShortsInRegularPlayerPatch {
|
|||||||
mainActivityRef = new WeakReference<>(activity);
|
mainActivityRef = new WeakReference<>(activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*/
|
||||||
|
public static boolean overrideBackPressToExit(boolean original) {
|
||||||
|
if (overrideBackPressToExit) {
|
||||||
|
Logger.printDebug(() -> "Overriding back press to exit activity");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@@ -45,6 +59,7 @@ public class OpenShortsInRegularPlayerPatch {
|
|||||||
try {
|
try {
|
||||||
ShortsPlayerType type = Settings.SHORTS_PLAYER_TYPE.get();
|
ShortsPlayerType type = Settings.SHORTS_PLAYER_TYPE.get();
|
||||||
if (type == ShortsPlayerType.SHORTS_PLAYER) {
|
if (type == ShortsPlayerType.SHORTS_PLAYER) {
|
||||||
|
overrideBackPressToExit = false;
|
||||||
return false; // Default unpatched behavior.
|
return false; // Default unpatched behavior.
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,13 +76,17 @@ public class OpenShortsInRegularPlayerPatch {
|
|||||||
// set to open in the regular player, so it's ignored as
|
// set to open in the regular player, so it's ignored as
|
||||||
// checking the map makes the patch more complicated.
|
// checking the map makes the patch more complicated.
|
||||||
Logger.printDebug(() -> "Ignoring Short with no videoId");
|
Logger.printDebug(() -> "Ignoring Short with no videoId");
|
||||||
|
overrideBackPressToExit = false;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NavigationButton.getSelectedNavigationButton() == NavigationButton.SHORTS) {
|
if (NavigationButton.getSelectedNavigationButton() == NavigationButton.SHORTS) {
|
||||||
|
overrideBackPressToExit = false;
|
||||||
return false; // Always use Shorts player for the Shorts nav button.
|
return false; // Always use Shorts player for the Shorts nav button.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
overrideBackPressToExit = true;
|
||||||
|
|
||||||
final boolean forceFullScreen = (type == ShortsPlayerType.REGULAR_PLAYER_FULLSCREEN);
|
final boolean forceFullScreen = (type == ShortsPlayerType.REGULAR_PLAYER_FULLSCREEN);
|
||||||
OpenVideosFullscreenHookPatch.setOpenNextVideoFullscreen(forceFullScreen);
|
OpenVideosFullscreenHookPatch.setOpenNextVideoFullscreen(forceFullScreen);
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public class OpenVideosFullscreenHookPatch {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isFullScreenPatchIncluded()) {
|
if (!isFullScreenPatchIncluded()) {
|
||||||
return false;
|
return original;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Settings.OPEN_VIDEOS_FULLSCREEN_PORTRAIT.get();
|
return Settings.OPEN_VIDEOS_FULLSCREEN_PORTRAIT.get();
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch;
|
import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch;
|
||||||
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
||||||
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
import app.revanced.extension.youtube.shared.PlayerType;
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
|
|
||||||
@@ -69,13 +68,6 @@ public class ReturnYouTubeDislikePatch {
|
|||||||
@Nullable
|
@Nullable
|
||||||
private static volatile String lastPrefetchedVideoId;
|
private static volatile String lastPrefetchedVideoId;
|
||||||
|
|
||||||
public static void onRYDStatusChange(boolean rydEnabled) {
|
|
||||||
ReturnYouTubeDislikeApi.resetRateLimits();
|
|
||||||
// Must remove all values to protect against using stale data
|
|
||||||
// if the user enables RYD while a video is on screen.
|
|
||||||
clearData();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void clearData() {
|
private static void clearData() {
|
||||||
currentVideoData = null;
|
currentVideoData = null;
|
||||||
lastLithoShortsVideoData = null;
|
lastLithoShortsVideoData = null;
|
||||||
@@ -160,11 +152,15 @@ public class ReturnYouTubeDislikePatch {
|
|||||||
return original; // No need to check for Shorts in the context.
|
return original; // No need to check for Shorts in the context.
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conversionContextString.contains("|shorts_dislike_button.eml")) {
|
if (Utils.containsAny(conversionContextString,
|
||||||
|
"|shorts_dislike_button.eml", "|reel_dislike_button.eml"
|
||||||
|
)) {
|
||||||
return getShortsSpan(original, true);
|
return getShortsSpan(original, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (conversionContextString.contains("|shorts_like_button.eml")) {
|
if (Utils.containsAny(conversionContextString,
|
||||||
|
"|shorts_like_button.eml", "|reel_like_button.eml"
|
||||||
|
)) {
|
||||||
if (!Utils.containsNumber(original)) {
|
if (!Utils.containsNumber(original)) {
|
||||||
Logger.printDebug(() -> "Replacing hidden likes count");
|
Logger.printDebug(() -> "Replacing hidden likes count");
|
||||||
return getShortsSpan(original, false);
|
return getShortsSpan(original, false);
|
||||||
@@ -274,7 +270,7 @@ public class ReturnYouTubeDislikePatch {
|
|||||||
Logger.printDebug(() -> "Adding rolling number TextView changes");
|
Logger.printDebug(() -> "Adding rolling number TextView changes");
|
||||||
view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels);
|
view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels);
|
||||||
ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable();
|
ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable();
|
||||||
if (Utils.isRightToLeftTextLayout()) {
|
if (Utils.isRightToLeftLocale()) {
|
||||||
view.setCompoundDrawables(null, null, separator, null);
|
view.setCompoundDrawables(null, null, separator, null);
|
||||||
} else {
|
} else {
|
||||||
view.setCompoundDrawables(separator, null, null, null);
|
view.setCompoundDrawables(separator, null, null, null);
|
||||||
@@ -369,6 +365,11 @@ public class ReturnYouTubeDislikePatch {
|
|||||||
if (videoId.equals(lastPrefetchedVideoId)) {
|
if (videoId.equals(lastPrefetchedVideoId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!Utils.isNetworkConnected()) {
|
||||||
|
Logger.printDebug(() -> "Cannot pre-fetch RYD, network is not connected");
|
||||||
|
lastPrefetchedVideoId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort();
|
final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort();
|
||||||
// Shorts shelf in home and subscription feed causes player response hook to be called,
|
// Shorts shelf in home and subscription feed causes player response hook to be called,
|
||||||
@@ -423,6 +424,12 @@ public class ReturnYouTubeDislikePatch {
|
|||||||
}
|
}
|
||||||
Logger.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType);
|
Logger.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType);
|
||||||
|
|
||||||
|
if (!Utils.isNetworkConnected()) {
|
||||||
|
Logger.printDebug(() -> "Cannot fetch RYD, network is not connected");
|
||||||
|
currentVideoData = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ReturnYouTubeDislike data = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
ReturnYouTubeDislike data = ReturnYouTubeDislike.getFetchForVideoId(videoId);
|
||||||
// Pre-emptively set the data to short status.
|
// Pre-emptively set the data to short status.
|
||||||
// Required to prevent Shorts data from being used on a minimized video in incognito mode.
|
// Required to prevent Shorts data from being used on a minimized video in incognito mode.
|
||||||
|
|||||||
@@ -354,4 +354,23 @@ public final class VideoInformation {
|
|||||||
return videoTime >= videoLength && videoLength > 0;
|
return videoTime >= videoLength && videoLength > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overrides the current playback speed.
|
||||||
|
* Rest of the implementation added by patch.
|
||||||
|
*/
|
||||||
|
public static void overridePlaybackSpeed(float speedOverride) {
|
||||||
|
Logger.printDebug(() -> "Overriding playback speed to: " + speedOverride);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Injection point.
|
||||||
|
*
|
||||||
|
* @param newlyLoadedPlaybackSpeed The current playback speed.
|
||||||
|
*/
|
||||||
|
public static void setPlaybackSpeed(float newlyLoadedPlaybackSpeed) {
|
||||||
|
if (playbackSpeed != newlyLoadedPlaybackSpeed) {
|
||||||
|
Logger.printDebug(() -> "Video speed changed: " + newlyLoadedPlaybackSpeed);
|
||||||
|
playbackSpeed = newlyLoadedPlaybackSpeed;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
package app.revanced.extension.youtube.patches;
|
||||||
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
@@ -33,10 +31,9 @@ public final class WideSearchbarPatch {
|
|||||||
final int paddingRight = searchBarView.getPaddingRight();
|
final int paddingRight = searchBarView.getPaddingRight();
|
||||||
final int paddingTop = searchBarView.getPaddingTop();
|
final int paddingTop = searchBarView.getPaddingTop();
|
||||||
final int paddingBottom = searchBarView.getPaddingBottom();
|
final int paddingBottom = searchBarView.getPaddingBottom();
|
||||||
final int paddingStart = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
|
final int paddingStart = Utils.dipToPixels(8);
|
||||||
8, Resources.getSystem().getDisplayMetrics());
|
|
||||||
|
|
||||||
if (Utils.isRightToLeftTextLayout()) {
|
if (Utils.isRightToLeftLocale()) {
|
||||||
searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom);
|
searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom);
|
||||||
} else {
|
} else {
|
||||||
searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom);
|
searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom);
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.patches;
|
|
||||||
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
|
||||||
public class ZoomHapticsPatch {
|
|
||||||
public static boolean shouldVibrate() {
|
|
||||||
return !Settings.DISABLE_ZOOM_HAPTICS.get();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -64,48 +64,45 @@ public final class AdsFilter extends Filter {
|
|||||||
"_interstitial"
|
"_interstitial"
|
||||||
);
|
);
|
||||||
|
|
||||||
final var buttonedAd = new StringFilterGroup(
|
|
||||||
Settings.HIDE_BUTTONED_ADS,
|
|
||||||
"_ad_with",
|
|
||||||
"_buttoned_layout",
|
|
||||||
// text_image_button_group_layout, landscape_image_button_group_layout, full_width_square_image_button_group_layout
|
|
||||||
"image_button_group_layout",
|
|
||||||
"full_width_square_image_layout",
|
|
||||||
"video_display_button_group_layout",
|
|
||||||
"landscape_image_wide_button_layout",
|
|
||||||
"video_display_carousel_button_group_layout",
|
|
||||||
"video_display_full_buttoned_short_dr_layout",
|
|
||||||
"compact_landscape_image_layout", // Tablet layout search results.
|
|
||||||
"text_image_no_button_layout" // Tablet layout search results.
|
|
||||||
);
|
|
||||||
|
|
||||||
final var generalAds = new StringFilterGroup(
|
final var generalAds = new StringFilterGroup(
|
||||||
Settings.HIDE_GENERAL_ADS,
|
Settings.HIDE_GENERAL_ADS,
|
||||||
|
"_ad_with",
|
||||||
|
"_buttoned_layout",
|
||||||
"ads_video_with_context",
|
"ads_video_with_context",
|
||||||
"banner_text_icon",
|
"banner_text_icon",
|
||||||
"square_image_layout",
|
"brand_video_shelf",
|
||||||
"watch_metadata_app_promo",
|
"brand_video_singleton",
|
||||||
"video_display_full_layout",
|
|
||||||
"hero_promo_image",
|
|
||||||
"statement_banner",
|
|
||||||
"carousel_footered_layout",
|
"carousel_footered_layout",
|
||||||
"text_image_button_layout",
|
"carousel_headered_layout",
|
||||||
|
"compact_landscape_image_layout", // Tablet layout search results.
|
||||||
|
"composite_concurrent_carousel_layout",
|
||||||
|
"full_width_portrait_image_layout",
|
||||||
|
"full_width_square_image_carousel_layout",
|
||||||
|
"full_width_square_image_layout",
|
||||||
|
"hero_promo_image",
|
||||||
|
// text_image_button_group_layout, landscape_image_button_group_layout, full_width_square_image_button_group_layout
|
||||||
|
"image_button_group_layout",
|
||||||
|
"landscape_image_wide_button_layout",
|
||||||
"primetime_promo",
|
"primetime_promo",
|
||||||
"product_details",
|
"product_details",
|
||||||
"composite_concurrent_carousel_layout",
|
"square_image_layout",
|
||||||
"carousel_headered_layout",
|
"statement_banner",
|
||||||
"full_width_portrait_image_layout",
|
"text_image_button_layout",
|
||||||
"brand_video_shelf",
|
"text_image_no_button_layout", // Tablet layout search results.
|
||||||
"brand_video_singleton"
|
"video_display_button_group_layout",
|
||||||
|
"video_display_carousel_button_group_layout",
|
||||||
|
"video_display_full_buttoned_short_dr_layout",
|
||||||
|
"video_display_full_layout",
|
||||||
|
"watch_metadata_app_promo"
|
||||||
);
|
);
|
||||||
|
|
||||||
final var movieAds = new StringFilterGroup(
|
final var movieAds = new StringFilterGroup(
|
||||||
Settings.HIDE_MOVIES_SECTION,
|
Settings.HIDE_MOVIES_SECTION,
|
||||||
"browsy_bar",
|
"browsy_bar",
|
||||||
"compact_movie",
|
"compact_movie",
|
||||||
|
"compact_tvfilm_item",
|
||||||
"horizontal_movie_shelf",
|
"horizontal_movie_shelf",
|
||||||
"movie_and_show_upsell_card",
|
"movie_and_show_upsell_card",
|
||||||
"compact_tvfilm_item",
|
|
||||||
"offer_module_root"
|
"offer_module_root"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -160,7 +157,6 @@ public final class AdsFilter extends Filter {
|
|||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
generalAds,
|
generalAds,
|
||||||
buttonedAd,
|
|
||||||
merchandise,
|
merchandise,
|
||||||
viewProducts,
|
viewProducts,
|
||||||
selfSponsor,
|
selfSponsor,
|
||||||
@@ -177,34 +173,30 @@ public final class AdsFilter extends Filter {
|
|||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == playerShoppingShelf) {
|
if (matchedGroup == playerShoppingShelf) {
|
||||||
if (contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered()) {
|
return contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for the index because of likelihood of false positives.
|
// Check for the index because of likelihood of false positives.
|
||||||
if (matchedGroup == shoppingLinks && contentIndex != 0) {
|
if (contentIndex != 0 && matchedGroup == shoppingLinks) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exceptions.matches(path))
|
if (exceptions.matches(path)) {
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (matchedGroup == fullscreenAd) {
|
if (matchedGroup == fullscreenAd) {
|
||||||
if (path.contains("|ImageType|")) closeFullscreenAd();
|
if (path.contains("|ImageType|")) closeFullscreenAd();
|
||||||
|
|
||||||
return false; // Do not actually filter the fullscreen ad otherwise it will leave a dimmed screen.
|
// Do not actually filter the fullscreen ad otherwise it will leave a dimmed screen.
|
||||||
}
|
|
||||||
|
|
||||||
if (matchedGroup == channelProfile) {
|
|
||||||
if (visitStoreButton.check(protobufBufferArray).isFiltered()) {
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
if (matchedGroup == channelProfile) {
|
||||||
|
return visitStoreButton.check(protobufBufferArray).isFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -99,29 +99,23 @@ final class ButtonsFilter extends Filter {
|
|||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == likeSubscribeGlow) {
|
if (matchedGroup == likeSubscribeGlow) {
|
||||||
if ((path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
|
return (path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
|
||||||
&& path.contains(ANIMATED_VECTOR_TYPE_PATH)) {
|
&& path.contains(ANIMATED_VECTOR_TYPE_PATH);
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the current matched group is the action bar group,
|
// If the current matched group is the action bar group,
|
||||||
// in case every filter group is enabled, hide the action bar.
|
// in case every filter group is enabled, hide the action bar.
|
||||||
if (matchedGroup == actionBarGroup) {
|
if (matchedGroup == actionBarGroup) {
|
||||||
if (!isEveryFilterGroupEnabled()) {
|
return isEveryFilterGroupEnabled();
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} else if (matchedGroup == bufferFilterPathGroup) {
|
|
||||||
// Make sure the current path is the right one
|
|
||||||
// to avoid false positives.
|
|
||||||
if (!path.startsWith(VIDEO_ACTION_BAR_PATH)) return false;
|
|
||||||
|
|
||||||
// In case the group list has no match, return false.
|
|
||||||
if (!bufferButtonsGroupList.check(protobufBufferArray).isFiltered()) return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
if (matchedGroup == bufferFilterPathGroup) {
|
||||||
|
// Make sure the current path is the right one
|
||||||
|
// to avoid false positives.
|
||||||
|
return path.startsWith(VIDEO_ACTION_BAR_PATH)
|
||||||
|
&& bufferButtonsGroupList.check(protobufBufferArray).isFiltered();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,22 +88,15 @@ final class CommentsFilter extends Filter {
|
|||||||
if (matchedGroup == commentComposer) {
|
if (matchedGroup == commentComposer) {
|
||||||
// To completely hide the emoji buttons (and leave no empty space), the timestamp button is
|
// To completely hide the emoji buttons (and leave no empty space), the timestamp button is
|
||||||
// also hidden because the buffer is exactly the same and there's no way selectively hide.
|
// also hidden because the buffer is exactly the same and there's no way selectively hide.
|
||||||
if (contentIndex == 0
|
return contentIndex == 0
|
||||||
&& path.endsWith(TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH)
|
&& path.endsWith(TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH)
|
||||||
&& emojiPickerBufferGroup.check(protobufBufferArray).isFiltered()) {
|
&& emojiPickerBufferGroup.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == filterChipBar) {
|
if (matchedGroup == filterChipBar) {
|
||||||
if (aiCommentsSummary.check(protobufBufferArray).isFiltered()) {
|
return aiCommentsSummary.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,9 +153,11 @@ final class CustomFilter extends Filter {
|
|||||||
if (custom.startsWith && contentIndex != 0) {
|
if (custom.startsWith && contentIndex != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) {
|
|
||||||
return false;
|
if (custom.bufferSearch == null) {
|
||||||
|
return true; // No buffer filter, only path filtering.
|
||||||
}
|
}
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
|
return custom.bufferSearch.matches(protobufBufferArray);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,6 +28,11 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
"cell_expandable_metadata.eml"
|
"cell_expandable_metadata.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final StringFilterGroup askSection = new StringFilterGroup(
|
||||||
|
Settings.HIDE_ASK_SECTION,
|
||||||
|
"youchat_entrypoint.eml"
|
||||||
|
);
|
||||||
|
|
||||||
final StringFilterGroup attributesSection = new StringFilterGroup(
|
final StringFilterGroup attributesSection = new StringFilterGroup(
|
||||||
Settings.HIDE_ATTRIBUTES_SECTION,
|
Settings.HIDE_ATTRIBUTES_SECTION,
|
||||||
"gaming_section",
|
"gaming_section",
|
||||||
@@ -73,6 +78,7 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
aiGeneratedVideoSummarySection,
|
aiGeneratedVideoSummarySection,
|
||||||
|
askSection,
|
||||||
attributesSection,
|
attributesSection,
|
||||||
infoCardsSection,
|
infoCardsSection,
|
||||||
howThisWasMadeSection,
|
howThisWasMadeSection,
|
||||||
@@ -88,13 +94,9 @@ final class DescriptionComponentsFilter extends Filter {
|
|||||||
if (exceptions.matches(path)) return false;
|
if (exceptions.matches(path)) return false;
|
||||||
|
|
||||||
if (matchedGroup == macroMarkersCarousel) {
|
if (matchedGroup == macroMarkersCarousel) {
|
||||||
if (contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered()) {
|
return contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,6 @@ import java.util.ArrayList;
|
|||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters litho based components.
|
* Filters litho based components.
|
||||||
*
|
*
|
||||||
@@ -62,10 +59,7 @@ abstract class Filter {
|
|||||||
* Called after an enabled filter has been matched.
|
* Called after an enabled filter has been matched.
|
||||||
* Default implementation is to always filter the matched component and log the action.
|
* Default implementation is to always filter the matched component and log the action.
|
||||||
* Subclasses can perform additional or different checks if needed.
|
* Subclasses can perform additional or different checks if needed.
|
||||||
* <p>
|
*
|
||||||
* If the content is to be filtered, subclasses should always
|
|
||||||
* call this method (and never return a plain 'true').
|
|
||||||
* That way the logs will always show when a component was filtered and which filter hide it.
|
|
||||||
* <p>
|
* <p>
|
||||||
* Method is called off the main thread.
|
* Method is called off the main thread.
|
||||||
*
|
*
|
||||||
@@ -76,14 +70,6 @@ abstract class Filter {
|
|||||||
*/
|
*/
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (BaseSettings.DEBUG.get()) {
|
|
||||||
String filterSimpleName = getClass().getSimpleName();
|
|
||||||
if (contentType == FilterContentType.IDENTIFIER) {
|
|
||||||
Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier);
|
|
||||||
} else {
|
|
||||||
Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ final class KeywordContentFilter extends Filter {
|
|||||||
MutableReference<String> matchRef = new MutableReference<>();
|
MutableReference<String> matchRef = new MutableReference<>();
|
||||||
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
|
if (bufferSearch.matches(protobufBufferArray, matchRef)) {
|
||||||
updateStats(true, matchRef.value);
|
updateStats(true, matchRef.value);
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStats(false, null);
|
updateStats(false, null);
|
||||||
|
|||||||
@@ -34,12 +34,11 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
private final StringFilterGroup notifyMe;
|
private final StringFilterGroup notifyMe;
|
||||||
private final StringFilterGroup singleItemInformationPanel;
|
private final StringFilterGroup singleItemInformationPanel;
|
||||||
private final StringFilterGroup expandableMetadata;
|
private final StringFilterGroup expandableMetadata;
|
||||||
private final ByteArrayFilterGroup searchResultRecommendations;
|
|
||||||
private final StringFilterGroup searchResultVideo;
|
|
||||||
private final StringFilterGroup compactChannelBarInner;
|
private final StringFilterGroup compactChannelBarInner;
|
||||||
private final StringFilterGroup compactChannelBarInnerButton;
|
private final StringFilterGroup compactChannelBarInnerButton;
|
||||||
private final ByteArrayFilterGroup joinMembershipButton;
|
private final ByteArrayFilterGroup joinMembershipButton;
|
||||||
private final StringFilterGroup horizontalShelves;
|
private final StringFilterGroup horizontalShelves;
|
||||||
|
private final ByteArrayFilterGroup ticketShelf;
|
||||||
|
|
||||||
public LayoutComponentsFilter() {
|
public LayoutComponentsFilter() {
|
||||||
exceptions.addPatterns(
|
exceptions.addPatterns(
|
||||||
@@ -233,14 +232,9 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
"mixed_content_shelf"
|
"mixed_content_shelf"
|
||||||
);
|
);
|
||||||
|
|
||||||
searchResultVideo = new StringFilterGroup(
|
final var searchResultRecommendationLabels = new StringFilterGroup(
|
||||||
Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS,
|
Settings.HIDE_SEARCH_RESULT_RECOMMENDATION_LABELS,
|
||||||
"search_video_with_context.eml"
|
"endorsement_header_footer.eml"
|
||||||
);
|
|
||||||
|
|
||||||
searchResultRecommendations = new ByteArrayFilterGroup(
|
|
||||||
Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS,
|
|
||||||
"endorsement_header_footer"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
horizontalShelves = new StringFilterGroup(
|
horizontalShelves = new StringFilterGroup(
|
||||||
@@ -251,6 +245,11 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
"horizontal_tile_shelf.eml"
|
"horizontal_tile_shelf.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ticketShelf = new ByteArrayFilterGroup(
|
||||||
|
Settings.HIDE_TICKET_SHELF,
|
||||||
|
"ticket"
|
||||||
|
);
|
||||||
|
|
||||||
addPathCallbacks(
|
addPathCallbacks(
|
||||||
expandableMetadata,
|
expandableMetadata,
|
||||||
inFeedSurvey,
|
inFeedSurvey,
|
||||||
@@ -258,7 +257,7 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
compactChannelBar,
|
compactChannelBar,
|
||||||
communityPosts,
|
communityPosts,
|
||||||
paidPromotion,
|
paidPromotion,
|
||||||
searchResultVideo,
|
searchResultRecommendationLabels,
|
||||||
latestPosts,
|
latestPosts,
|
||||||
channelWatermark,
|
channelWatermark,
|
||||||
communityGuidelines,
|
communityGuidelines,
|
||||||
@@ -293,50 +292,29 @@ public final class LayoutComponentsFilter extends Filter {
|
|||||||
// From 2025, the medical information panel is no longer shown in the search results.
|
// From 2025, the medical information panel is no longer shown in the search results.
|
||||||
// Therefore, this identifier does not filter when the search bar is activated.
|
// Therefore, this identifier does not filter when the search bar is activated.
|
||||||
if (matchedGroup == singleItemInformationPanel) {
|
if (matchedGroup == singleItemInformationPanel) {
|
||||||
if (PlayerType.getCurrent().isMaximizedOrFullscreen() || !NavigationBar.isSearchBarActive()) {
|
return PlayerType.getCurrent().isMaximizedOrFullscreen() || !NavigationBar.isSearchBarActive();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchedGroup == searchResultVideo) {
|
|
||||||
if (searchResultRecommendations.check(protobufBufferArray).isFiltered()) {
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The groups are excluded from the filter due to the exceptions list below.
|
// The groups are excluded from the filter due to the exceptions list below.
|
||||||
// Filter them separately here.
|
// Filter them separately here.
|
||||||
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata)
|
if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata) {
|
||||||
{
|
return true;
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
|
if (exceptions.matches(path)) return false; // Exceptions are not filtered.
|
||||||
|
|
||||||
if (matchedGroup == compactChannelBarInner) {
|
if (matchedGroup == compactChannelBarInner) {
|
||||||
if (compactChannelBarInnerButton.check(path).isFiltered()) {
|
return compactChannelBarInnerButton.check(path).isFiltered()
|
||||||
// The filter may be broad, but in the context of a compactChannelBarInnerButton,
|
// 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.
|
// it's safe to assume that the button is the only thing that should be hidden.
|
||||||
if (joinMembershipButton.check(protobufBufferArray).isFiltered()) {
|
&& joinMembershipButton.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == horizontalShelves) {
|
if (matchedGroup == horizontalShelves) {
|
||||||
if (contentIndex == 0 && hideShelves()) {
|
return contentIndex == 0 && (hideShelves() || ticketShelf.check(protobufBufferArray).isFiltered());
|
||||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import java.nio.ByteBuffer;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.youtube.StringTrieSearch;
|
import app.revanced.extension.youtube.StringTrieSearch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
@@ -114,12 +115,29 @@ public final class LithoFilterPatch {
|
|||||||
if (!group.includeInSearch()) {
|
if (!group.includeInSearch()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (String pattern : group.filters) {
|
for (String pattern : group.filters) {
|
||||||
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> {
|
String filterSimpleName = filter.getClass().getSimpleName();
|
||||||
|
|
||||||
|
pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex,
|
||||||
|
matchedLength, callbackParameter) -> {
|
||||||
if (!group.isEnabled()) return false;
|
if (!group.isEnabled()) return false;
|
||||||
|
|
||||||
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
|
LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter;
|
||||||
return filter.isFiltered(parameters.identifier, parameters.path, parameters.protoBuffer,
|
final boolean isFiltered = filter.isFiltered(parameters.identifier,
|
||||||
group, type, matchedStartIndex);
|
parameters.path, parameters.protoBuffer, group, type, matchedStartIndex);
|
||||||
|
|
||||||
|
if (isFiltered && BaseSettings.DEBUG.get()) {
|
||||||
|
if (type == Filter.FilterContentType.IDENTIFIER) {
|
||||||
|
Logger.printDebug(() -> "Filtered " + filterSimpleName
|
||||||
|
+ " identifier: " + parameters.identifier);
|
||||||
|
} else {
|
||||||
|
Logger.printDebug(() -> "Filtered " + filterSimpleName
|
||||||
|
+ " path: " + parameters.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isFiltered;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,18 +10,11 @@ import app.revanced.extension.youtube.settings.Settings;
|
|||||||
*/
|
*/
|
||||||
public final class PlaybackSpeedMenuFilterPatch extends Filter {
|
public final class PlaybackSpeedMenuFilterPatch extends Filter {
|
||||||
|
|
||||||
/**
|
|
||||||
* Old litho based speed selection menu.
|
|
||||||
*/
|
|
||||||
public static volatile boolean isOldPlaybackSpeedMenuVisible;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 0.05x speed selection menu.
|
* 0.05x speed selection menu.
|
||||||
*/
|
*/
|
||||||
public static volatile boolean isPlaybackRateSelectorMenuVisible;
|
public static volatile boolean isPlaybackRateSelectorMenuVisible;
|
||||||
|
|
||||||
private final StringFilterGroup oldPlaybackMenuGroup;
|
|
||||||
|
|
||||||
public PlaybackSpeedMenuFilterPatch() {
|
public PlaybackSpeedMenuFilterPatch() {
|
||||||
// 0.05x litho speed menu.
|
// 0.05x litho speed menu.
|
||||||
var playbackRateSelectorGroup = new StringFilterGroup(
|
var playbackRateSelectorGroup = new StringFilterGroup(
|
||||||
@@ -29,22 +22,13 @@ public final class PlaybackSpeedMenuFilterPatch extends Filter {
|
|||||||
"playback_rate_selector_menu_sheet.eml-js"
|
"playback_rate_selector_menu_sheet.eml-js"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Old litho based speed menu.
|
addPathCallbacks(playbackRateSelectorGroup);
|
||||||
oldPlaybackMenuGroup = new StringFilterGroup(
|
|
||||||
Settings.CUSTOM_SPEED_MENU,
|
|
||||||
"playback_speed_sheet_content.eml-js");
|
|
||||||
|
|
||||||
addPathCallbacks(playbackRateSelectorGroup, oldPlaybackMenuGroup);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == oldPlaybackMenuGroup) {
|
isPlaybackRateSelectorMenuVisible = true;
|
||||||
isOldPlaybackSpeedMenuVisible = true;
|
|
||||||
} else {
|
|
||||||
isPlaybackRateSelectorMenuVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
|||||||
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
|
||||||
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
|
||||||
if (matchedGroup == videoQualityMenuFooter) {
|
if (matchedGroup == videoQualityMenuFooter) {
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentIndex != 0) {
|
if (contentIndex != 0) {
|
||||||
@@ -111,11 +111,6 @@ public class PlayerFlyoutMenuItemsFilter extends Filter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flyoutFilterGroupList.check(protobufBufferArray).isFiltered()) {
|
return flyoutFilterGroupList.check(protobufBufferArray).isFiltered();
|
||||||
// Super class handles logging.
|
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,12 +143,14 @@ public final class ShortsFilter extends Filter {
|
|||||||
|
|
||||||
StringFilterGroup likeButton = new StringFilterGroup(
|
StringFilterGroup likeButton = new StringFilterGroup(
|
||||||
Settings.HIDE_SHORTS_LIKE_BUTTON,
|
Settings.HIDE_SHORTS_LIKE_BUTTON,
|
||||||
"shorts_like_button.eml"
|
"shorts_like_button.eml",
|
||||||
|
"reel_like_button.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
StringFilterGroup dislikeButton = new StringFilterGroup(
|
StringFilterGroup dislikeButton = new StringFilterGroup(
|
||||||
Settings.HIDE_SHORTS_DISLIKE_BUTTON,
|
Settings.HIDE_SHORTS_DISLIKE_BUTTON,
|
||||||
"shorts_dislike_button.eml"
|
"shorts_dislike_button.eml",
|
||||||
|
"reel_dislike_button.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
joinButton = new StringFilterGroup(
|
joinButton = new StringFilterGroup(
|
||||||
@@ -168,12 +170,13 @@ public final class ShortsFilter extends Filter {
|
|||||||
|
|
||||||
shortsActionBar = new StringFilterGroup(
|
shortsActionBar = new StringFilterGroup(
|
||||||
null,
|
null,
|
||||||
"shorts_action_bar.eml"
|
"shorts_action_bar.eml",
|
||||||
|
"reel_action_bar.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
actionButton = new StringFilterGroup(
|
actionButton = new StringFilterGroup(
|
||||||
null,
|
null,
|
||||||
// Can be simply 'button.eml' or 'shorts_video_action_button.eml'
|
// Can be simply 'button.eml', 'shorts_video_action_button.eml' or 'reel_action_button.eml'
|
||||||
"button.eml"
|
"button.eml"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -195,15 +198,18 @@ public final class ShortsFilter extends Filter {
|
|||||||
videoActionButtonGroupList.addAll(
|
videoActionButtonGroupList.addAll(
|
||||||
new ByteArrayFilterGroup(
|
new ByteArrayFilterGroup(
|
||||||
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
|
Settings.HIDE_SHORTS_COMMENTS_BUTTON,
|
||||||
"reel_comment_button"
|
"reel_comment_button",
|
||||||
|
"youtube_shorts_comment_outline"
|
||||||
),
|
),
|
||||||
new ByteArrayFilterGroup(
|
new ByteArrayFilterGroup(
|
||||||
Settings.HIDE_SHORTS_SHARE_BUTTON,
|
Settings.HIDE_SHORTS_SHARE_BUTTON,
|
||||||
"reel_share_button"
|
"reel_share_button",
|
||||||
|
"youtube_shorts_share_outline"
|
||||||
),
|
),
|
||||||
new ByteArrayFilterGroup(
|
new ByteArrayFilterGroup(
|
||||||
Settings.HIDE_SHORTS_REMIX_BUTTON,
|
Settings.HIDE_SHORTS_REMIX_BUTTON,
|
||||||
"reel_remix_button"
|
"reel_remix_button",
|
||||||
|
"youtube_shorts_remix_outline"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -278,27 +284,18 @@ public final class ShortsFilter extends Filter {
|
|||||||
if (contentType == FilterContentType.PATH) {
|
if (contentType == FilterContentType.PATH) {
|
||||||
if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
|
if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
|
||||||
// Selectively filter to avoid false positive filtering of other subscribe/join buttons.
|
// Selectively filter to avoid false positive filtering of other subscribe/join buttons.
|
||||||
if (path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH)) {
|
return path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH);
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == shortsCompactFeedVideoPath) {
|
if (matchedGroup == shortsCompactFeedVideoPath) {
|
||||||
if (shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered()) {
|
return shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Video action buttons (comment, share, remix) have the same path.
|
// Video action buttons (comment, share, remix) have the same path.
|
||||||
// Like and dislike are separate path filters and don't require buffer searching.
|
// Like and dislike are separate path filters and don't require buffer searching.
|
||||||
if (matchedGroup == shortsActionBar) {
|
if (matchedGroup == shortsActionBar) {
|
||||||
if (actionButton.check(path).isFiltered()
|
return actionButton.check(path).isFiltered()
|
||||||
&& videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) {
|
&& videoActionButtonGroupList.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchedGroup == suggestedAction) {
|
if (matchedGroup == suggestedAction) {
|
||||||
@@ -306,28 +303,23 @@ public final class ShortsFilter extends Filter {
|
|||||||
// This has a secondary effect of hiding all new un-identified actions
|
// This has a secondary effect of hiding all new un-identified actions
|
||||||
// under the assumption that the user wants all suggestions hidden.
|
// under the assumption that the user wants all suggestions hidden.
|
||||||
if (isEverySuggestedActionFilterEnabled()) {
|
if (isEverySuggestedActionFilterEnabled()) {
|
||||||
return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) {
|
return suggestedActionsGroupList.check(protobufBufferArray).isFiltered();
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
return true;
|
||||||
// Feed/search identifier components.
|
|
||||||
if (matchedGroup == shelfHeader) {
|
|
||||||
// Because the header is used in watch history and possibly other places, check for the index,
|
|
||||||
// which is 0 when the shelf header is used for Shorts.
|
|
||||||
if (contentIndex != 0) return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldHideShortsFeedItems()) return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Super class handles logging.
|
// Feed/search identifier components.
|
||||||
return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
|
if (matchedGroup == shelfHeader) {
|
||||||
|
// Because the header is used in watch history and possibly other places, check for the index,
|
||||||
|
// which is 0 when the shelf header is used for Shorts.
|
||||||
|
if (contentIndex != 0) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldHideShortsFeedItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean shouldHideShortsFeedItems() {
|
private static boolean shouldHideShortsFeedItems() {
|
||||||
|
|||||||
@@ -1,30 +1,57 @@
|
|||||||
package app.revanced.extension.youtube.patches.playback.speed;
|
package app.revanced.extension.youtube.patches.playback.speed;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.sf;
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.dipToPixels;
|
||||||
|
|
||||||
import android.preference.ListPreference;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.PixelFormat;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.graphics.drawable.ShapeDrawable;
|
||||||
|
import android.graphics.drawable.shapes.RoundRectShape;
|
||||||
|
import android.icu.text.NumberFormat;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.Gravity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewParent;
|
import android.view.ViewParent;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.GridLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.SeekBar;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import java.lang.ref.WeakReference;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.youtube.ThemeHelper;
|
||||||
|
import app.revanced.extension.youtube.patches.VideoInformation;
|
||||||
import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilterPatch;
|
import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilterPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.shared.PlayerType;
|
||||||
|
import kotlin.Unit;
|
||||||
|
import kotlin.jvm.functions.Function1;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class CustomPlaybackSpeedPatch {
|
public class CustomPlaybackSpeedPatch {
|
||||||
|
|
||||||
private static final float PLAYBACK_SPEED_AUTO = Settings.PLAYBACK_SPEED_DEFAULT.defaultValue;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum playback speed, exclusive value. Custom speeds must be less than this value.
|
* Maximum playback speed, inclusive. Custom speeds must be this or less.
|
||||||
* <p>
|
* <p>
|
||||||
* Going over 8x does not increase the actual playback speed any higher,
|
* Going over 8x does not increase the actual playback speed any higher,
|
||||||
* and the UI selector starts flickering and acting weird.
|
* and the UI selector starts flickering and acting weird.
|
||||||
@@ -32,6 +59,11 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
*/
|
*/
|
||||||
public static final float PLAYBACK_SPEED_MAXIMUM = 8;
|
public static final float PLAYBACK_SPEED_MAXIMUM = 8;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scale used to convert user speed to {@link android.widget.ProgressBar#setProgress(int)}.
|
||||||
|
*/
|
||||||
|
private static final float PROGRESS_BAR_VALUE_SCALE = 100;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tap and hold speed.
|
* Tap and hold speed.
|
||||||
*/
|
*/
|
||||||
@@ -40,21 +72,28 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
/**
|
/**
|
||||||
* Custom playback speeds.
|
* Custom playback speeds.
|
||||||
*/
|
*/
|
||||||
public static float[] customPlaybackSpeeds;
|
public static final float[] customPlaybackSpeeds;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The last time the old playback menu was forcefully called.
|
* Formats speeds to UI strings.
|
||||||
*/
|
*/
|
||||||
private static long lastTimeOldPlaybackMenuInvoked;
|
private static final NumberFormat speedFormatter = NumberFormat.getNumberInstance();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PreferenceList entries and values, of all available playback speeds.
|
* Weak reference to the currently open dialog.
|
||||||
*/
|
*/
|
||||||
private static String[] preferenceListEntries, preferenceListEntryValues;
|
private static WeakReference<Dialog> currentDialog = new WeakReference<>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum and maximum custom playback speeds of {@link #customPlaybackSpeeds}.
|
||||||
|
*/
|
||||||
|
private static final float customPlaybackSpeedsMin, customPlaybackSpeedsMax;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get();
|
// Cap at 2 decimals (rounds automatically).
|
||||||
|
speedFormatter.setMaximumFractionDigits(2);
|
||||||
|
|
||||||
|
final float holdSpeed = Settings.SPEED_TAP_AND_HOLD.get();
|
||||||
if (holdSpeed > 0 && holdSpeed <= PLAYBACK_SPEED_MAXIMUM) {
|
if (holdSpeed > 0 && holdSpeed <= PLAYBACK_SPEED_MAXIMUM) {
|
||||||
TAP_AND_HOLD_SPEED = holdSpeed;
|
TAP_AND_HOLD_SPEED = holdSpeed;
|
||||||
} else {
|
} else {
|
||||||
@@ -62,7 +101,9 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
TAP_AND_HOLD_SPEED = Settings.SPEED_TAP_AND_HOLD.resetToDefault();
|
TAP_AND_HOLD_SPEED = Settings.SPEED_TAP_AND_HOLD.resetToDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCustomSpeeds();
|
customPlaybackSpeeds = loadCustomSpeeds();
|
||||||
|
customPlaybackSpeedsMin = customPlaybackSpeeds[0];
|
||||||
|
customPlaybackSpeedsMax = customPlaybackSpeeds[customPlaybackSpeeds.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,37 +117,41 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
Utils.showToastLong(str("revanced_custom_playback_speeds_invalid", PLAYBACK_SPEED_MAXIMUM));
|
Utils.showToastLong(str("revanced_custom_playback_speeds_invalid", PLAYBACK_SPEED_MAXIMUM));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void loadCustomSpeeds() {
|
private static float[] loadCustomSpeeds() {
|
||||||
try {
|
try {
|
||||||
String[] speedStrings = Settings.CUSTOM_PLAYBACK_SPEEDS.get().split("\\s+");
|
// Automatically replace commas with periods,
|
||||||
|
// if the user added speeds in a localized format.
|
||||||
|
String[] speedStrings = Settings.CUSTOM_PLAYBACK_SPEEDS.get()
|
||||||
|
.replace(',', '.').split("\\s+");
|
||||||
Arrays.sort(speedStrings);
|
Arrays.sort(speedStrings);
|
||||||
if (speedStrings.length == 0) {
|
if (speedStrings.length == 0) {
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
|
|
||||||
customPlaybackSpeeds = new float[speedStrings.length];
|
float[] speeds = new float[speedStrings.length];
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (String speedString : speedStrings) {
|
for (String speedString : speedStrings) {
|
||||||
final float speedFloat = Float.parseFloat(speedString);
|
final float speedFloat = Float.parseFloat(speedString);
|
||||||
if (speedFloat <= 0 || arrayContains(customPlaybackSpeeds, speedFloat)) {
|
if (speedFloat <= 0 || arrayContains(speeds, speedFloat)) {
|
||||||
throw new IllegalArgumentException();
|
throw new IllegalArgumentException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (speedFloat >= PLAYBACK_SPEED_MAXIMUM) {
|
if (speedFloat > PLAYBACK_SPEED_MAXIMUM) {
|
||||||
showInvalidCustomSpeedToast();
|
showInvalidCustomSpeedToast();
|
||||||
Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
|
Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
|
||||||
loadCustomSpeeds();
|
return loadCustomSpeeds();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
customPlaybackSpeeds[i++] = speedFloat;
|
speeds[i++] = speedFloat;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return speeds;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printInfo(() -> "parse error", ex);
|
Logger.printInfo(() -> "Parse error", ex);
|
||||||
Utils.showToastLong(str("revanced_custom_playback_speeds_parse_exception"));
|
Utils.showToastShort(str("revanced_custom_playback_speeds_parse_exception"));
|
||||||
Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
|
Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
|
||||||
loadCustomSpeeds();
|
return loadCustomSpeeds();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,33 +162,6 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a settings preference list with the available playback speeds.
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public static void initializeListPreference(ListPreference preference) {
|
|
||||||
if (preferenceListEntries == null) {
|
|
||||||
final int numberOfEntries = customPlaybackSpeeds.length + 1;
|
|
||||||
preferenceListEntries = new String[numberOfEntries];
|
|
||||||
preferenceListEntryValues = new String[numberOfEntries];
|
|
||||||
|
|
||||||
// Auto speed (same behavior as unpatched).
|
|
||||||
preferenceListEntries[0] = sf("revanced_custom_playback_speeds_auto").toString();
|
|
||||||
preferenceListEntryValues[0] = String.valueOf(PLAYBACK_SPEED_AUTO);
|
|
||||||
|
|
||||||
int i = 1;
|
|
||||||
for (float speed : customPlaybackSpeeds) {
|
|
||||||
String speedString = String.valueOf(speed);
|
|
||||||
preferenceListEntries[i] = speedString + "x";
|
|
||||||
preferenceListEntryValues[i] = speedString;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preference.setEntries(preferenceListEntries);
|
|
||||||
preference.setEntryValues(preferenceListEntryValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
*/
|
*/
|
||||||
@@ -151,38 +169,28 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
|
||||||
try {
|
try {
|
||||||
if (PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible) {
|
if (PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible) {
|
||||||
if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 5)) {
|
if (hideLithoMenuAndShowCustomSpeedMenu(recyclerView, 5)) {
|
||||||
PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible = false;
|
PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible = false;
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "isPlaybackRateSelectorMenuVisible failure", ex);
|
Logger.printException(() -> "onFlyoutMenuCreate failure", ex);
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (PlaybackSpeedMenuFilterPatch.isOldPlaybackSpeedMenuVisible) {
|
|
||||||
if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 8)) {
|
|
||||||
PlaybackSpeedMenuFilterPatch.isOldPlaybackSpeedMenuVisible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "isOldPlaybackSpeedMenuVisible failure", ex);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static boolean hideLithoMenuAndShowOldSpeedMenu(RecyclerView recyclerView, int expectedChildCount) {
|
@SuppressWarnings("SameParameterValue")
|
||||||
|
private static boolean hideLithoMenuAndShowCustomSpeedMenu(RecyclerView recyclerView, int expectedChildCount) {
|
||||||
if (recyclerView.getChildCount() == 0) {
|
if (recyclerView.getChildCount() == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
View firstChild = recyclerView.getChildAt(0);
|
View firstChild = recyclerView.getChildAt(0);
|
||||||
if (!(firstChild instanceof ViewGroup PlaybackSpeedParentView)) {
|
if (!(firstChild instanceof ViewGroup playbackSpeedParentView)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PlaybackSpeedParentView.getChildCount() != expectedChildCount) {
|
if (playbackSpeedParentView.getChildCount() != expectedChildCount) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,23 +214,418 @@ public class CustomPlaybackSpeedPatch {
|
|||||||
((ViewGroup) parentView3rd).setVisibility(View.GONE);
|
((ViewGroup) parentView3rd).setVisibility(View.GONE);
|
||||||
((ViewGroup) parentView4th).setVisibility(View.GONE);
|
((ViewGroup) parentView4th).setVisibility(View.GONE);
|
||||||
|
|
||||||
// Close the litho speed menu and show the old one.
|
// Close the litho speed menu and show the modern custom speed dialog.
|
||||||
showOldPlaybackSpeedMenu();
|
showModernCustomPlaybackSpeedDialog(recyclerView.getContext());
|
||||||
|
Logger.printDebug(() -> "Modern playback speed dialog shown");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void showOldPlaybackSpeedMenu() {
|
/**
|
||||||
// This method is sometimes used multiple times.
|
* Displays a modern custom dialog for adjusting video playback speed.
|
||||||
// To prevent this, ignore method reuse within 1 second.
|
* <p>
|
||||||
final long now = System.currentTimeMillis();
|
* This method creates a dialog with a slider, plus/minus buttons, and preset speed buttons
|
||||||
if (now - lastTimeOldPlaybackMenuInvoked < 1000) {
|
* to allow the user to modify the video playback speed. The dialog is styled with rounded
|
||||||
Logger.printDebug(() -> "Ignoring call to showOldPlaybackSpeedMenu");
|
* corners and themed colors, positioned at the bottom of the screen. The playback speed
|
||||||
return;
|
* can be adjusted in 0.05 increments using the slider or buttons, or set directly to preset
|
||||||
}
|
* values. The dialog updates the displayed speed in real-time and applies changes to the
|
||||||
lastTimeOldPlaybackMenuInvoked = now;
|
* video playback. The dialog is dismissed if the player enters Picture-in-Picture (PiP) mode.
|
||||||
Logger.printDebug(() -> "Old video quality menu shown");
|
*/
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
public static void showModernCustomPlaybackSpeedDialog(Context context) {
|
||||||
|
// Create a dialog without a theme for custom appearance.
|
||||||
|
Dialog dialog = new Dialog(context);
|
||||||
|
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE); // Remove default title bar.
|
||||||
|
|
||||||
// Rest of the implementation added by patch.
|
// Store the dialog reference.
|
||||||
|
currentDialog = new WeakReference<>(dialog);
|
||||||
|
|
||||||
|
// Create main vertical LinearLayout for dialog content.
|
||||||
|
LinearLayout mainLayout = new LinearLayout(context);
|
||||||
|
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
|
||||||
|
// Preset size constants.
|
||||||
|
final int dip4 = dipToPixels(4); // Height for handle bar.
|
||||||
|
final int dip5 = dipToPixels(5);
|
||||||
|
final int dip6 = dipToPixels(6); // Padding for mainLayout from bottom.
|
||||||
|
final int dip8 = dipToPixels(8); // Padding for mainLayout from left and right.
|
||||||
|
final int dip20 = dipToPixels(20);
|
||||||
|
final int dip32 = dipToPixels(32); // Height for in-rows speed buttons.
|
||||||
|
final int dip36 = dipToPixels(36); // Height for minus and plus buttons.
|
||||||
|
final int dip40 = dipToPixels(40); // Width for handle bar.
|
||||||
|
final int dip60 = dipToPixels(60); // Height for speed button container.
|
||||||
|
|
||||||
|
mainLayout.setPadding(dip5, dip8, dip5, dip8);
|
||||||
|
|
||||||
|
// Set rounded rectangle background for the main layout.
|
||||||
|
RoundRectShape roundRectShape = new RoundRectShape(
|
||||||
|
createCornerRadii(12), null, null);
|
||||||
|
ShapeDrawable background = new ShapeDrawable(roundRectShape);
|
||||||
|
background.getPaint().setColor(ThemeHelper.getDialogBackgroundColor());
|
||||||
|
mainLayout.setBackground(background);
|
||||||
|
|
||||||
|
// Add handle bar at the top.
|
||||||
|
View handleBar = new View(context);
|
||||||
|
ShapeDrawable handleBackground = new ShapeDrawable(new RoundRectShape(
|
||||||
|
createCornerRadii(4), null, null));
|
||||||
|
handleBackground.getPaint().setColor(getAdjustedBackgroundColor(true));
|
||||||
|
handleBar.setBackground(handleBackground);
|
||||||
|
LinearLayout.LayoutParams handleParams = new LinearLayout.LayoutParams(
|
||||||
|
dip40, // handle bar width.
|
||||||
|
dip4 // handle bar height.
|
||||||
|
);
|
||||||
|
handleParams.gravity = Gravity.CENTER_HORIZONTAL; // Center horizontally.
|
||||||
|
handleParams.setMargins(0, 0, 0, dip20); // 20dp bottom margins.
|
||||||
|
handleBar.setLayoutParams(handleParams);
|
||||||
|
// Add handle bar view to main layout.
|
||||||
|
mainLayout.addView(handleBar);
|
||||||
|
|
||||||
|
// Display current playback speed.
|
||||||
|
TextView currentSpeedText = new TextView(context);
|
||||||
|
float currentSpeed = VideoInformation.getPlaybackSpeed();
|
||||||
|
// Initially show with only 0 minimum digits, so 1.0 shows as 1x
|
||||||
|
currentSpeedText.setText(formatSpeedStringX(currentSpeed, 0));
|
||||||
|
currentSpeedText.setTextColor(ThemeHelper.getForegroundColor());
|
||||||
|
currentSpeedText.setTextSize(16);
|
||||||
|
currentSpeedText.setTypeface(Typeface.DEFAULT_BOLD);
|
||||||
|
currentSpeedText.setGravity(Gravity.CENTER);
|
||||||
|
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
textParams.setMargins(0, 0, 0, 0);
|
||||||
|
currentSpeedText.setLayoutParams(textParams);
|
||||||
|
// Add current speed text view to main layout.
|
||||||
|
mainLayout.addView(currentSpeedText);
|
||||||
|
|
||||||
|
// Create horizontal layout for slider and +/- buttons.
|
||||||
|
LinearLayout sliderLayout = new LinearLayout(context);
|
||||||
|
sliderLayout.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
sliderLayout.setGravity(Gravity.CENTER_VERTICAL);
|
||||||
|
sliderLayout.setPadding(dip5, dip5, dip5, dip5); // 5dp padding.
|
||||||
|
|
||||||
|
// Create minus button.
|
||||||
|
Button minusButton = new Button(context, null, 0); // Disable default theme style.
|
||||||
|
minusButton.setText(""); // No text on button.
|
||||||
|
ShapeDrawable minusBackground = new ShapeDrawable(new RoundRectShape(createCornerRadii(20), null, null));
|
||||||
|
minusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
|
||||||
|
minusButton.setBackground(minusBackground);
|
||||||
|
OutlineSymbolDrawable minusDrawable = new OutlineSymbolDrawable(false); // Minus symbol.
|
||||||
|
minusButton.setForeground(minusDrawable);
|
||||||
|
LinearLayout.LayoutParams minusParams = new LinearLayout.LayoutParams(dip36, dip36);
|
||||||
|
minusParams.setMargins(0, 0, dip5, 0); // 5dp to slider.
|
||||||
|
minusButton.setLayoutParams(minusParams);
|
||||||
|
|
||||||
|
// Create slider for speed adjustment.
|
||||||
|
SeekBar speedSlider = new SeekBar(context);
|
||||||
|
speedSlider.setMax(speedToProgressValue(customPlaybackSpeedsMax));
|
||||||
|
speedSlider.setProgress(speedToProgressValue(currentSpeed));
|
||||||
|
speedSlider.getProgressDrawable().setColorFilter(
|
||||||
|
ThemeHelper.getForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme progress bar.
|
||||||
|
speedSlider.getThumb().setColorFilter(
|
||||||
|
ThemeHelper.getForegroundColor(), PorterDuff.Mode.SRC_IN); // Theme slider thumb.
|
||||||
|
LinearLayout.LayoutParams sliderParams = new LinearLayout.LayoutParams(
|
||||||
|
0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
|
||||||
|
sliderParams.setMargins(dip5, 0, dip5, 0); // 5dp to -/+ buttons.
|
||||||
|
speedSlider.setLayoutParams(sliderParams);
|
||||||
|
|
||||||
|
// Create plus button.
|
||||||
|
Button plusButton = new Button(context, null, 0); // Disable default theme style.
|
||||||
|
plusButton.setText(""); // No text on button.
|
||||||
|
ShapeDrawable plusBackground = new ShapeDrawable(new RoundRectShape(
|
||||||
|
createCornerRadii(20), null, null));
|
||||||
|
plusBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
|
||||||
|
plusButton.setBackground(plusBackground);
|
||||||
|
OutlineSymbolDrawable plusDrawable = new OutlineSymbolDrawable(true); // Plus symbol.
|
||||||
|
plusButton.setForeground(plusDrawable);
|
||||||
|
LinearLayout.LayoutParams plusParams = new LinearLayout.LayoutParams(dip36, dip36);
|
||||||
|
plusParams.setMargins(dip5, 0, 0, 0); // 5dp to slider.
|
||||||
|
plusButton.setLayoutParams(plusParams);
|
||||||
|
|
||||||
|
// Add -/+ and slider views to slider layout.
|
||||||
|
sliderLayout.addView(minusButton);
|
||||||
|
sliderLayout.addView(speedSlider);
|
||||||
|
sliderLayout.addView(plusButton);
|
||||||
|
|
||||||
|
LinearLayout.LayoutParams sliderLayoutParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
sliderLayoutParams.setMargins(0, 0, 0, dip5); // 5dp bottom margin.
|
||||||
|
sliderLayout.setLayoutParams(sliderLayoutParams);
|
||||||
|
|
||||||
|
// Add slider layout to main layout.
|
||||||
|
mainLayout.addView(sliderLayout);
|
||||||
|
|
||||||
|
Function<Float, Void> userSelectedSpeed = newSpeed -> {
|
||||||
|
final float roundedSpeed = roundSpeedToNearestIncrement(newSpeed);
|
||||||
|
if (VideoInformation.getPlaybackSpeed() == roundedSpeed) {
|
||||||
|
// Nothing has changed. New speed rounds to the current speed.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoInformation.overridePlaybackSpeed(roundedSpeed);
|
||||||
|
RememberPlaybackSpeedPatch.userSelectedPlaybackSpeed(roundedSpeed);
|
||||||
|
currentSpeedText.setText(formatSpeedStringX(roundedSpeed, 2)); // Update display.
|
||||||
|
speedSlider.setProgress(speedToProgressValue(roundedSpeed)); // Update slider.
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set listener for slider to update playback speed.
|
||||||
|
speedSlider.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||||
|
if (fromUser) {
|
||||||
|
// Convert from progress value to video playback speed.
|
||||||
|
userSelectedSpeed.apply(customPlaybackSpeedsMin + (progress / PROGRESS_BAR_VALUE_SCALE));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStartTrackingTouch(SeekBar seekBar) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStopTrackingTouch(SeekBar seekBar) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
minusButton.setOnClickListener(v -> userSelectedSpeed.apply(
|
||||||
|
VideoInformation.getPlaybackSpeed() - 0.05f));
|
||||||
|
plusButton.setOnClickListener(v -> userSelectedSpeed.apply(
|
||||||
|
VideoInformation.getPlaybackSpeed() + 0.05f));
|
||||||
|
|
||||||
|
// Create GridLayout for preset speed buttons.
|
||||||
|
GridLayout gridLayout = new GridLayout(context);
|
||||||
|
gridLayout.setColumnCount(5); // 5 columns for speed buttons.
|
||||||
|
gridLayout.setAlignmentMode(GridLayout.ALIGN_BOUNDS);
|
||||||
|
gridLayout.setRowCount((int) Math.ceil(customPlaybackSpeeds.length / 5.0));
|
||||||
|
LinearLayout.LayoutParams gridParams = new LinearLayout.LayoutParams(
|
||||||
|
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||||
|
gridParams.setMargins(0, 0, 0, 0); // No margins around GridLayout.
|
||||||
|
gridLayout.setLayoutParams(gridParams);
|
||||||
|
|
||||||
|
// For all buttons show at least 1 zero in decimal (2 -> "2.0").
|
||||||
|
speedFormatter.setMinimumFractionDigits(1);
|
||||||
|
|
||||||
|
// Add buttons for each preset playback speed.
|
||||||
|
for (float speed : customPlaybackSpeeds) {
|
||||||
|
// Container for button and optional label.
|
||||||
|
FrameLayout buttonContainer = new FrameLayout(context);
|
||||||
|
|
||||||
|
// Set layout parameters for each grid cell.
|
||||||
|
GridLayout.LayoutParams containerParams = new GridLayout.LayoutParams();
|
||||||
|
containerParams.width = 0; // Equal width for columns.
|
||||||
|
containerParams.columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1, 1f);
|
||||||
|
containerParams.setMargins(dip5, 0, dip5, 0); // Button margins.
|
||||||
|
containerParams.height = dip60; // Fixed height for button and label.
|
||||||
|
buttonContainer.setLayoutParams(containerParams);
|
||||||
|
|
||||||
|
// Create speed button.
|
||||||
|
Button speedButton = new Button(context, null, 0);
|
||||||
|
speedButton.setText(speedFormatter.format(speed)); // Do not use 'x' speed format.
|
||||||
|
speedButton.setTextColor(ThemeHelper.getForegroundColor());
|
||||||
|
speedButton.setTextSize(12);
|
||||||
|
speedButton.setAllCaps(false);
|
||||||
|
speedButton.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
ShapeDrawable buttonBackground = new ShapeDrawable(new RoundRectShape(
|
||||||
|
createCornerRadii(20), null, null));
|
||||||
|
buttonBackground.getPaint().setColor(getAdjustedBackgroundColor(false));
|
||||||
|
speedButton.setBackground(buttonBackground);
|
||||||
|
speedButton.setPadding(dip5, dip5, dip5, dip5);
|
||||||
|
|
||||||
|
// Center button vertically and stretch horizontally in container.
|
||||||
|
FrameLayout.LayoutParams buttonParams = new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT, dip32, Gravity.CENTER);
|
||||||
|
speedButton.setLayoutParams(buttonParams);
|
||||||
|
|
||||||
|
// Add speed buttons view to buttons container layout.
|
||||||
|
buttonContainer.addView(speedButton);
|
||||||
|
|
||||||
|
// Add "Normal" label for 1.0x speed.
|
||||||
|
if (speed == 1.0f) {
|
||||||
|
TextView normalLabel = new TextView(context);
|
||||||
|
// Use same 'Normal' string as stock YouTube.
|
||||||
|
normalLabel.setText(str("normal_playback_rate_label"));
|
||||||
|
normalLabel.setTextColor(ThemeHelper.getForegroundColor());
|
||||||
|
normalLabel.setTextSize(10);
|
||||||
|
normalLabel.setGravity(Gravity.CENTER);
|
||||||
|
|
||||||
|
FrameLayout.LayoutParams labelParams = new FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
|
||||||
|
labelParams.bottomMargin = 0; // Position label below button.
|
||||||
|
normalLabel.setLayoutParams(labelParams);
|
||||||
|
|
||||||
|
buttonContainer.addView(normalLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
speedButton.setOnClickListener(v -> userSelectedSpeed.apply(speed));
|
||||||
|
|
||||||
|
gridLayout.addView(buttonContainer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add in-rows speed buttons layout to main layout.
|
||||||
|
mainLayout.addView(gridLayout);
|
||||||
|
|
||||||
|
// Wrap mainLayout in another LinearLayout for side margins.
|
||||||
|
LinearLayout wrapperLayout = new LinearLayout(context);
|
||||||
|
wrapperLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
wrapperLayout.setPadding(dip8, 0, dip8, 0); // 8dp side margins.
|
||||||
|
wrapperLayout.addView(mainLayout);
|
||||||
|
dialog.setContentView(wrapperLayout);
|
||||||
|
|
||||||
|
// Configure dialog window to appear at the bottom.
|
||||||
|
Window window = dialog.getWindow();
|
||||||
|
if (window != null) {
|
||||||
|
WindowManager.LayoutParams params = window.getAttributes();
|
||||||
|
params.gravity = Gravity.BOTTOM; // Position at bottom of screen.
|
||||||
|
params.y = dip6; // 6dp margin from bottom.
|
||||||
|
// In landscape, use the smaller dimension (height) as portrait width.
|
||||||
|
int portraitWidth = context.getResources().getDisplayMetrics().widthPixels;
|
||||||
|
if (context.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
|
portraitWidth = Math.min(
|
||||||
|
portraitWidth,
|
||||||
|
context.getResources().getDisplayMetrics().heightPixels);
|
||||||
|
}
|
||||||
|
params.width = portraitWidth; // Use portrait width.
|
||||||
|
params.height = WindowManager.LayoutParams.WRAP_CONTENT;
|
||||||
|
window.setAttributes(params);
|
||||||
|
window.setBackgroundDrawable(null); // Remove default dialog background.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create observer for PlayerType changes.
|
||||||
|
Function1<PlayerType, Unit> playerTypeObserver = new Function1<>() {
|
||||||
|
@Override
|
||||||
|
public Unit invoke(PlayerType type) {
|
||||||
|
Dialog current = currentDialog.get();
|
||||||
|
if (current == null || !current.isShowing()) {
|
||||||
|
// Should never happen.
|
||||||
|
PlayerType.getOnChange().removeObserver(this);
|
||||||
|
Logger.printException(() -> "Removing player type listener as dialog is null or closed");
|
||||||
|
} else if (type == PlayerType.WATCH_WHILE_PICTURE_IN_PICTURE) {
|
||||||
|
current.dismiss();
|
||||||
|
Logger.printDebug(() -> "Playback speed dialog dismissed due to PiP mode");
|
||||||
|
}
|
||||||
|
return Unit.INSTANCE;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add observer to dismiss dialog when entering PiP mode.
|
||||||
|
PlayerType.getOnChange().addObserver(playerTypeObserver);
|
||||||
|
|
||||||
|
// Remove observer when dialog is dismissed.
|
||||||
|
dialog.setOnDismissListener(d -> {
|
||||||
|
PlayerType.getOnChange().removeObserver(playerTypeObserver);
|
||||||
|
Logger.printDebug(() -> "PlayerType observer removed on dialog dismiss");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Apply slide-in animation when showing the dialog.
|
||||||
|
final int fadeDurationFast = Utils.getResourceInteger("fade_duration_fast");
|
||||||
|
Animation slideInABottomAnimation = Utils.getResourceAnimation("slide_in_bottom");
|
||||||
|
slideInABottomAnimation.setDuration(fadeDurationFast);
|
||||||
|
mainLayout.startAnimation(slideInABottomAnimation);
|
||||||
|
|
||||||
|
dialog.show(); // Display the dialog.
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an array of corner radii for a rounded rectangle shape.
|
||||||
|
*
|
||||||
|
* @param dp The radius in density-independent pixels (dp) to apply to all corners.
|
||||||
|
* @return An array of eight float values representing the corner radii
|
||||||
|
* (top-left, top-right, bottom-right, bottom-left).
|
||||||
|
*/
|
||||||
|
private static float[] createCornerRadii(float dp) {
|
||||||
|
final float radius = dipToPixels(dp);
|
||||||
|
return new float[]{radius, radius, radius, radius, radius, radius, radius, radius};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param speed The playback speed value to format.
|
||||||
|
* @return A string representation of the speed with 'x' (e.g. "1.25x" or "1.00x").
|
||||||
|
*/
|
||||||
|
private static String formatSpeedStringX(float speed, int minimumFractionDigits) {
|
||||||
|
speedFormatter.setMinimumFractionDigits(minimumFractionDigits);
|
||||||
|
return speedFormatter.format(speed) + 'x';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return user speed converted to a value for {@link SeekBar#setProgress(int)}.
|
||||||
|
*/
|
||||||
|
private static int speedToProgressValue(float speed) {
|
||||||
|
return (int) ((speed - customPlaybackSpeedsMin) * PROGRESS_BAR_VALUE_SCALE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rounds the given playback speed to the nearest 0.05 increment and ensures it is within valid bounds.
|
||||||
|
*
|
||||||
|
* @param speed The playback speed to round.
|
||||||
|
* @return The rounded speed, constrained to the specified bounds.
|
||||||
|
*/
|
||||||
|
private static float roundSpeedToNearestIncrement(float speed) {
|
||||||
|
// Round to nearest 0.05 speed.
|
||||||
|
final float roundedSpeed = Math.round(speed / 0.05f) * 0.05f;
|
||||||
|
return Utils.clamp(roundedSpeed, 0.05f, PLAYBACK_SPEED_MAXIMUM);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adjusts the background color based on the current theme.
|
||||||
|
*
|
||||||
|
* @param isHandleBar If true, applies a stronger darkening factor (0.9) for the handle bar in light theme;
|
||||||
|
* if false, applies a standard darkening factor (0.95) for other elements in light theme.
|
||||||
|
* @return A modified background color, lightened by 20% for dark themes or darkened by 5% (or 10% for handle bar)
|
||||||
|
* for light themes to ensure visual contrast.
|
||||||
|
*/
|
||||||
|
public static int getAdjustedBackgroundColor(boolean isHandleBar) {
|
||||||
|
final int baseColor = ThemeHelper.getDialogBackgroundColor();
|
||||||
|
float darkThemeFactor = isHandleBar ? 1.25f : 1.115f; // 1.25f for handleBar, 1.115f for others in dark theme.
|
||||||
|
float lightThemeFactor = isHandleBar ? 0.9f : 0.95f; // 0.9f for handleBar, 0.95f for others in light theme.
|
||||||
|
return ThemeHelper.isDarkTheme()
|
||||||
|
? ThemeHelper.adjustColorBrightness(baseColor, darkThemeFactor) // Lighten for dark theme.
|
||||||
|
: ThemeHelper.adjustColorBrightness(baseColor, lightThemeFactor); // Darken for light theme.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom Drawable for rendering outlined plus and minus symbols on buttons.
|
||||||
|
*/
|
||||||
|
class OutlineSymbolDrawable extends Drawable {
|
||||||
|
private final boolean isPlus; // Determines if the symbol is a plus or minus.
|
||||||
|
private final Paint paint;
|
||||||
|
|
||||||
|
OutlineSymbolDrawable(boolean isPlus) {
|
||||||
|
this.isPlus = isPlus;
|
||||||
|
paint = new Paint(Paint.ANTI_ALIAS_FLAG); // Enable anti-aliasing for smooth rendering.
|
||||||
|
paint.setColor(ThemeHelper.getForegroundColor());
|
||||||
|
paint.setStyle(Paint.Style.STROKE); // Use stroke style for outline.
|
||||||
|
paint.setStrokeWidth(dipToPixels(1)); // 1dp stroke width.
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(Canvas canvas) {
|
||||||
|
Rect bounds = getBounds();
|
||||||
|
final int width = bounds.width();
|
||||||
|
final int height = bounds.height();
|
||||||
|
final float centerX = width / 2f; // Center X coordinate.
|
||||||
|
final float centerY = height / 2f; // Center Y coordinate.
|
||||||
|
final float size = Math.min(width, height) * 0.25f; // Symbol size is 25% of button dimensions.
|
||||||
|
|
||||||
|
// Draw horizontal line for both plus and minus symbols.
|
||||||
|
canvas.drawLine(centerX - size, centerY, centerX + size, centerY, paint);
|
||||||
|
if (isPlus) {
|
||||||
|
// Draw vertical line for plus symbol.
|
||||||
|
canvas.drawLine(centerX, centerY - size, centerX, centerY + size, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(int alpha) {
|
||||||
|
paint.setAlpha(alpha);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(ColorFilter colorFilter) {
|
||||||
|
paint.setColorFilter(colorFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity() {
|
||||||
|
return PixelFormat.TRANSLUCENT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ public final class RememberPlaybackSpeedPatch {
|
|||||||
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
|
public static void userSelectedPlaybackSpeed(float playbackSpeed) {
|
||||||
try {
|
try {
|
||||||
if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
|
if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
|
||||||
// With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
|
// With the 0.05x menu, if the speed is set by a patch to higher than 2.0x
|
||||||
// then the menu will allow increasing without bounds but the max speed is
|
// then the menu will allow increasing without bounds but the max speed is
|
||||||
// still capped to under 8.0x.
|
// still capped to 8.0x.
|
||||||
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f);
|
playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM);
|
||||||
|
|
||||||
// Prevent toast spamming if using the 0.05x adjustments.
|
// Prevent toast spamming if using the 0.05x adjustments.
|
||||||
// Show exactly one toast after the user stops interacting with the speed menu.
|
// Show exactly one toast after the user stops interacting with the speed menu.
|
||||||
@@ -57,7 +57,7 @@ public final class RememberPlaybackSpeedPatch {
|
|||||||
}
|
}
|
||||||
Settings.PLAYBACK_SPEED_DEFAULT.save(finalPlaybackSpeed);
|
Settings.PLAYBACK_SPEED_DEFAULT.save(finalPlaybackSpeed);
|
||||||
|
|
||||||
Utils.showToastLong(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
|
Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
|
||||||
}, TOAST_DELAY_MILLISECONDS);
|
}, TOAST_DELAY_MILLISECONDS);
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
|||||||
@@ -20,13 +20,16 @@ import app.revanced.extension.youtube.settings.Settings;
|
|||||||
public class ProgressBarDrawable extends Drawable {
|
public class ProgressBarDrawable extends Drawable {
|
||||||
|
|
||||||
private final Paint paint = new Paint();
|
private final Paint paint = new Paint();
|
||||||
|
{
|
||||||
|
paint.setColor(SeekbarColorPatch.getSeekbarColor());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void draw(@NonNull Canvas canvas) {
|
public void draw(@NonNull Canvas canvas) {
|
||||||
if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) {
|
if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
paint.setColor(SeekbarColorPatch.getSeekbarColor());
|
|
||||||
canvas.drawRect(getBounds(), paint);
|
canvas.drawRect(getBounds(), paint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.patches.theme;
|
package app.revanced.extension.youtube.patches.theme;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.clamp;
|
||||||
|
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@@ -59,7 +60,7 @@ public final class SeekbarColorPatch {
|
|||||||
* this is the color value of {@link Settings#SEEKBAR_CUSTOM_COLOR_PRIMARY}.
|
* this is the color value of {@link Settings#SEEKBAR_CUSTOM_COLOR_PRIMARY}.
|
||||||
* Otherwise this is {@link #ORIGINAL_SEEKBAR_COLOR}.
|
* Otherwise this is {@link #ORIGINAL_SEEKBAR_COLOR}.
|
||||||
*/
|
*/
|
||||||
private static int customSeekbarColor = ORIGINAL_SEEKBAR_COLOR;
|
private static final int customSeekbarColor;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom seekbar hue, saturation, and brightness values.
|
* Custom seekbar hue, saturation, and brightness values.
|
||||||
@@ -76,24 +77,25 @@ public final class SeekbarColorPatch {
|
|||||||
Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv);
|
Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv);
|
||||||
ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2];
|
ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2];
|
||||||
|
|
||||||
if (SEEKBAR_CUSTOM_COLOR_ENABLED) {
|
customSeekbarColor = SEEKBAR_CUSTOM_COLOR_ENABLED
|
||||||
loadCustomSeekbarColor();
|
? loadCustomSeekbarColor()
|
||||||
}
|
: ORIGINAL_SEEKBAR_COLOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void loadCustomSeekbarColor() {
|
private static int loadCustomSeekbarColor() {
|
||||||
try {
|
try {
|
||||||
customSeekbarColor = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_PRIMARY.get());
|
final int color = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_PRIMARY.get());
|
||||||
Color.colorToHSV(customSeekbarColor, customSeekbarColorHSV);
|
Color.colorToHSV(color, customSeekbarColorHSV);
|
||||||
|
customSeekbarColorGradient[0] = color;
|
||||||
customSeekbarColorGradient[0] = customSeekbarColor;
|
|
||||||
customSeekbarColorGradient[1] = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_ACCENT.get());
|
customSeekbarColorGradient[1] = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_ACCENT.get());
|
||||||
|
|
||||||
|
return color;
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Utils.showToastShort(str("revanced_seekbar_custom_color_invalid"));
|
Utils.showToastShort(str("revanced_seekbar_custom_color_invalid"));
|
||||||
Settings.SEEKBAR_CUSTOM_COLOR_PRIMARY.resetToDefault();
|
Settings.SEEKBAR_CUSTOM_COLOR_PRIMARY.resetToDefault();
|
||||||
Settings.SEEKBAR_CUSTOM_COLOR_ACCENT.resetToDefault();
|
Settings.SEEKBAR_CUSTOM_COLOR_ACCENT.resetToDefault();
|
||||||
|
|
||||||
loadCustomSeekbarColor();
|
return loadCustomSeekbarColor();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,6 +115,7 @@ public final class SeekbarColorPatch {
|
|||||||
: (int) channel3Bits;
|
: (int) channel3Bits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("SameParameterValue")
|
||||||
private static String get9BitStyleIdentifier(int color24Bit) {
|
private static String get9BitStyleIdentifier(int color24Bit) {
|
||||||
final int r3 = colorChannelTo3Bits(Color.red(color24Bit));
|
final int r3 = colorChannelTo3Bits(Color.red(color24Bit));
|
||||||
final int g3 = colorChannelTo3Bits(Color.green(color24Bit));
|
final int g3 = colorChannelTo3Bits(Color.green(color24Bit));
|
||||||
@@ -178,7 +181,7 @@ public final class SeekbarColorPatch {
|
|||||||
//noinspection ConstantConditions
|
//noinspection ConstantConditions
|
||||||
if (false) { // Set true to force slow animation for development.
|
if (false) { // Set true to force slow animation for development.
|
||||||
final int longAnimation = Utils.getResourceIdentifier(
|
final int longAnimation = Utils.getResourceIdentifier(
|
||||||
Utils.isDarkModeEnabled(Utils.getContext())
|
Utils.isDarkModeEnabled()
|
||||||
? "startup_animation_5s_30fps_dark"
|
? "startup_animation_5s_30fps_dark"
|
||||||
: "startup_animation_5s_30fps_light",
|
: "startup_animation_5s_30fps_light",
|
||||||
"raw");
|
"raw");
|
||||||
@@ -378,14 +381,4 @@ public final class SeekbarColorPatch {
|
|||||||
return originalColor;
|
return originalColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @noinspection SameParameterValue */
|
|
||||||
private static int clamp(int value, int lower, int upper) {
|
|
||||||
return Math.max(lower, Math.min(value, upper));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @noinspection SameParameterValue */
|
|
||||||
private static float clamp(float value, float lower, float upper) {
|
|
||||||
return Math.max(lower, Math.min(value, upper));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -21,8 +21,6 @@ import android.text.Spanned;
|
|||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.ImageSpan;
|
import android.text.style.ImageSpan;
|
||||||
import android.text.style.ReplacementSpan;
|
import android.text.style.ReplacementSpan;
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.util.TypedValue;
|
|
||||||
|
|
||||||
import androidx.annotation.GuardedBy;
|
import androidx.annotation.GuardedBy;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -120,16 +118,13 @@ public class ReturnYouTubeDislike {
|
|||||||
private static final ShapeDrawable leftSeparatorShape;
|
private static final ShapeDrawable leftSeparatorShape;
|
||||||
|
|
||||||
static {
|
static {
|
||||||
DisplayMetrics dp = Objects.requireNonNull(Utils.getContext()).getResources().getDisplayMetrics();
|
|
||||||
|
|
||||||
leftSeparatorBounds = new Rect(0, 0,
|
leftSeparatorBounds = new Rect(0, 0,
|
||||||
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp),
|
Utils.dipToPixels(1.2f),
|
||||||
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dp));
|
Utils.dipToPixels(14f));
|
||||||
final int middleSeparatorSize =
|
final int middleSeparatorSize = Utils.dipToPixels(3.7f);
|
||||||
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp);
|
|
||||||
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
|
middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize);
|
||||||
|
|
||||||
leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8.4f, dp);
|
leftSeparatorShapePaddingPixels = Utils.dipToPixels(8.4f);
|
||||||
|
|
||||||
leftSeparatorShape = new ShapeDrawable(new RectShape());
|
leftSeparatorShape = new ShapeDrawable(new RectShape());
|
||||||
leftSeparatorShape.setBounds(leftSeparatorBounds);
|
leftSeparatorShape.setBounds(leftSeparatorBounds);
|
||||||
@@ -235,7 +230,7 @@ public class ReturnYouTubeDislike {
|
|||||||
final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
|
final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get();
|
||||||
|
|
||||||
if (!compactLayout) {
|
if (!compactLayout) {
|
||||||
String leftSeparatorString = getTextDirectionString();
|
String leftSeparatorString = Utils.getTextDirectionString();
|
||||||
final Spannable leftSeparatorSpan;
|
final Spannable leftSeparatorSpan;
|
||||||
if (isRollingNumber) {
|
if (isRollingNumber) {
|
||||||
leftSeparatorSpan = new SpannableString(leftSeparatorString);
|
leftSeparatorSpan = new SpannableString(leftSeparatorString);
|
||||||
@@ -279,12 +274,6 @@ public class ReturnYouTubeDislike {
|
|||||||
return new SpannableString(builder);
|
return new SpannableString(builder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static @NonNull String getTextDirectionString() {
|
|
||||||
return Utils.isRightToLeftTextLayout()
|
|
||||||
? "\u200F" // u200F = right to left character
|
|
||||||
: "\u200E"; // u200E = left to right character
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return If the text is likely for a previously created likes/dislikes segmented span.
|
* @return If the text is likely for a previously created likes/dislikes segmented span.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package app.revanced.extension.youtube.returnyoutubedislike.ui;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.settings.preference.UrlLinkPreference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows tapping the RYD about preference to open the website.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class ReturnYouTubeDislikeAboutPreference extends UrlLinkPreference {
|
||||||
|
{
|
||||||
|
externalUrl = "https://returnyoutubedislike.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
public ReturnYouTubeDislikeAboutPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
public ReturnYouTubeDislikeAboutPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package app.revanced.extension.youtube.returnyoutubedislike.ui;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceCategory;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
||||||
|
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class ReturnYouTubeDislikeDebugStatsPreferenceCategory extends PreferenceCategory {
|
||||||
|
|
||||||
|
private static final boolean SHOW_RYD_DEBUG_STATS = BaseSettings.DEBUG.get();
|
||||||
|
|
||||||
|
private static String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) {
|
||||||
|
if (value == 0) {
|
||||||
|
return str(summaryStringZeroKey);
|
||||||
|
}
|
||||||
|
return str(summaryStringOneOrMoreKey, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String createMillisecondStringFromNumber(long number) {
|
||||||
|
return String.format(str("revanced_ryd_statistics_millisecond_text"), number);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReturnYouTubeDislikeDebugStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View onCreateView(ViewGroup parent) {
|
||||||
|
if (!SHOW_RYD_DEBUG_STATS) {
|
||||||
|
// Use an empty view to hide without removing.
|
||||||
|
return new View(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.onCreateView(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onAttachedToActivity() {
|
||||||
|
try {
|
||||||
|
super.onAttachedToActivity();
|
||||||
|
if (!SHOW_RYD_DEBUG_STATS) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Updating stats preferences");
|
||||||
|
removeAll();
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallResponseTimeAverage_title",
|
||||||
|
createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage())
|
||||||
|
);
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallResponseTimeMin_title",
|
||||||
|
createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin())
|
||||||
|
);
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallResponseTimeMax_title",
|
||||||
|
createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax())
|
||||||
|
);
|
||||||
|
|
||||||
|
String fetchCallTimeWaitingLastSummary;
|
||||||
|
final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast();
|
||||||
|
if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) {
|
||||||
|
fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary");
|
||||||
|
} else {
|
||||||
|
fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast);
|
||||||
|
}
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallResponseTimeLast_title",
|
||||||
|
fetchCallTimeWaitingLastSummary
|
||||||
|
);
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallCount_title",
|
||||||
|
createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(),
|
||||||
|
"revanced_ryd_statistics_getFetchCallCount_zero_summary",
|
||||||
|
"revanced_ryd_statistics_getFetchCallCount_non_zero_summary"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getFetchCallNumberOfFailures_title",
|
||||||
|
createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(),
|
||||||
|
"revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary",
|
||||||
|
"revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
addStatisticPreference(
|
||||||
|
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title",
|
||||||
|
createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(),
|
||||||
|
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary",
|
||||||
|
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onAttachedToActivity failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addStatisticPreference(String titleKey, String SummaryText) {
|
||||||
|
Preference statisticPreference = new Preference(getContext());
|
||||||
|
statisticPreference.setSelectable(false);
|
||||||
|
statisticPreference.setTitle(str(titleKey));
|
||||||
|
statisticPreference.setSummary(SummaryText);
|
||||||
|
addPreference(statisticPreference);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,13 +6,10 @@ import android.annotation.SuppressLint;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.preference.PreferenceFragment;
|
import android.preference.PreferenceFragment;
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.AppLanguage;
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
@@ -21,8 +18,6 @@ import app.revanced.extension.youtube.ThemeHelper;
|
|||||||
import app.revanced.extension.youtube.patches.VersionCheckPatch;
|
import app.revanced.extension.youtube.patches.VersionCheckPatch;
|
||||||
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
|
import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch;
|
||||||
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
|
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
|
||||||
import app.revanced.extension.youtube.settings.preference.ReturnYouTubeDislikePreferenceFragment;
|
|
||||||
import app.revanced.extension.youtube.settings.preference.SponsorBlockPreferenceFragment;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hooks LicenseActivity.
|
* Hooks LicenseActivity.
|
||||||
@@ -88,28 +83,15 @@ public class LicenseActivityHook {
|
|||||||
licenseActivity.setContentView(getResourceIdentifier(
|
licenseActivity.setContentView(getResourceIdentifier(
|
||||||
"revanced_settings_with_toolbar", "layout"));
|
"revanced_settings_with_toolbar", "layout"));
|
||||||
|
|
||||||
PreferenceFragment fragment;
|
// Sanity check.
|
||||||
String toolbarTitleResourceName;
|
String dataString = licenseActivity.getIntent().getDataString();
|
||||||
String dataString = Objects.requireNonNull(licenseActivity.getIntent().getDataString());
|
if (!"revanced_settings_intent".equals(dataString)) {
|
||||||
switch (dataString) {
|
Logger.printException(() -> "Unknown intent: " + dataString);
|
||||||
case "revanced_sb_settings_intent":
|
return;
|
||||||
toolbarTitleResourceName = "revanced_sb_settings_title";
|
|
||||||
fragment = new SponsorBlockPreferenceFragment();
|
|
||||||
break;
|
|
||||||
case "revanced_ryd_settings_intent":
|
|
||||||
toolbarTitleResourceName = "revanced_ryd_settings_title";
|
|
||||||
fragment = new ReturnYouTubeDislikePreferenceFragment();
|
|
||||||
break;
|
|
||||||
case "revanced_settings_intent":
|
|
||||||
toolbarTitleResourceName = "revanced_settings_title";
|
|
||||||
fragment = new ReVancedPreferenceFragment();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
Logger.printException(() -> "Unknown setting: " + dataString);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createToolbar(licenseActivity, toolbarTitleResourceName);
|
PreferenceFragment fragment = new ReVancedPreferenceFragment();
|
||||||
|
createToolbar(licenseActivity, fragment);
|
||||||
|
|
||||||
//noinspection deprecation
|
//noinspection deprecation
|
||||||
licenseActivity.getFragmentManager()
|
licenseActivity.getFragmentManager()
|
||||||
@@ -122,7 +104,7 @@ public class LicenseActivityHook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UseCompatLoadingForDrawables")
|
@SuppressLint("UseCompatLoadingForDrawables")
|
||||||
private static void createToolbar(Activity activity, String toolbarTitleResourceName) {
|
private static void createToolbar(Activity activity, PreferenceFragment fragment) {
|
||||||
// Replace dummy placeholder toolbar.
|
// Replace dummy placeholder toolbar.
|
||||||
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
// This is required to fix submenu title alignment issue with Android ASOP 15+
|
||||||
ViewGroup toolBarParent = activity.findViewById(
|
ViewGroup toolBarParent = activity.findViewById(
|
||||||
@@ -134,11 +116,9 @@ public class LicenseActivityHook {
|
|||||||
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
|
Toolbar toolbar = new Toolbar(toolBarParent.getContext());
|
||||||
toolbar.setBackgroundColor(ThemeHelper.getToolbarBackgroundColor());
|
toolbar.setBackgroundColor(ThemeHelper.getToolbarBackgroundColor());
|
||||||
toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable());
|
toolbar.setNavigationIcon(ReVancedPreferenceFragment.getBackButtonDrawable());
|
||||||
toolbar.setNavigationOnClickListener(view -> activity.onBackPressed());
|
toolbar.setTitle(getResourceIdentifier("revanced_settings_title", "string"));
|
||||||
toolbar.setTitle(getResourceIdentifier(toolbarTitleResourceName, "string"));
|
|
||||||
|
|
||||||
final int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16,
|
final int margin = Utils.dipToPixels(16);
|
||||||
Utils.getContext().getResources().getDisplayMetrics());
|
|
||||||
toolbar.setTitleMarginStart(margin);
|
toolbar.setTitleMarginStart(margin);
|
||||||
toolbar.setTitleMarginEnd(margin);
|
toolbar.setTitleMarginEnd(margin);
|
||||||
TextView toolbarTextView = Utils.getChildView(toolbar, false,
|
TextView toolbarTextView = Utils.getChildView(toolbar, false,
|
||||||
@@ -148,6 +128,11 @@ public class LicenseActivityHook {
|
|||||||
}
|
}
|
||||||
setToolbarLayoutParams(toolbar);
|
setToolbarLayoutParams(toolbar);
|
||||||
|
|
||||||
|
// Add Search Icon and EditText for ReVancedPreferenceFragment only.
|
||||||
|
if (fragment instanceof ReVancedPreferenceFragment) {
|
||||||
|
SearchViewController.addSearchViewComponents(activity, toolbar, (ReVancedPreferenceFragment) fragment);
|
||||||
|
}
|
||||||
|
|
||||||
toolBarParent.addView(toolbar, 0);
|
toolBarParent.addView(toolbar, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
package app.revanced.extension.youtube.settings;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.GradientDrawable;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.AutoCompleteTextView;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.SearchView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.AppLanguage;
|
||||||
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
|
import app.revanced.extension.shared.settings.StringSetting;
|
||||||
|
import app.revanced.extension.youtube.ThemeHelper;
|
||||||
|
import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller for managing the search view in ReVanced settings.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"deprecated", "DiscouragedApi"})
|
||||||
|
public class SearchViewController {
|
||||||
|
private static final int MAX_HISTORY_SIZE = 5;
|
||||||
|
|
||||||
|
private final SearchView searchView;
|
||||||
|
private final FrameLayout searchContainer;
|
||||||
|
private final Toolbar toolbar;
|
||||||
|
private final Activity activity;
|
||||||
|
private boolean isSearchActive;
|
||||||
|
private final CharSequence originalTitle;
|
||||||
|
private final Deque<String> searchHistory;
|
||||||
|
private final AutoCompleteTextView autoCompleteTextView;
|
||||||
|
private final boolean showSettingsSearchHistory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a background drawable for the SearchView with rounded corners.
|
||||||
|
*/
|
||||||
|
private static GradientDrawable createBackgroundDrawable(Context context) {
|
||||||
|
GradientDrawable background = new GradientDrawable();
|
||||||
|
background.setShape(GradientDrawable.RECTANGLE);
|
||||||
|
background.setCornerRadius(28 * context.getResources().getDisplayMetrics().density); // 28dp corner radius.
|
||||||
|
int baseColor = ThemeHelper.getBackgroundColor();
|
||||||
|
int adjustedColor = ThemeHelper.isDarkTheme()
|
||||||
|
? ThemeHelper.adjustColorBrightness(baseColor, 1.11f) // Lighten for dark theme.
|
||||||
|
: ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
|
||||||
|
background.setColor(adjustedColor);
|
||||||
|
return background;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a background drawable for suggestion items with rounded corners.
|
||||||
|
*/
|
||||||
|
private static GradientDrawable createSuggestionBackgroundDrawable(Context context) {
|
||||||
|
GradientDrawable background = new GradientDrawable();
|
||||||
|
background.setShape(GradientDrawable.RECTANGLE);
|
||||||
|
background.setCornerRadius(8 * context.getResources().getDisplayMetrics().density); // 8dp corner radius.
|
||||||
|
return background;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds search view components to the activity.
|
||||||
|
*/
|
||||||
|
public static void addSearchViewComponents(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
|
||||||
|
new SearchViewController(activity, toolbar, fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchViewController(Activity activity, Toolbar toolbar, ReVancedPreferenceFragment fragment) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.toolbar = toolbar;
|
||||||
|
this.originalTitle = toolbar.getTitle();
|
||||||
|
this.showSettingsSearchHistory = Settings.SETTINGS_SEARCH_HISTORY.get();
|
||||||
|
this.searchHistory = new LinkedList<>();
|
||||||
|
StringSetting searchEntries = Settings.SETTINGS_SEARCH_ENTRIES;
|
||||||
|
if (showSettingsSearchHistory) {
|
||||||
|
String entries = searchEntries.get();
|
||||||
|
if (!entries.isBlank()) {
|
||||||
|
searchHistory.addAll(Arrays.asList(entries.split("\n")));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Clear old saved history if the user turns off the feature.
|
||||||
|
searchEntries.resetToDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve SearchView and container from XML.
|
||||||
|
searchView = activity.findViewById(getResourceIdentifier(
|
||||||
|
"revanced_search_view", "id"));
|
||||||
|
searchContainer = activity.findViewById(getResourceIdentifier(
|
||||||
|
"revanced_search_view_container", "id"));
|
||||||
|
|
||||||
|
// Initialize AutoCompleteTextView.
|
||||||
|
autoCompleteTextView = searchView.findViewById(
|
||||||
|
searchView.getContext().getResources().getIdentifier(
|
||||||
|
"android:id/search_src_text", null, null));
|
||||||
|
|
||||||
|
// Set background and query hint.
|
||||||
|
searchView.setBackground(createBackgroundDrawable(toolbar.getContext()));
|
||||||
|
searchView.setQueryHint(str("revanced_settings_search_hint"));
|
||||||
|
|
||||||
|
// Configure RTL support based on app language.
|
||||||
|
AppLanguage appLanguage = BaseSettings.REVANCED_LANGUAGE.get();
|
||||||
|
if (Utils.isRightToLeftLocale(appLanguage.getLocale())) {
|
||||||
|
searchView.setTextDirection(View.TEXT_DIRECTION_RTL);
|
||||||
|
searchView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up search history suggestions.
|
||||||
|
if (showSettingsSearchHistory) {
|
||||||
|
setupSearchHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up query text listener.
|
||||||
|
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextSubmit(String query) {
|
||||||
|
try {
|
||||||
|
String queryTrimmed = query.trim();
|
||||||
|
if (!queryTrimmed.isEmpty()) {
|
||||||
|
saveSearchQuery(queryTrimmed);
|
||||||
|
}
|
||||||
|
// Hide suggestions on submit.
|
||||||
|
if (showSettingsSearchHistory && autoCompleteTextView != null) {
|
||||||
|
autoCompleteTextView.dismissDropDown();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onQueryTextSubmit failure", ex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onQueryTextChange(String newText) {
|
||||||
|
try {
|
||||||
|
Logger.printDebug(() -> "Search query: " + newText);
|
||||||
|
fragment.filterPreferences(newText);
|
||||||
|
// Prevent suggestions from showing during text input.
|
||||||
|
if (showSettingsSearchHistory && autoCompleteTextView != null) {
|
||||||
|
if (!newText.isEmpty()) {
|
||||||
|
autoCompleteTextView.dismissDropDown();
|
||||||
|
autoCompleteTextView.setThreshold(Integer.MAX_VALUE); // Disable autocomplete suggestions.
|
||||||
|
} else {
|
||||||
|
autoCompleteTextView.setThreshold(1); // Re-enable for empty input.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onQueryTextChange failure", ex);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set menu and search icon.
|
||||||
|
final int actionSearchId = getResourceIdentifier("action_search", "id");
|
||||||
|
toolbar.inflateMenu(getResourceIdentifier("revanced_search_menu", "menu"));
|
||||||
|
MenuItem searchItem = toolbar.getMenu().findItem(actionSearchId);
|
||||||
|
searchItem.setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme()
|
||||||
|
? "yt_outline_search_white_24"
|
||||||
|
: "yt_outline_search_black_24",
|
||||||
|
"drawable")).setTooltipText(null);
|
||||||
|
|
||||||
|
// Set menu item click listener.
|
||||||
|
toolbar.setOnMenuItemClickListener(item -> {
|
||||||
|
try {
|
||||||
|
if (item.getItemId() == actionSearchId) {
|
||||||
|
if (!isSearchActive) {
|
||||||
|
openSearch();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "menu click failure", ex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set navigation click listener.
|
||||||
|
toolbar.setNavigationOnClickListener(view -> {
|
||||||
|
try {
|
||||||
|
if (isSearchActive) {
|
||||||
|
closeSearch();
|
||||||
|
} else {
|
||||||
|
activity.onBackPressed();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "navigation click failure", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up the search history suggestions for the SearchView with custom adapter.
|
||||||
|
*/
|
||||||
|
private void setupSearchHistory() {
|
||||||
|
if (autoCompleteTextView != null) {
|
||||||
|
SearchHistoryAdapter adapter = new SearchHistoryAdapter(activity, new ArrayList<>(searchHistory));
|
||||||
|
autoCompleteTextView.setAdapter(adapter);
|
||||||
|
autoCompleteTextView.setThreshold(1); // Initial threshold for empty input.
|
||||||
|
autoCompleteTextView.setLongClickable(true);
|
||||||
|
|
||||||
|
// Show suggestions only when search bar is active and query is empty.
|
||||||
|
autoCompleteTextView.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (hasFocus && isSearchActive && autoCompleteTextView.getText().length() == 0) {
|
||||||
|
autoCompleteTextView.showDropDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves a search query to the search history.
|
||||||
|
* @param query The search query to save.
|
||||||
|
*/
|
||||||
|
private void saveSearchQuery(String query) {
|
||||||
|
if (!showSettingsSearchHistory) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
searchHistory.remove(query); // Remove if already exists to update position.
|
||||||
|
searchHistory.addFirst(query); // Add to the most recent.
|
||||||
|
|
||||||
|
// Remove extra old entries.
|
||||||
|
while (searchHistory.size() > MAX_HISTORY_SIZE) {
|
||||||
|
String last = searchHistory.removeLast();
|
||||||
|
Logger.printDebug(() -> "Removing search history query: " + last);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveSearchHistory();
|
||||||
|
|
||||||
|
updateSearchHistoryAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a search query from the search history.
|
||||||
|
* @param query The search query to remove.
|
||||||
|
*/
|
||||||
|
private void removeSearchQuery(String query) {
|
||||||
|
searchHistory.remove(query);
|
||||||
|
|
||||||
|
saveSearchHistory();
|
||||||
|
|
||||||
|
updateSearchHistoryAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the search history to the shared preferences.
|
||||||
|
*/
|
||||||
|
private void saveSearchHistory() {
|
||||||
|
Logger.printDebug(() -> "Saving search history: " + searchHistory);
|
||||||
|
|
||||||
|
Settings.SETTINGS_SEARCH_ENTRIES.save(
|
||||||
|
String.join("\n", searchHistory)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the search history adapter with the latest history.
|
||||||
|
*/
|
||||||
|
private void updateSearchHistoryAdapter() {
|
||||||
|
if (autoCompleteTextView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchHistoryAdapter adapter = (SearchHistoryAdapter) autoCompleteTextView.getAdapter();
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.clear();
|
||||||
|
adapter.addAll(searchHistory);
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the search view and shows the keyboard.
|
||||||
|
*/
|
||||||
|
private void openSearch() {
|
||||||
|
isSearchActive = true;
|
||||||
|
toolbar.getMenu().findItem(getResourceIdentifier(
|
||||||
|
"action_search", "id")).setVisible(false);
|
||||||
|
toolbar.setTitle("");
|
||||||
|
searchContainer.setVisibility(View.VISIBLE);
|
||||||
|
searchView.requestFocus();
|
||||||
|
|
||||||
|
// Show keyboard.
|
||||||
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
imm.showSoftInput(searchView, InputMethodManager.SHOW_IMPLICIT);
|
||||||
|
|
||||||
|
// Show suggestions with a slight delay.
|
||||||
|
if (showSettingsSearchHistory && autoCompleteTextView != null && autoCompleteTextView.getText().length() == 0) {
|
||||||
|
searchView.postDelayed(() -> {
|
||||||
|
if (isSearchActive && autoCompleteTextView.getText().length() == 0) {
|
||||||
|
autoCompleteTextView.showDropDown();
|
||||||
|
}
|
||||||
|
}, 100); // 100ms delay to ensure focus is stable.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the search view and hides the keyboard.
|
||||||
|
*/
|
||||||
|
private void closeSearch() {
|
||||||
|
isSearchActive = false;
|
||||||
|
toolbar.getMenu().findItem(getResourceIdentifier(
|
||||||
|
"action_search", "id"))
|
||||||
|
.setIcon(getResourceIdentifier(ThemeHelper.isDarkTheme()
|
||||||
|
? "yt_outline_search_white_24"
|
||||||
|
: "yt_outline_search_black_24",
|
||||||
|
"drawable")
|
||||||
|
).setVisible(true);
|
||||||
|
toolbar.setTitle(originalTitle);
|
||||||
|
searchContainer.setVisibility(View.GONE);
|
||||||
|
searchView.setQuery("", false);
|
||||||
|
|
||||||
|
// Hide keyboard.
|
||||||
|
InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
|
imm.hideSoftInputFromWindow(searchView.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom ArrayAdapter for search history.
|
||||||
|
*/
|
||||||
|
private class SearchHistoryAdapter extends ArrayAdapter<String> {
|
||||||
|
public SearchHistoryAdapter(Context context, List<String> history) {
|
||||||
|
super(context, 0, history);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) {
|
||||||
|
if (convertView == null) {
|
||||||
|
convertView = LinearLayout.inflate(getContext(), getResourceIdentifier(
|
||||||
|
"revanced_search_suggestion_item", "layout"), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply rounded corners programmatically.
|
||||||
|
convertView.setBackground(createSuggestionBackgroundDrawable(getContext()));
|
||||||
|
String query = getItem(position);
|
||||||
|
|
||||||
|
// Set query text.
|
||||||
|
TextView textView = convertView.findViewById(getResourceIdentifier(
|
||||||
|
"suggestion_text", "id"));
|
||||||
|
if (textView != null) {
|
||||||
|
textView.setText(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set click listener for inserting query into SearchView.
|
||||||
|
convertView.setOnClickListener(v -> {
|
||||||
|
searchView.setQuery(query, true); // Insert selected query and submit.
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set long click listener for deletion confirmation.
|
||||||
|
convertView.setOnLongClickListener(v -> {
|
||||||
|
new AlertDialog.Builder(activity)
|
||||||
|
.setTitle(query)
|
||||||
|
.setMessage(str("revanced_settings_search_remove_message"))
|
||||||
|
.setPositiveButton(android.R.string.ok,
|
||||||
|
(dialog, which) -> removeSearchQuery(query))
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,8 @@ import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehavi
|
|||||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
|
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP;
|
||||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY;
|
||||||
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE;
|
import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.settings.preference.SharedPrefCategory;
|
||||||
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
|
import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider.SwipeOverlayStyle;
|
||||||
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@@ -61,12 +63,11 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting CUSTOM_SPEED_MENU = new BooleanSetting("revanced_custom_speed_menu", TRUE);
|
public static final BooleanSetting CUSTOM_SPEED_MENU = new BooleanSetting("revanced_custom_speed_menu", TRUE);
|
||||||
public static final FloatSetting PLAYBACK_SPEED_DEFAULT = new FloatSetting("revanced_playback_speed_default", -2.0f);
|
public static final FloatSetting PLAYBACK_SPEED_DEFAULT = new FloatSetting("revanced_playback_speed_default", -2.0f);
|
||||||
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds",
|
public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds",
|
||||||
"0.25\n0.5\n0.75\n0.9\n0.95\n1.0\n1.05\n1.1\n1.25\n1.5\n1.75\n2.0\n3.0\n4.0\n5.0", true);
|
"0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.5\n3.0\n4.0\n5.0\n6.0\n7.0\n8.0", true);
|
||||||
// Audio
|
// Audio
|
||||||
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", FALSE, new ForceOriginalAudioAvailability());
|
public static final BooleanSetting FORCE_ORIGINAL_AUDIO = new BooleanSetting("revanced_force_original_audio", FALSE, new ForceOriginalAudioAvailability());
|
||||||
|
|
||||||
// Ads
|
// Ads
|
||||||
public static final BooleanSetting HIDE_BUTTONED_ADS = new BooleanSetting("revanced_hide_buttoned_ads", TRUE);
|
|
||||||
public static final BooleanSetting HIDE_END_SCREEN_STORE_BANNER = new BooleanSetting("revanced_hide_end_screen_store_banner", TRUE, true);
|
public static final BooleanSetting HIDE_END_SCREEN_STORE_BANNER = new BooleanSetting("revanced_hide_end_screen_store_banner", TRUE, true);
|
||||||
public static final BooleanSetting HIDE_FULLSCREEN_ADS = new BooleanSetting("revanced_hide_fullscreen_ads", TRUE);
|
public static final BooleanSetting HIDE_FULLSCREEN_ADS = new BooleanSetting("revanced_hide_fullscreen_ads", TRUE);
|
||||||
public static final BooleanSetting HIDE_GENERAL_ADS = new BooleanSetting("revanced_hide_general_ads", TRUE);
|
public static final BooleanSetting HIDE_GENERAL_ADS = new BooleanSetting("revanced_hide_general_ads", TRUE);
|
||||||
@@ -103,8 +104,9 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_MOVIES_SECTION = new BooleanSetting("revanced_hide_movies_section", TRUE);
|
public static final BooleanSetting HIDE_MOVIES_SECTION = new BooleanSetting("revanced_hide_movies_section", TRUE);
|
||||||
public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", TRUE);
|
public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", TRUE);
|
||||||
public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE);
|
public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE);
|
||||||
public static final BooleanSetting HIDE_SEARCH_RESULT_RECOMMENDATIONS = new BooleanSetting("revanced_hide_search_result_recommendations", TRUE);
|
public static final BooleanSetting HIDE_SEARCH_RESULT_RECOMMENDATION_LABELS = new BooleanSetting("revanced_hide_search_result_recommendation_labels", TRUE);
|
||||||
public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true);
|
public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true);
|
||||||
|
public static final BooleanSetting HIDE_TICKET_SHELF = new BooleanSetting("revanced_hide_ticket_shelf", FALSE);
|
||||||
// Alternative thumbnails
|
// Alternative thumbnails
|
||||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
|
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL);
|
||||||
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscription", ThumbnailOption.ORIGINAL);
|
public static final EnumSetting<ThumbnailOption> ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscription", ThumbnailOption.ORIGINAL);
|
||||||
@@ -139,6 +141,7 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_EMERGENCY_BOX = new BooleanSetting("revanced_hide_emergency_box", TRUE);
|
public static final BooleanSetting HIDE_EMERGENCY_BOX = new BooleanSetting("revanced_hide_emergency_box", TRUE);
|
||||||
public static final BooleanSetting HIDE_ENDSCREEN_CARDS = new BooleanSetting("revanced_hide_endscreen_cards", FALSE);
|
public static final BooleanSetting HIDE_ENDSCREEN_CARDS = new BooleanSetting("revanced_hide_endscreen_cards", FALSE);
|
||||||
public static final BooleanSetting HIDE_END_SCREEN_SUGGESTED_VIDEO = new BooleanSetting("revanced_end_screen_suggested_video", FALSE, true);
|
public static final BooleanSetting HIDE_END_SCREEN_SUGGESTED_VIDEO = new BooleanSetting("revanced_end_screen_suggested_video", FALSE, true);
|
||||||
|
public static final BooleanSetting HIDE_RELATED_VIDEO_OVERLAY = new BooleanSetting("revanced_hide_related_video_overlay", FALSE, true);
|
||||||
public static final BooleanSetting HIDE_HIDE_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_channel_guidelines", TRUE);
|
public static final BooleanSetting HIDE_HIDE_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_channel_guidelines", TRUE);
|
||||||
public static final BooleanSetting HIDE_INFO_PANELS = new BooleanSetting("revanced_hide_info_panels", TRUE);
|
public static final BooleanSetting HIDE_INFO_PANELS = new BooleanSetting("revanced_hide_info_panels", TRUE);
|
||||||
public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE);
|
public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE);
|
||||||
@@ -182,6 +185,7 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting HIDE_COMMENTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_comments_thanks_button", TRUE);
|
public static final BooleanSetting HIDE_COMMENTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_comments_thanks_button", TRUE);
|
||||||
// Description
|
// Description
|
||||||
public static final BooleanSetting HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION = new BooleanSetting("revanced_hide_ai_generated_video_summary_section", FALSE);
|
public static final BooleanSetting HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION = new BooleanSetting("revanced_hide_ai_generated_video_summary_section", FALSE);
|
||||||
|
public static final BooleanSetting HIDE_ASK_SECTION = new BooleanSetting("revanced_hide_ask_section", FALSE);
|
||||||
public static final BooleanSetting HIDE_ATTRIBUTES_SECTION = new BooleanSetting("revanced_hide_attributes_section", FALSE);
|
public static final BooleanSetting HIDE_ATTRIBUTES_SECTION = new BooleanSetting("revanced_hide_attributes_section", FALSE);
|
||||||
public static final BooleanSetting HIDE_CHAPTERS_SECTION = new BooleanSetting("revanced_hide_chapters_section", TRUE);
|
public static final BooleanSetting HIDE_CHAPTERS_SECTION = new BooleanSetting("revanced_hide_chapters_section", TRUE);
|
||||||
public static final BooleanSetting HIDE_HOW_THIS_WAS_MADE_SECTION = new BooleanSetting("revanced_hide_how_this_was_made_section", FALSE);
|
public static final BooleanSetting HIDE_HOW_THIS_WAS_MADE_SECTION = new BooleanSetting("revanced_hide_how_this_was_made_section", FALSE);
|
||||||
@@ -217,6 +221,8 @@ public class Settings extends BaseSettings {
|
|||||||
|
|
||||||
// General layout
|
// General layout
|
||||||
public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("revanced_restore_old_settings_menus", FALSE, true);
|
public static final BooleanSetting RESTORE_OLD_SETTINGS_MENUS = new BooleanSetting("revanced_restore_old_settings_menus", FALSE, true);
|
||||||
|
public static final BooleanSetting SETTINGS_SEARCH_HISTORY = new BooleanSetting("revanced_settings_search_history", TRUE, true);
|
||||||
|
public static final StringSetting SETTINGS_SEARCH_ENTRIES = new StringSetting("revanced_settings_search_entries", "", true);
|
||||||
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
|
public static final EnumSetting<FormFactor> CHANGE_FORM_FACTOR = new EnumSetting<>("revanced_change_form_factor", FormFactor.DEFAULT, true, "revanced_change_form_factor_user_dialog_message");
|
||||||
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
|
public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true);
|
||||||
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
|
public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true);
|
||||||
@@ -303,16 +309,16 @@ public class Settings extends BaseSettings {
|
|||||||
public static final BooleanSetting AUTO_REPEAT = new BooleanSetting("revanced_auto_repeat", FALSE);
|
public static final BooleanSetting AUTO_REPEAT = new BooleanSetting("revanced_auto_repeat", FALSE);
|
||||||
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
|
public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE);
|
||||||
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
|
public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false);
|
||||||
public static final BooleanSetting DISABLE_ZOOM_HAPTICS = new BooleanSetting("revanced_disable_zoom_haptics", TRUE);
|
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE);
|
||||||
|
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_PRECISE_SEEKING = new BooleanSetting("revanced_disable_haptic_feedback_precise_seeking", FALSE);
|
||||||
|
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO = new BooleanSetting("revanced_disable_haptic_feedback_seek_undo", FALSE);
|
||||||
|
public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_ZOOM = new BooleanSetting("revanced_disable_haptic_feedback_zoom", FALSE);
|
||||||
public static final BooleanSetting EXTERNAL_BROWSER = new BooleanSetting("revanced_external_browser", TRUE, true);
|
public static final BooleanSetting EXTERNAL_BROWSER = new BooleanSetting("revanced_external_browser", TRUE, true);
|
||||||
public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE);
|
public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE);
|
||||||
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true,
|
public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true,
|
||||||
"revanced_spoof_device_dimensions_user_dialog_message");
|
"revanced_spoof_device_dimensions_user_dialog_message");
|
||||||
/**
|
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
|
||||||
* When enabled, share the debug logs with care.
|
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
|
||||||
* The buffer contains select user data, including the client ip address and information that could identify the end user.
|
|
||||||
*/
|
|
||||||
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, parent(BaseSettings.DEBUG));
|
|
||||||
|
|
||||||
// Swipe controls
|
// Swipe controls
|
||||||
public static final BooleanSetting SWIPE_CHANGE_VIDEO = new BooleanSetting("revanced_swipe_change_video", FALSE, true);
|
public static final BooleanSetting SWIPE_CHANGE_VIDEO = new BooleanSetting("revanced_swipe_change_video", FALSE, true);
|
||||||
@@ -331,28 +337,30 @@ public class Settings extends BaseSettings {
|
|||||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
||||||
public static final IntegerSetting SWIPE_OVERLAY_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true,
|
public static final IntegerSetting SWIPE_OVERLAY_OPACITY = new IntegerSetting("revanced_swipe_overlay_background_opacity", 60, true,
|
||||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
||||||
public static final StringSetting SWIPE_OVERLAY_PROGRESS_COLOR = new StringSetting("revanced_swipe_overlay_progress_color", "#FFFFFF", true,
|
public static final StringSetting SWIPE_OVERLAY_BRIGHTNESS_COLOR = new StringSetting("revanced_swipe_overlay_progress_brightness_color", "#FFFFFF", true,
|
||||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
parent(SWIPE_BRIGHTNESS));
|
||||||
|
public static final StringSetting SWIPE_OVERLAY_VOLUME_COLOR = new StringSetting("revanced_swipe_overlay_progress_volume_color", "#FFFFFF", true,
|
||||||
|
parent(SWIPE_VOLUME));
|
||||||
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true,
|
public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true,
|
||||||
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME));
|
||||||
public static final BooleanSetting SWIPE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_swipe_save_and_restore_brightness", TRUE, true, parent(SWIPE_BRIGHTNESS));
|
public static final BooleanSetting SWIPE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_swipe_save_and_restore_brightness", TRUE, true,
|
||||||
|
parent(SWIPE_BRIGHTNESS));
|
||||||
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1f);
|
public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1f);
|
||||||
public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true, parent(SWIPE_BRIGHTNESS));
|
public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true,
|
||||||
|
parent(SWIPE_BRIGHTNESS));
|
||||||
|
|
||||||
// ReturnYoutubeDislike
|
// ReturnYoutubeDislike
|
||||||
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE);
|
public static final BooleanSetting RYD_ENABLED = new BooleanSetting("revanced_ryd_enabled", TRUE);
|
||||||
public static final StringSetting RYD_USER_ID = new StringSetting("ryd_user_id", "", false, false);
|
public static final StringSetting RYD_USER_ID = new StringSetting("revanced_ryd_user_id", "", false, false);
|
||||||
public static final BooleanSetting RYD_SHORTS = new BooleanSetting("ryd_shorts", TRUE, parent(RYD_ENABLED));
|
public static final BooleanSetting RYD_SHORTS = new BooleanSetting("revanced_ryd_shorts", TRUE, parent(RYD_ENABLED));
|
||||||
public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("ryd_dislike_percentage", FALSE, parent(RYD_ENABLED));
|
public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("revanced_ryd_dislike_percentage", FALSE, true, parent(RYD_ENABLED));
|
||||||
public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("ryd_compact_layout", FALSE, parent(RYD_ENABLED));
|
public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("revanced_ryd_compact_layout", FALSE, true, parent(RYD_ENABLED));
|
||||||
public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("ryd_estimated_like", TRUE, parent(RYD_ENABLED));
|
public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("revanced_ryd_estimated_like", TRUE, true, parent(RYD_ENABLED));
|
||||||
public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("ryd_toast_on_connection_error", TRUE, parent(RYD_ENABLED));
|
public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("revanced_ryd_toast_on_connection_error", TRUE, parent(RYD_ENABLED));
|
||||||
|
|
||||||
// SponsorBlock
|
// SponsorBlock
|
||||||
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
|
public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE);
|
||||||
/**
|
/** Do not use id setting directly. Instead use {@link SponsorBlockSettings}. */
|
||||||
* Do not use directly, instead use {@link SponsorBlockSettings}
|
|
||||||
*/
|
|
||||||
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "");
|
public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", "");
|
||||||
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
|
public static final IntegerSetting SB_CREATE_NEW_SEGMENT_STEP = new IntegerSetting("sb_create_new_segment_step", 150, parent(SB_ENABLED));
|
||||||
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
|
public static final BooleanSetting SB_VOTING_BUTTON = new BooleanSetting("sb_voting_button", FALSE, parent(SB_ENABLED));
|
||||||
@@ -416,12 +424,10 @@ public class Settings extends BaseSettings {
|
|||||||
// region Migration
|
// region Migration
|
||||||
|
|
||||||
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_BUTTONS, HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS);
|
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_BUTTONS, HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS);
|
||||||
|
|
||||||
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER, HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER);
|
migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER, HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER);
|
||||||
|
|
||||||
migrateOldSettingToNew(DEPRECATED_DISABLE_SUGGESTED_VIDEO_END_SCREEN, HIDE_END_SCREEN_SUGGESTED_VIDEO);
|
migrateOldSettingToNew(DEPRECATED_DISABLE_SUGGESTED_VIDEO_END_SCREEN, HIDE_END_SCREEN_SUGGESTED_VIDEO);
|
||||||
|
|
||||||
migrateOldSettingToNew(DEPRECATED_RESTORE_OLD_VIDEO_QUALITY_MENU, ADVANCED_VIDEO_QUALITY_MENU);
|
migrateOldSettingToNew(DEPRECATED_RESTORE_OLD_VIDEO_QUALITY_MENU, ADVANCED_VIDEO_QUALITY_MENU);
|
||||||
|
migrateOldSettingToNew(DEPRECATED_AUTO_CAPTIONS, DISABLE_AUTO_CAPTIONS);
|
||||||
|
|
||||||
// Migrate renamed enum.
|
// Migrate renamed enum.
|
||||||
//noinspection deprecation
|
//noinspection deprecation
|
||||||
@@ -464,10 +470,15 @@ public class Settings extends BaseSettings {
|
|||||||
SPOOF_APP_VERSION_TARGET.resetToDefault();
|
SPOOF_APP_VERSION_TARGET.resetToDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!DEPRECATED_AUTO_CAPTIONS.isSetToDefault()) {
|
// RYD requires manually migrating old settings since the lack of
|
||||||
DISABLE_AUTO_CAPTIONS.save(true);
|
// a "revanced_" on the old setting causes duplicate key exceptions during export.
|
||||||
DEPRECATED_AUTO_CAPTIONS.resetToDefault();
|
SharedPrefCategory revancedPrefs = Setting.preferences;
|
||||||
}
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_USER_ID, "ryd_user_id");
|
||||||
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_ENABLED, "ryd_enabled");
|
||||||
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_DISLIKE_PERCENTAGE, "ryd_dislike_percentage");
|
||||||
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_COMPACT_LAYOUT, "ryd_compact_layout");
|
||||||
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_ESTIMATED_LIKE, "ryd_estimated_like");
|
||||||
|
Setting.migrateFromOldPreferences(revancedPrefs, RYD_TOAST_ON_CONNECTION_ERROR, "ryd_toast_on_connection_error");
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@@ -478,4 +489,3 @@ public class Settings extends BaseSettings {
|
|||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,15 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows tapping the DeArrow about preference to open the DeArrow website.
|
* Allows tapping the DeArrow about preference to open the DeArrow website.
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings({"unused", "deprecation"})
|
@SuppressWarnings("unused")
|
||||||
public class AlternativeThumbnailsAboutDeArrowPreference extends Preference {
|
public class AlternativeThumbnailsAboutDeArrowPreference extends UrlLinkPreference {
|
||||||
{
|
{
|
||||||
setOnPreferenceClickListener(pref -> {
|
externalUrl = "https://dearrow.ajay.app";
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
||||||
i.setData(Uri.parse("https://dearrow.ajay.app"));
|
|
||||||
pref.getContext().startActivity(i);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom preference that clears the ReVanced debug log buffer when clicked.
|
||||||
|
* Invokes the {@link LogBufferManager#clearLogBuffer} method.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class ClearLogBufferPreference extends Preference {
|
||||||
|
|
||||||
|
{
|
||||||
|
setOnPreferenceClickListener(pref -> {
|
||||||
|
LogBufferManager.clearLogBuffer();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
public ClearLogBufferPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
public ClearLogBufferPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
public ClearLogBufferPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.sf;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.preference.ListPreference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom video speeds used by {@link CustomPlaybackSpeedPatch}.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public final class CustomVideoSpeedListPreference extends ListPreference {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a settings preference list with the available playback speeds.
|
||||||
|
*/
|
||||||
|
private void initializeEntryValues() {
|
||||||
|
float[] customPlaybackSpeeds = CustomPlaybackSpeedPatch.customPlaybackSpeeds;
|
||||||
|
final int numberOfEntries = customPlaybackSpeeds.length + 1;
|
||||||
|
String[] preferenceListEntries = new String[numberOfEntries];
|
||||||
|
String[] preferenceListEntryValues = new String[numberOfEntries];
|
||||||
|
|
||||||
|
// Auto speed (same behavior as unpatched).
|
||||||
|
preferenceListEntries[0] = sf("revanced_custom_playback_speeds_auto").toString();
|
||||||
|
preferenceListEntryValues[0] = String.valueOf(Settings.PLAYBACK_SPEED_DEFAULT.defaultValue);
|
||||||
|
|
||||||
|
int i = 1;
|
||||||
|
for (float speed : customPlaybackSpeeds) {
|
||||||
|
String speedString = String.valueOf(speed);
|
||||||
|
preferenceListEntries[i] = speedString + "x";
|
||||||
|
preferenceListEntryValues[i] = speedString;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEntries(preferenceListEntries);
|
||||||
|
setEntryValues(preferenceListEntryValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
initializeEntryValues();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomVideoSpeedListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomVideoSpeedListPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomVideoSpeedListPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CustomVideoSpeedListPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import app.revanced.extension.shared.settings.preference.LogBufferManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A custom preference that triggers exporting ReVanced debug logs to the clipboard when clicked.
|
||||||
|
* Invokes the {@link LogBufferManager#exportToClipboard} method.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"deprecation", "unused"})
|
||||||
|
public class ExportLogToClipboardPreference extends Preference {
|
||||||
|
|
||||||
|
{
|
||||||
|
setOnPreferenceClickListener(pref -> {
|
||||||
|
LogBufferManager.exportToClipboard();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
public ExportLogToClipboardPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
public ExportLogToClipboardPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
public ExportLogToClipboardPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,12 +19,15 @@ public class HtmlPreference extends Preference {
|
|||||||
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
super(context, attrs, defStyleAttr, defStyleRes);
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
super(context, attrs, defStyleAttr);
|
super(context, attrs, defStyleAttr);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HtmlPreference(Context context, AttributeSet attrs) {
|
public HtmlPreference(Context context, AttributeSet attrs) {
|
||||||
super(context, attrs);
|
super(context, attrs);
|
||||||
}
|
}
|
||||||
|
|
||||||
public HtmlPreference(Context context) {
|
public HtmlPreference(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
@@ -9,34 +10,65 @@ import android.graphics.drawable.Drawable;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.preference.ListPreference;
|
import android.preference.ListPreference;
|
||||||
import android.preference.Preference;
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceCategory;
|
||||||
|
import android.preference.PreferenceGroup;
|
||||||
import android.preference.PreferenceScreen;
|
import android.preference.PreferenceScreen;
|
||||||
import android.util.Pair;
|
import android.preference.SwitchPreference;
|
||||||
import android.util.TypedValue;
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.BackgroundColorSpan;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowInsets;
|
import android.view.WindowInsets;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toolbar;
|
import android.widget.Toolbar;
|
||||||
|
|
||||||
|
import androidx.annotation.CallSuper;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Deque;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
import app.revanced.extension.shared.settings.BaseSettings;
|
||||||
import app.revanced.extension.shared.settings.EnumSetting;
|
|
||||||
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
|
import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment;
|
||||||
|
import app.revanced.extension.shared.settings.preference.NoTitlePreferenceCategory;
|
||||||
import app.revanced.extension.youtube.ThemeHelper;
|
import app.revanced.extension.youtube.ThemeHelper;
|
||||||
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
|
||||||
import app.revanced.extension.youtube.settings.LicenseActivityHook;
|
import app.revanced.extension.youtube.settings.LicenseActivityHook;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Preference fragment for ReVanced settings.
|
* Preference fragment for ReVanced settings.
|
||||||
*
|
|
||||||
* @noinspection deprecation
|
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main PreferenceScreen used to display the current set of preferences.
|
||||||
|
* This screen is manipulated during initialization and filtering to show or hide preferences.
|
||||||
|
*/
|
||||||
|
private PreferenceScreen preferenceScreen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A copy of the original PreferenceScreen created during initialization.
|
||||||
|
* Used to restore the preference structure to its initial state after filtering or other modifications.
|
||||||
|
*/
|
||||||
|
private PreferenceScreen originalPreferenceScreen;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for searching preferences. A Collection of all preferences including nested preferences.
|
||||||
|
* Root preferences are excluded (no need to search what's on the root screen),
|
||||||
|
* but their sub preferences are included.
|
||||||
|
*/
|
||||||
|
private final List<AbstractPreferenceSearchData<?>> allPreferences = new ArrayList<>();
|
||||||
|
|
||||||
@SuppressLint("UseCompatLoadingForDrawables")
|
@SuppressLint("UseCompatLoadingForDrawables")
|
||||||
public static Drawable getBackButtonDrawable() {
|
public static Drawable getBackButtonDrawable() {
|
||||||
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
|
final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme()
|
||||||
@@ -47,85 +79,140 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sorts a preference list by menu entries, but preserves the first value as the first entry.
|
* Initializes the preference fragment, copying the original screen to allow full restoration.
|
||||||
*
|
|
||||||
* @noinspection SameParameterValue
|
|
||||||
*/
|
*/
|
||||||
private static void sortListPreferenceByValues(ListPreference listPreference, int firstEntriesToPreserve) {
|
|
||||||
CharSequence[] entries = listPreference.getEntries();
|
|
||||||
CharSequence[] entryValues = listPreference.getEntryValues();
|
|
||||||
final int entrySize = entries.length;
|
|
||||||
|
|
||||||
if (entrySize != entryValues.length) {
|
|
||||||
// Xml array declaration has a missing/extra entry.
|
|
||||||
throw new IllegalStateException();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Pair<String, String>> firstPairs = new ArrayList<>(firstEntriesToPreserve);
|
|
||||||
List<Pair<String, String>> pairsToSort = new ArrayList<>(entrySize);
|
|
||||||
|
|
||||||
for (int i = 0; i < entrySize; i++) {
|
|
||||||
Pair<String, String> pair = new Pair<>(entries[i].toString(), entryValues[i].toString());
|
|
||||||
if (i < firstEntriesToPreserve) {
|
|
||||||
firstPairs.add(pair);
|
|
||||||
} else {
|
|
||||||
pairsToSort.add(pair);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pairsToSort.sort((pair1, pair2)
|
|
||||||
-> pair1.first.compareToIgnoreCase(pair2.first));
|
|
||||||
|
|
||||||
CharSequence[] sortedEntries = new CharSequence[entrySize];
|
|
||||||
CharSequence[] sortedEntryValues = new CharSequence[entrySize];
|
|
||||||
|
|
||||||
int i = 0;
|
|
||||||
for (Pair<String, String> pair : firstPairs) {
|
|
||||||
sortedEntries[i] = pair.first;
|
|
||||||
sortedEntryValues[i] = pair.second;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (Pair<String, String> pair : pairsToSort) {
|
|
||||||
sortedEntries[i] = pair.first;
|
|
||||||
sortedEntryValues[i] = pair.second;
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
listPreference.setEntries(sortedEntries);
|
|
||||||
listPreference.setEntryValues(sortedEntryValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initialize() {
|
protected void initialize() {
|
||||||
super.initialize();
|
super.initialize();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setPreferenceScreenToolbar(getPreferenceScreen());
|
preferenceScreen = getPreferenceScreen();
|
||||||
|
Utils.sortPreferenceGroups(preferenceScreen);
|
||||||
|
|
||||||
// If the preference was included, then initialize it based on the available playback speed.
|
// Store the original structure for restoration after filtering.
|
||||||
Preference preference = findPreference(Settings.PLAYBACK_SPEED_DEFAULT.key);
|
originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getContext());
|
||||||
if (preference instanceof ListPreference playbackPreference) {
|
for (int i = 0, count = preferenceScreen.getPreferenceCount(); i < count; i++) {
|
||||||
CustomPlaybackSpeedPatch.initializeListPreference(playbackPreference);
|
originalPreferenceScreen.addPreference(preferenceScreen.getPreference(i));
|
||||||
}
|
}
|
||||||
|
|
||||||
sortPreferenceListMenu(Settings.CHANGE_START_PAGE);
|
setPreferenceScreenToolbar(preferenceScreen);
|
||||||
sortPreferenceListMenu(Settings.SPOOF_VIDEO_STREAMS_LANGUAGE);
|
|
||||||
sortPreferenceListMenu(BaseSettings.REVANCED_LANGUAGE);
|
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "initialize failure", ex);
|
Logger.printException(() -> "initialize failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sortPreferenceListMenu(EnumSetting<?> setting) {
|
/**
|
||||||
Preference preference = findPreference(setting.key);
|
* Called when the fragment starts, ensuring all preferences are collected after initialization.
|
||||||
if (preference instanceof ListPreference languagePreference) {
|
*/
|
||||||
sortListPreferenceByValues(languagePreference, 1);
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
try {
|
||||||
|
if (allPreferences.isEmpty()) {
|
||||||
|
// Must collect preferences on start and not in initialize since
|
||||||
|
// legacy SB settings are not loaded yet.
|
||||||
|
Logger.printDebug(() -> "Collecting preferences to search");
|
||||||
|
|
||||||
|
// Do not show root menu preferences in search results.
|
||||||
|
// Instead search for everything that's not shown when search is not active.
|
||||||
|
collectPreferences(preferenceScreen, 1, 0);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onStart failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively collects all preferences from the screen or group.
|
||||||
|
* @param includeDepth Menu depth to start including preferences.
|
||||||
|
* A value of 0 adds all preferences.
|
||||||
|
*/
|
||||||
|
private void collectPreferences(PreferenceGroup group, int includeDepth, int currentDepth) {
|
||||||
|
for (int i = 0, count = group.getPreferenceCount(); i < count; i++) {
|
||||||
|
Preference preference = group.getPreference(i);
|
||||||
|
if (includeDepth <= currentDepth && !(preference instanceof PreferenceCategory)
|
||||||
|
&& !(preference instanceof SponsorBlockPreferenceGroup)) {
|
||||||
|
|
||||||
|
AbstractPreferenceSearchData<?> data;
|
||||||
|
if (preference instanceof SwitchPreference switchPref) {
|
||||||
|
data = new SwitchPreferenceSearchData(switchPref);
|
||||||
|
} else if (preference instanceof ListPreference listPref) {
|
||||||
|
data = new ListPreferenceSearchData(listPref);
|
||||||
|
} else {
|
||||||
|
data = new PreferenceSearchData(preference);
|
||||||
|
}
|
||||||
|
|
||||||
|
allPreferences.add(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preference instanceof PreferenceGroup subGroup) {
|
||||||
|
collectPreferences(subGroup, includeDepth, currentDepth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters the preferences using the given query string and applies highlighting.
|
||||||
|
*/
|
||||||
|
public void filterPreferences(String query) {
|
||||||
|
preferenceScreen.removeAll();
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(query)) {
|
||||||
|
// Restore original preferences and their titles/summaries/entries.
|
||||||
|
for (int i = 0, count = originalPreferenceScreen.getPreferenceCount(); i < count; i++) {
|
||||||
|
preferenceScreen.addPreference(originalPreferenceScreen.getPreference(i));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (AbstractPreferenceSearchData<?> data : allPreferences) {
|
||||||
|
data.clearHighlighting();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigation path -> Category
|
||||||
|
Map<String, PreferenceCategory> categoryMap = new HashMap<>();
|
||||||
|
String queryLower = Utils.removePunctuationToLowercase(query);
|
||||||
|
|
||||||
|
Pattern queryPattern = Pattern.compile(Pattern.quote(Utils.removePunctuationToLowercase(query)),
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
for (AbstractPreferenceSearchData<?> data : allPreferences) {
|
||||||
|
if (data.matchesSearchQuery(queryLower)) {
|
||||||
|
data.applyHighlighting(queryLower, queryPattern);
|
||||||
|
|
||||||
|
String navigationPath = data.navigationPath;
|
||||||
|
PreferenceCategory group = categoryMap.computeIfAbsent(navigationPath, key -> {
|
||||||
|
PreferenceCategory newGroup = new PreferenceCategory(preferenceScreen.getContext());
|
||||||
|
newGroup.setTitle(navigationPath);
|
||||||
|
preferenceScreen.addPreference(newGroup);
|
||||||
|
return newGroup;
|
||||||
|
});
|
||||||
|
group.addPreference(data.preference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show 'No results found' if search results are empty.
|
||||||
|
if (categoryMap.isEmpty()) {
|
||||||
|
Preference noResultsPreference = new Preference(preferenceScreen.getContext());
|
||||||
|
noResultsPreference.setTitle(str("revanced_settings_search_no_results_title", query));
|
||||||
|
noResultsPreference.setSummary(str("revanced_settings_search_no_results_summary"));
|
||||||
|
noResultsPreference.setSelectable(false);
|
||||||
|
// Set icon for the placeholder preference.
|
||||||
|
noResultsPreference.setLayoutResource(getResourceIdentifier(
|
||||||
|
"revanced_preference_with_icon_no_search_result", "layout"));
|
||||||
|
noResultsPreference.setIcon(getResourceIdentifier(
|
||||||
|
ThemeHelper.isDarkTheme() ? "yt_outline_search_white_24" : "yt_outline_search_black_24",
|
||||||
|
"drawable"));
|
||||||
|
preferenceScreen.addPreference(noResultsPreference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets toolbar for all nested preference screens.
|
||||||
|
*/
|
||||||
private void setPreferenceScreenToolbar(PreferenceScreen parentScreen) {
|
private void setPreferenceScreenToolbar(PreferenceScreen parentScreen) {
|
||||||
for (int i = 0, preferenceCount = parentScreen.getPreferenceCount(); i < preferenceCount; i++) {
|
for (int i = 0, count = parentScreen.getPreferenceCount(); i < count; i++) {
|
||||||
Preference childPreference = parentScreen.getPreference(i);
|
Preference childPreference = parentScreen.getPreference(i);
|
||||||
if (childPreference instanceof PreferenceScreen) {
|
if (childPreference instanceof PreferenceScreen) {
|
||||||
// Recursively set sub preferences.
|
// Recursively set sub preferences.
|
||||||
@@ -156,9 +243,8 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
|||||||
toolbar.setTitle(childScreen.getTitle());
|
toolbar.setTitle(childScreen.getTitle());
|
||||||
toolbar.setNavigationIcon(getBackButtonDrawable());
|
toolbar.setNavigationIcon(getBackButtonDrawable());
|
||||||
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
|
toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss());
|
||||||
final int margin = (int) TypedValue.applyDimension(
|
|
||||||
TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()
|
final int margin = Utils.dipToPixels(16);
|
||||||
);
|
|
||||||
toolbar.setTitleMargin(margin, 0, margin, 0);
|
toolbar.setTitleMargin(margin, 0, margin, 0);
|
||||||
|
|
||||||
TextView toolbarTextView = Utils.getChildView(toolbar,
|
TextView toolbarTextView = Utils.getChildView(toolbar,
|
||||||
@@ -177,3 +263,277 @@ public class ReVancedPreferenceFragment extends AbstractPreferenceFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class AbstractPreferenceSearchData<T extends Preference> {
|
||||||
|
/**
|
||||||
|
* @return The navigation path for the given preference, such as "Player > Action buttons".
|
||||||
|
*/
|
||||||
|
private static String getPreferenceNavigationString(Preference preference) {
|
||||||
|
Deque<CharSequence> pathElements = new ArrayDeque<>();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
preference = preference.getParent();
|
||||||
|
|
||||||
|
if (preference == null) {
|
||||||
|
if (pathElements.isEmpty()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
Locale locale = BaseSettings.REVANCED_LANGUAGE.get().getLocale();
|
||||||
|
return Utils.getTextDirectionString(locale) + String.join(" > ", pathElements);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(preference instanceof NoTitlePreferenceCategory)
|
||||||
|
&& !(preference instanceof SponsorBlockPreferenceGroup)) {
|
||||||
|
CharSequence title = preference.getTitle();
|
||||||
|
if (title != null && title.length() > 0) {
|
||||||
|
pathElements.addFirst(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Highlights the search query in the given text by applying color span.
|
||||||
|
* @param text The original text to process.
|
||||||
|
* @param queryPattern The search query to highlight.
|
||||||
|
* @return The text with highlighted query matches as a SpannableStringBuilder.
|
||||||
|
*/
|
||||||
|
static CharSequence highlightSearchQuery(CharSequence text, Pattern queryPattern) {
|
||||||
|
if (TextUtils.isEmpty(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int baseColor = ThemeHelper.getBackgroundColor();
|
||||||
|
final int adjustedColor = ThemeHelper.isDarkTheme()
|
||||||
|
? ThemeHelper.adjustColorBrightness(baseColor, 1.20f) // Lighten for dark theme.
|
||||||
|
: ThemeHelper.adjustColorBrightness(baseColor, 0.95f); // Darken for light theme.
|
||||||
|
BackgroundColorSpan highlightSpan = new BackgroundColorSpan(adjustedColor);
|
||||||
|
|
||||||
|
SpannableStringBuilder spannable = new SpannableStringBuilder(text);
|
||||||
|
Matcher matcher = queryPattern.matcher(text);
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
spannable.setSpan(
|
||||||
|
highlightSpan,
|
||||||
|
matcher.start(),
|
||||||
|
matcher.end(),
|
||||||
|
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return spannable;
|
||||||
|
}
|
||||||
|
|
||||||
|
final T preference;
|
||||||
|
final String key;
|
||||||
|
final String navigationPath;
|
||||||
|
boolean highlightingApplied;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
CharSequence originalTitle;
|
||||||
|
@Nullable
|
||||||
|
String searchTitle;
|
||||||
|
|
||||||
|
AbstractPreferenceSearchData(T pref) {
|
||||||
|
preference = pref;
|
||||||
|
key = Utils.removePunctuationToLowercase(pref.getKey());
|
||||||
|
navigationPath = getPreferenceNavigationString(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void updateSearchDataIfNeeded() {
|
||||||
|
if (highlightingApplied) {
|
||||||
|
// Must clear, otherwise old highlighting is still applied.
|
||||||
|
clearHighlighting();
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence title = preference.getTitle();
|
||||||
|
if (originalTitle != title) { // Check using reference equality.
|
||||||
|
originalTitle = title;
|
||||||
|
searchTitle = Utils.removePunctuationToLowercase(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
boolean matchesSearchQuery(String query) {
|
||||||
|
updateSearchDataIfNeeded();
|
||||||
|
|
||||||
|
return key.contains(query)
|
||||||
|
|| searchTitle != null && searchTitle.contains(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void applyHighlighting(String query, Pattern queryPattern) {
|
||||||
|
preference.setTitle(highlightSearchQuery(originalTitle, queryPattern));
|
||||||
|
highlightingApplied = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void clearHighlighting() {
|
||||||
|
if (highlightingApplied) {
|
||||||
|
preference.setTitle(originalTitle);
|
||||||
|
highlightingApplied = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regular preference type that only uses the base preference summary.
|
||||||
|
* Should only be used if a more specific data class does not exist.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class PreferenceSearchData extends AbstractPreferenceSearchData<Preference> {
|
||||||
|
@Nullable
|
||||||
|
CharSequence originalSummary;
|
||||||
|
@Nullable
|
||||||
|
String searchSummary;
|
||||||
|
|
||||||
|
PreferenceSearchData(Preference pref) {
|
||||||
|
super(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSearchDataIfNeeded() {
|
||||||
|
super.updateSearchDataIfNeeded();
|
||||||
|
|
||||||
|
CharSequence summary = preference.getSummary();
|
||||||
|
if (originalSummary != summary) {
|
||||||
|
originalSummary = summary;
|
||||||
|
searchSummary = Utils.removePunctuationToLowercase(summary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matchesSearchQuery(String query) {
|
||||||
|
return super.matchesSearchQuery(query)
|
||||||
|
|| searchSummary != null && searchSummary.contains(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void applyHighlighting(String query, Pattern queryPattern) {
|
||||||
|
super.applyHighlighting(query, queryPattern);
|
||||||
|
|
||||||
|
preference.setSummary(highlightSearchQuery(originalSummary, queryPattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void clearHighlighting() {
|
||||||
|
if (highlightingApplied) {
|
||||||
|
preference.setSummary(originalSummary);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.clearHighlighting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch preference type that uses summaryOn and summaryOff.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class SwitchPreferenceSearchData extends AbstractPreferenceSearchData<SwitchPreference> {
|
||||||
|
@Nullable
|
||||||
|
CharSequence originalSummaryOn, originalSummaryOff;
|
||||||
|
@Nullable
|
||||||
|
String searchSummaryOn, searchSummaryOff;
|
||||||
|
|
||||||
|
SwitchPreferenceSearchData(SwitchPreference pref) {
|
||||||
|
super(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSearchDataIfNeeded() {
|
||||||
|
super.updateSearchDataIfNeeded();
|
||||||
|
|
||||||
|
CharSequence summaryOn = preference.getSummaryOn();
|
||||||
|
if (originalSummaryOn != summaryOn) {
|
||||||
|
originalSummaryOn = summaryOn;
|
||||||
|
searchSummaryOn = Utils.removePunctuationToLowercase(summaryOn);
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence summaryOff = preference.getSummaryOff();
|
||||||
|
if (originalSummaryOff != summaryOff) {
|
||||||
|
originalSummaryOff = summaryOff;
|
||||||
|
searchSummaryOff = Utils.removePunctuationToLowercase(summaryOff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matchesSearchQuery(String query) {
|
||||||
|
return super.matchesSearchQuery(query)
|
||||||
|
|| searchSummaryOn != null && searchSummaryOn.contains(query)
|
||||||
|
|| searchSummaryOff != null && searchSummaryOff.contains(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void applyHighlighting(String query, Pattern queryPattern) {
|
||||||
|
super.applyHighlighting(query, queryPattern);
|
||||||
|
|
||||||
|
preference.setSummaryOn(highlightSearchQuery(originalSummaryOn, queryPattern));
|
||||||
|
preference.setSummaryOff(highlightSearchQuery(originalSummaryOff, queryPattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void clearHighlighting() {
|
||||||
|
if (highlightingApplied) {
|
||||||
|
preference.setSummaryOn(originalSummaryOn);
|
||||||
|
preference.setSummaryOff(originalSummaryOff);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.clearHighlighting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List preference type that uses entries.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
class ListPreferenceSearchData extends AbstractPreferenceSearchData<ListPreference> {
|
||||||
|
@Nullable
|
||||||
|
CharSequence[] originalEntries;
|
||||||
|
@Nullable
|
||||||
|
String searchEntries;
|
||||||
|
|
||||||
|
ListPreferenceSearchData(ListPreference pref) {
|
||||||
|
super(pref);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSearchDataIfNeeded() {
|
||||||
|
super.updateSearchDataIfNeeded();
|
||||||
|
|
||||||
|
CharSequence[] entries = preference.getEntries();
|
||||||
|
if (originalEntries != entries) {
|
||||||
|
originalEntries = entries;
|
||||||
|
searchEntries = Utils.removePunctuationToLowercase(String.join(" ", entries));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean matchesSearchQuery(String query) {
|
||||||
|
return super.matchesSearchQuery(query)
|
||||||
|
|| searchEntries != null && searchEntries.contains(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
void applyHighlighting(String query, Pattern queryPattern) {
|
||||||
|
super.applyHighlighting(query, queryPattern);
|
||||||
|
|
||||||
|
if (originalEntries != null) {
|
||||||
|
final int length = originalEntries.length;
|
||||||
|
CharSequence[] highlightedEntries = new CharSequence[length];
|
||||||
|
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
highlightedEntries[i] = highlightSearchQuery(originalEntries[i], queryPattern);
|
||||||
|
|
||||||
|
// Cannot highlight the summary text, because ListPreference uses
|
||||||
|
// the toString() of the summary CharSequence which strips away all formatting.
|
||||||
|
}
|
||||||
|
|
||||||
|
preference.setEntries(highlightedEntries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
void clearHighlighting() {
|
||||||
|
if (highlightingApplied) {
|
||||||
|
preference.setEntries(originalEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
super.clearHighlighting();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,257 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.PreferenceCategory;
|
|
||||||
import android.preference.PreferenceFragment;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.preference.PreferenceScreen;
|
|
||||||
import android.preference.SwitchPreference;
|
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
|
||||||
import app.revanced.extension.shared.settings.BaseSettings;
|
|
||||||
import app.revanced.extension.youtube.patches.ReturnYouTubeDislikePatch;
|
|
||||||
import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike;
|
|
||||||
import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
|
|
||||||
/** @noinspection deprecation*/
|
|
||||||
public class ReturnYouTubeDislikePreferenceFragment extends PreferenceFragment {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If dislikes are shown on Shorts.
|
|
||||||
*/
|
|
||||||
private SwitchPreference shortsPreference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If dislikes are shown as percentage.
|
|
||||||
*/
|
|
||||||
private SwitchPreference percentagePreference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If segmented like/dislike button uses smaller compact layout.
|
|
||||||
*/
|
|
||||||
private SwitchPreference compactLayoutPreference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If hidden likes are replaced with an estimated value.
|
|
||||||
*/
|
|
||||||
private SwitchPreference estimatedLikesPreference;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If segmented like/dislike button uses smaller compact layout.
|
|
||||||
*/
|
|
||||||
private SwitchPreference toastOnRYDNotAvailable;
|
|
||||||
|
|
||||||
private void updateUIState() {
|
|
||||||
shortsPreference.setEnabled(Settings.RYD_SHORTS.isAvailable());
|
|
||||||
percentagePreference.setEnabled(Settings.RYD_DISLIKE_PERCENTAGE.isAvailable());
|
|
||||||
compactLayoutPreference.setEnabled(Settings.RYD_COMPACT_LAYOUT.isAvailable());
|
|
||||||
estimatedLikesPreference.setEnabled(Settings.RYD_ESTIMATED_LIKE.isAvailable());
|
|
||||||
toastOnRYDNotAvailable.setEnabled(Settings.RYD_TOAST_ON_CONNECTION_ERROR.isAvailable());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
try {
|
|
||||||
Activity context = getActivity();
|
|
||||||
PreferenceManager manager = getPreferenceManager();
|
|
||||||
manager.setSharedPreferencesName(Setting.preferences.name);
|
|
||||||
PreferenceScreen preferenceScreen = manager.createPreferenceScreen(context);
|
|
||||||
setPreferenceScreen(preferenceScreen);
|
|
||||||
|
|
||||||
SwitchPreference enabledPreference = new SwitchPreference(context);
|
|
||||||
enabledPreference.setChecked(Settings.RYD_ENABLED.get());
|
|
||||||
enabledPreference.setTitle(str("revanced_ryd_enable_title"));
|
|
||||||
enabledPreference.setSummaryOn(str("revanced_ryd_enable_summary_on"));
|
|
||||||
enabledPreference.setSummaryOff(str("revanced_ryd_enable_summary_off"));
|
|
||||||
enabledPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
final Boolean rydIsEnabled = (Boolean) newValue;
|
|
||||||
Settings.RYD_ENABLED.save(rydIsEnabled);
|
|
||||||
ReturnYouTubeDislikePatch.onRYDStatusChange(rydIsEnabled);
|
|
||||||
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(enabledPreference);
|
|
||||||
|
|
||||||
shortsPreference = new SwitchPreference(context);
|
|
||||||
shortsPreference.setChecked(Settings.RYD_SHORTS.get());
|
|
||||||
shortsPreference.setTitle(str("revanced_ryd_shorts_title"));
|
|
||||||
String shortsSummary = str("revanced_ryd_shorts_summary_on_disclaimer");
|
|
||||||
shortsPreference.setSummaryOn(shortsSummary);
|
|
||||||
shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off"));
|
|
||||||
shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
Settings.RYD_SHORTS.save((Boolean) newValue);
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(shortsPreference);
|
|
||||||
|
|
||||||
percentagePreference = new SwitchPreference(context);
|
|
||||||
percentagePreference.setChecked(Settings.RYD_DISLIKE_PERCENTAGE.get());
|
|
||||||
percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title"));
|
|
||||||
percentagePreference.setSummaryOn(str("revanced_ryd_dislike_percentage_summary_on"));
|
|
||||||
percentagePreference.setSummaryOff(str("revanced_ryd_dislike_percentage_summary_off"));
|
|
||||||
percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
Settings.RYD_DISLIKE_PERCENTAGE.save((Boolean) newValue);
|
|
||||||
ReturnYouTubeDislike.clearAllUICaches();
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(percentagePreference);
|
|
||||||
|
|
||||||
compactLayoutPreference = new SwitchPreference(context);
|
|
||||||
compactLayoutPreference.setChecked(Settings.RYD_COMPACT_LAYOUT.get());
|
|
||||||
compactLayoutPreference.setTitle(str("revanced_ryd_compact_layout_title"));
|
|
||||||
compactLayoutPreference.setSummaryOn(str("revanced_ryd_compact_layout_summary_on"));
|
|
||||||
compactLayoutPreference.setSummaryOff(str("revanced_ryd_compact_layout_summary_off"));
|
|
||||||
compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
Settings.RYD_COMPACT_LAYOUT.save((Boolean) newValue);
|
|
||||||
ReturnYouTubeDislike.clearAllUICaches();
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(compactLayoutPreference);
|
|
||||||
|
|
||||||
estimatedLikesPreference = new SwitchPreference(context);
|
|
||||||
estimatedLikesPreference.setChecked(Settings.RYD_ESTIMATED_LIKE.get());
|
|
||||||
estimatedLikesPreference.setTitle(str("revanced_ryd_estimated_like_title"));
|
|
||||||
estimatedLikesPreference.setSummaryOn(str("revanced_ryd_estimated_like_summary_on"));
|
|
||||||
estimatedLikesPreference.setSummaryOff(str("revanced_ryd_estimated_like_summary_off"));
|
|
||||||
estimatedLikesPreference.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
Settings.RYD_ESTIMATED_LIKE.save((Boolean) newValue);
|
|
||||||
ReturnYouTubeDislike.clearAllUICaches();
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(estimatedLikesPreference);
|
|
||||||
|
|
||||||
toastOnRYDNotAvailable = new SwitchPreference(context);
|
|
||||||
toastOnRYDNotAvailable.setChecked(Settings.RYD_TOAST_ON_CONNECTION_ERROR.get());
|
|
||||||
toastOnRYDNotAvailable.setTitle(str("revanced_ryd_toast_on_connection_error_title"));
|
|
||||||
toastOnRYDNotAvailable.setSummaryOn(str("revanced_ryd_toast_on_connection_error_summary_on"));
|
|
||||||
toastOnRYDNotAvailable.setSummaryOff(str("revanced_ryd_toast_on_connection_error_summary_off"));
|
|
||||||
toastOnRYDNotAvailable.setOnPreferenceChangeListener((pref, newValue) -> {
|
|
||||||
Settings.RYD_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
|
|
||||||
updateUIState();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
preferenceScreen.addPreference(toastOnRYDNotAvailable);
|
|
||||||
|
|
||||||
updateUIState();
|
|
||||||
|
|
||||||
|
|
||||||
// About category
|
|
||||||
|
|
||||||
PreferenceCategory aboutCategory = new PreferenceCategory(context);
|
|
||||||
aboutCategory.setTitle(str("revanced_ryd_about"));
|
|
||||||
preferenceScreen.addPreference(aboutCategory);
|
|
||||||
|
|
||||||
// ReturnYouTubeDislike Website
|
|
||||||
|
|
||||||
Preference aboutWebsitePreference = new Preference(context);
|
|
||||||
aboutWebsitePreference.setTitle(str("revanced_ryd_attribution_title"));
|
|
||||||
aboutWebsitePreference.setSummary(str("revanced_ryd_attribution_summary"));
|
|
||||||
aboutWebsitePreference.setOnPreferenceClickListener(pref -> {
|
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
||||||
i.setData(Uri.parse("https://returnyoutubedislike.com"));
|
|
||||||
pref.getContext().startActivity(i);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
aboutCategory.addPreference(aboutWebsitePreference);
|
|
||||||
|
|
||||||
// RYD API connection statistics
|
|
||||||
|
|
||||||
if (BaseSettings.DEBUG.get()) {
|
|
||||||
PreferenceCategory emptyCategory = new PreferenceCategory(context); // vertical padding
|
|
||||||
preferenceScreen.addPreference(emptyCategory);
|
|
||||||
|
|
||||||
PreferenceCategory statisticsCategory = new PreferenceCategory(context);
|
|
||||||
statisticsCategory.setTitle(str("revanced_ryd_statistics_category_title"));
|
|
||||||
preferenceScreen.addPreference(statisticsCategory);
|
|
||||||
|
|
||||||
Preference statisticPreference;
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeAverage_title"));
|
|
||||||
statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage()));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMin_title"));
|
|
||||||
statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin()));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMax_title"));
|
|
||||||
statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax()));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
String fetchCallTimeWaitingLastSummary;
|
|
||||||
final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast();
|
|
||||||
if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) {
|
|
||||||
fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary");
|
|
||||||
} else {
|
|
||||||
fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast);
|
|
||||||
}
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeLast_title"));
|
|
||||||
statisticPreference.setSummary(fetchCallTimeWaitingLastSummary);
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallCount_title"));
|
|
||||||
statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(),
|
|
||||||
"revanced_ryd_statistics_getFetchCallCount_zero_summary",
|
|
||||||
"revanced_ryd_statistics_getFetchCallCount_non_zero_summary"));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallNumberOfFailures_title"));
|
|
||||||
statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(),
|
|
||||||
"revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary",
|
|
||||||
"revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary"));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
|
|
||||||
statisticPreference = new Preference(context);
|
|
||||||
statisticPreference.setSelectable(false);
|
|
||||||
statisticPreference.setTitle(str("revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title"));
|
|
||||||
statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(),
|
|
||||||
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary",
|
|
||||||
"revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary"));
|
|
||||||
preferenceScreen.addPreference(statisticPreference);
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.setPreferenceTitlesToMultiLineIfNeeded(preferenceScreen);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onCreate failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) {
|
|
||||||
if (value == 0) {
|
|
||||||
return str(summaryStringZeroKey);
|
|
||||||
}
|
|
||||||
return String.format(str(summaryStringOneOrMoreKey), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String createMillisecondStringFromNumber(long number) {
|
|
||||||
return String.format(str("revanced_ryd_statistics_millisecond_text"), number);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,629 +0,0 @@
|
|||||||
package app.revanced.extension.youtube.settings.preference;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.*;
|
|
||||||
import android.text.Html;
|
|
||||||
import android.text.InputType;
|
|
||||||
import android.util.TypedValue;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import app.revanced.extension.shared.Logger;
|
|
||||||
import app.revanced.extension.shared.Utils;
|
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
|
||||||
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryListPreference;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.UserStats;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.requests.SBRequester;
|
|
||||||
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController;
|
|
||||||
|
|
||||||
import static android.text.Html.fromHtml;
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
|
||||||
public class SponsorBlockPreferenceFragment extends PreferenceFragment {
|
|
||||||
|
|
||||||
private SwitchPreference sbEnabled;
|
|
||||||
private SwitchPreference addNewSegment;
|
|
||||||
private SwitchPreference votingEnabled;
|
|
||||||
private SwitchPreference autoHideSkipSegmentButton;
|
|
||||||
private SwitchPreference compactSkipButton;
|
|
||||||
private SwitchPreference squareLayout;
|
|
||||||
private SwitchPreference showSkipToast;
|
|
||||||
private SwitchPreference trackSkips;
|
|
||||||
private SwitchPreference showTimeWithoutSegments;
|
|
||||||
private SwitchPreference toastOnConnectionError;
|
|
||||||
|
|
||||||
private ResettableEditTextPreference newSegmentStep;
|
|
||||||
private ResettableEditTextPreference minSegmentDuration;
|
|
||||||
private EditTextPreference privateUserId;
|
|
||||||
private EditTextPreference importExport;
|
|
||||||
private Preference apiUrl;
|
|
||||||
|
|
||||||
private PreferenceCategory statsCategory;
|
|
||||||
private PreferenceCategory segmentCategory;
|
|
||||||
|
|
||||||
private void updateUI() {
|
|
||||||
try {
|
|
||||||
final boolean enabled = Settings.SB_ENABLED.get();
|
|
||||||
if (!enabled) {
|
|
||||||
SponsorBlockViewController.hideAll();
|
|
||||||
SegmentPlaybackController.setCurrentVideoId(null);
|
|
||||||
} else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) {
|
|
||||||
SponsorBlockViewController.hideNewSegmentLayout();
|
|
||||||
}
|
|
||||||
// Voting and add new segment buttons automatically show/hide themselves.
|
|
||||||
|
|
||||||
SponsorBlockViewController.updateLayout();
|
|
||||||
|
|
||||||
sbEnabled.setChecked(enabled);
|
|
||||||
|
|
||||||
addNewSegment.setChecked(Settings.SB_CREATE_NEW_SEGMENT.get());
|
|
||||||
addNewSegment.setEnabled(enabled);
|
|
||||||
|
|
||||||
votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get());
|
|
||||||
votingEnabled.setEnabled(enabled);
|
|
||||||
|
|
||||||
autoHideSkipSegmentButton.setEnabled(enabled);
|
|
||||||
autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get());
|
|
||||||
|
|
||||||
compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get());
|
|
||||||
compactSkipButton.setEnabled(enabled);
|
|
||||||
|
|
||||||
squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get());
|
|
||||||
squareLayout.setEnabled(enabled);
|
|
||||||
|
|
||||||
showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
|
|
||||||
showSkipToast.setEnabled(enabled);
|
|
||||||
|
|
||||||
toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get());
|
|
||||||
toastOnConnectionError.setEnabled(enabled);
|
|
||||||
|
|
||||||
trackSkips.setChecked(Settings.SB_TRACK_SKIP_COUNT.get());
|
|
||||||
trackSkips.setEnabled(enabled);
|
|
||||||
|
|
||||||
showTimeWithoutSegments.setChecked(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get());
|
|
||||||
showTimeWithoutSegments.setEnabled(enabled);
|
|
||||||
|
|
||||||
newSegmentStep.setText((Settings.SB_CREATE_NEW_SEGMENT_STEP.get()).toString());
|
|
||||||
newSegmentStep.setEnabled(enabled);
|
|
||||||
|
|
||||||
minSegmentDuration.setText((Settings.SB_SEGMENT_MIN_DURATION.get()).toString());
|
|
||||||
minSegmentDuration.setEnabled(enabled);
|
|
||||||
|
|
||||||
privateUserId.setText(Settings.SB_PRIVATE_USER_ID.get());
|
|
||||||
privateUserId.setEnabled(enabled);
|
|
||||||
|
|
||||||
// If the user has a private user id, then include a subtext that mentions not to share it.
|
|
||||||
String importExportSummary = SponsorBlockSettings.userHasSBPrivateId()
|
|
||||||
? str("revanced_sb_settings_ie_sum_warning")
|
|
||||||
: str("revanced_sb_settings_ie_sum");
|
|
||||||
importExport.setSummary(importExportSummary);
|
|
||||||
|
|
||||||
apiUrl.setEnabled(enabled);
|
|
||||||
importExport.setEnabled(enabled);
|
|
||||||
segmentCategory.setEnabled(enabled);
|
|
||||||
statsCategory.setEnabled(enabled);
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "update settings UI failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
try {
|
|
||||||
Activity context = getActivity();
|
|
||||||
PreferenceManager manager = getPreferenceManager();
|
|
||||||
manager.setSharedPreferencesName(Setting.preferences.name);
|
|
||||||
PreferenceScreen preferenceScreen = manager.createPreferenceScreen(context);
|
|
||||||
setPreferenceScreen(preferenceScreen);
|
|
||||||
|
|
||||||
SponsorBlockSettings.initialize();
|
|
||||||
|
|
||||||
sbEnabled = new SwitchPreference(context);
|
|
||||||
sbEnabled.setTitle(str("revanced_sb_enable_sb"));
|
|
||||||
sbEnabled.setSummary(str("revanced_sb_enable_sb_sum"));
|
|
||||||
preferenceScreen.addPreference(sbEnabled);
|
|
||||||
sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_ENABLED.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
addAppearanceCategory(context, preferenceScreen);
|
|
||||||
|
|
||||||
segmentCategory = new PreferenceCategory(context);
|
|
||||||
segmentCategory.setTitle(str("revanced_sb_diff_segments"));
|
|
||||||
preferenceScreen.addPreference(segmentCategory);
|
|
||||||
updateSegmentCategories();
|
|
||||||
|
|
||||||
addCreateSegmentCategory(context, preferenceScreen);
|
|
||||||
|
|
||||||
addGeneralCategory(context, preferenceScreen);
|
|
||||||
|
|
||||||
statsCategory = new PreferenceCategory(context);
|
|
||||||
statsCategory.setTitle(str("revanced_sb_stats"));
|
|
||||||
preferenceScreen.addPreference(statsCategory);
|
|
||||||
fetchAndDisplayStats();
|
|
||||||
|
|
||||||
addAboutCategory(context, preferenceScreen);
|
|
||||||
|
|
||||||
Utils.setPreferenceTitlesToMultiLineIfNeeded(preferenceScreen);
|
|
||||||
|
|
||||||
updateUI();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "onCreate failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addAppearanceCategory(Context context, PreferenceScreen screen) {
|
|
||||||
PreferenceCategory category = new PreferenceCategory(context);
|
|
||||||
screen.addPreference(category);
|
|
||||||
category.setTitle(str("revanced_sb_appearance_category"));
|
|
||||||
|
|
||||||
votingEnabled = new SwitchPreference(context);
|
|
||||||
votingEnabled.setTitle(str("revanced_sb_enable_voting"));
|
|
||||||
votingEnabled.setSummaryOn(str("revanced_sb_enable_voting_sum_on"));
|
|
||||||
votingEnabled.setSummaryOff(str("revanced_sb_enable_voting_sum_off"));
|
|
||||||
category.addPreference(votingEnabled);
|
|
||||||
votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_VOTING_BUTTON.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
autoHideSkipSegmentButton = new SwitchPreference(context);
|
|
||||||
autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
|
|
||||||
autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
|
|
||||||
autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
|
|
||||||
category.addPreference(autoHideSkipSegmentButton);
|
|
||||||
autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
compactSkipButton = new SwitchPreference(context);
|
|
||||||
compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button"));
|
|
||||||
compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on"));
|
|
||||||
compactSkipButton.setSummaryOff(str("revanced_sb_enable_compact_skip_button_sum_off"));
|
|
||||||
category.addPreference(compactSkipButton);
|
|
||||||
compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
squareLayout = new SwitchPreference(context);
|
|
||||||
squareLayout.setTitle(str("revanced_sb_square_layout"));
|
|
||||||
squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
|
|
||||||
squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
|
|
||||||
category.addPreference(squareLayout);
|
|
||||||
squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
showSkipToast = new SwitchPreference(context);
|
|
||||||
showSkipToast.setTitle(str("revanced_sb_general_skiptoast"));
|
|
||||||
showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on"));
|
|
||||||
showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off"));
|
|
||||||
showSkipToast.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
Utils.showToastShort(str("revanced_sb_skipped_sponsor"));
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(showSkipToast);
|
|
||||||
|
|
||||||
showTimeWithoutSegments = new SwitchPreference(context);
|
|
||||||
showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without"));
|
|
||||||
showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on"));
|
|
||||||
showTimeWithoutSegments.setSummaryOff(str("revanced_sb_general_time_without_sum_off"));
|
|
||||||
showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(showTimeWithoutSegments);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addCreateSegmentCategory(Context context, PreferenceScreen screen) {
|
|
||||||
PreferenceCategory category = new PreferenceCategory(context);
|
|
||||||
screen.addPreference(category);
|
|
||||||
category.setTitle(str("revanced_sb_create_segment_category"));
|
|
||||||
|
|
||||||
addNewSegment = new SwitchPreference(context);
|
|
||||||
addNewSegment.setTitle(str("revanced_sb_enable_create_segment"));
|
|
||||||
addNewSegment.setSummaryOn(str("revanced_sb_enable_create_segment_sum_on"));
|
|
||||||
addNewSegment.setSummaryOff(str("revanced_sb_enable_create_segment_sum_off"));
|
|
||||||
category.addPreference(addNewSegment);
|
|
||||||
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
|
|
||||||
Boolean newValue = (Boolean) o;
|
|
||||||
if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
|
|
||||||
new AlertDialog.Builder(preference1.getContext())
|
|
||||||
.setTitle(str("revanced_sb_guidelines_popup_title"))
|
|
||||||
.setMessage(str("revanced_sb_guidelines_popup_content"))
|
|
||||||
.setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null)
|
|
||||||
.setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines())
|
|
||||||
.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true))
|
|
||||||
.setCancelable(false)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
newSegmentStep = new ResettableEditTextPreference(context);
|
|
||||||
newSegmentStep.setSetting(Settings.SB_CREATE_NEW_SEGMENT_STEP);
|
|
||||||
newSegmentStep.setTitle(str("revanced_sb_general_adjusting"));
|
|
||||||
newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum"));
|
|
||||||
newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
|
|
||||||
newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
try {
|
|
||||||
final int newAdjustmentValue = Integer.parseInt(newValue.toString());
|
|
||||||
if (newAdjustmentValue != 0) {
|
|
||||||
Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
Logger.printInfo(() -> "Invalid new segment step", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.showToastLong(str("revanced_sb_general_adjusting_invalid"));
|
|
||||||
updateUI();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
category.addPreference(newSegmentStep);
|
|
||||||
|
|
||||||
Preference guidelinePreferences = new Preference(context);
|
|
||||||
guidelinePreferences.setTitle(str("revanced_sb_guidelines_preference_title"));
|
|
||||||
guidelinePreferences.setSummary(str("revanced_sb_guidelines_preference_sum"));
|
|
||||||
guidelinePreferences.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
openGuidelines();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(guidelinePreferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addGeneralCategory(final Context context, PreferenceScreen screen) {
|
|
||||||
PreferenceCategory category = new PreferenceCategory(context);
|
|
||||||
screen.addPreference(category);
|
|
||||||
category.setTitle(str("revanced_sb_general"));
|
|
||||||
|
|
||||||
toastOnConnectionError = new SwitchPreference(context);
|
|
||||||
toastOnConnectionError.setTitle(str("revanced_sb_toast_on_connection_error_title"));
|
|
||||||
toastOnConnectionError.setSummaryOn(str("revanced_sb_toast_on_connection_error_summary_on"));
|
|
||||||
toastOnConnectionError.setSummaryOff(str("revanced_sb_toast_on_connection_error_summary_off"));
|
|
||||||
toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(toastOnConnectionError);
|
|
||||||
|
|
||||||
trackSkips = new SwitchPreference(context);
|
|
||||||
trackSkips.setTitle(str("revanced_sb_general_skipcount"));
|
|
||||||
trackSkips.setSummaryOn(str("revanced_sb_general_skipcount_sum_on"));
|
|
||||||
trackSkips.setSummaryOff(str("revanced_sb_general_skipcount_sum_off"));
|
|
||||||
trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue);
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(trackSkips);
|
|
||||||
|
|
||||||
minSegmentDuration = new ResettableEditTextPreference(context);
|
|
||||||
minSegmentDuration.setSetting(Settings.SB_SEGMENT_MIN_DURATION);
|
|
||||||
minSegmentDuration.setTitle(str("revanced_sb_general_min_duration"));
|
|
||||||
minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum"));
|
|
||||||
minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
|
||||||
minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
try {
|
|
||||||
Float minTimeDuration = Float.valueOf(newValue.toString());
|
|
||||||
Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration);
|
|
||||||
return true;
|
|
||||||
} catch (NumberFormatException ex) {
|
|
||||||
Logger.printInfo(() -> "Invalid minimum segment duration", ex);
|
|
||||||
}
|
|
||||||
|
|
||||||
Utils.showToastLong(str("revanced_sb_general_min_duration_invalid"));
|
|
||||||
updateUI();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
category.addPreference(minSegmentDuration);
|
|
||||||
|
|
||||||
privateUserId = new EditTextPreference(context) {
|
|
||||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
|
||||||
Utils.setEditTextDialogTheme(builder);
|
|
||||||
|
|
||||||
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
|
||||||
Utils.setClipboard(getEditText().getText().toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
privateUserId.setTitle(str("revanced_sb_general_uuid"));
|
|
||||||
privateUserId.setSummary(str("revanced_sb_general_uuid_sum"));
|
|
||||||
privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
String newUUID = newValue.toString();
|
|
||||||
if (!SponsorBlockSettings.isValidSBUserId(newUUID)) {
|
|
||||||
Utils.showToastLong(str("revanced_sb_general_uuid_invalid"));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Settings.SB_PRIVATE_USER_ID.save(newUUID);
|
|
||||||
updateUI();
|
|
||||||
fetchAndDisplayStats();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(privateUserId);
|
|
||||||
|
|
||||||
apiUrl = new Preference(context);
|
|
||||||
apiUrl.setTitle(str("revanced_sb_general_api_url"));
|
|
||||||
apiUrl.setSummary(Html.fromHtml(str("revanced_sb_general_api_url_sum")));
|
|
||||||
apiUrl.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
EditText editText = new EditText(context);
|
|
||||||
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
|
||||||
editText.setText(Settings.SB_API_URL.get());
|
|
||||||
|
|
||||||
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
|
|
||||||
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) {
|
|
||||||
Settings.SB_API_URL.resetToDefault();
|
|
||||||
Utils.showToastLong(str("revanced_sb_api_url_reset"));
|
|
||||||
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
|
|
||||||
String serverAddress = editText.getText().toString();
|
|
||||||
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
|
|
||||||
Utils.showToastLong(str("revanced_sb_api_url_invalid"));
|
|
||||||
} else if (!serverAddress.equals(Settings.SB_API_URL.get())) {
|
|
||||||
Settings.SB_API_URL.save(serverAddress);
|
|
||||||
Utils.showToastLong(str("revanced_sb_api_url_changed"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
new AlertDialog.Builder(context)
|
|
||||||
.setTitle(apiUrl.getTitle())
|
|
||||||
.setView(editText)
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setNeutralButton(str("revanced_sb_reset"), urlChangeListener)
|
|
||||||
.setPositiveButton(android.R.string.ok, urlChangeListener)
|
|
||||||
.show();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(apiUrl);
|
|
||||||
|
|
||||||
importExport = new EditTextPreference(context) {
|
|
||||||
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
|
||||||
Utils.setEditTextDialogTheme(builder);
|
|
||||||
|
|
||||||
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
|
||||||
Utils.setClipboard(getEditText().getText().toString());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
importExport.setTitle(str("revanced_sb_settings_ie"));
|
|
||||||
// Summary is set in updateUI()
|
|
||||||
importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT
|
|
||||||
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
||||||
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
|
||||||
importExport.getEditText().setAutofillHints((String) null);
|
|
||||||
importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
|
|
||||||
importExport.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings());
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
importExport.setOnPreferenceChangeListener((preference1, newValue) -> {
|
|
||||||
SponsorBlockSettings.importDesktopSettings((String) newValue);
|
|
||||||
updateSegmentCategories();
|
|
||||||
fetchAndDisplayStats();
|
|
||||||
updateUI();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
category.addPreference(importExport);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSegmentCategories() {
|
|
||||||
try {
|
|
||||||
segmentCategory.removeAll();
|
|
||||||
|
|
||||||
Activity activity = getActivity();
|
|
||||||
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
|
||||||
segmentCategory.addPreference(new SegmentCategoryListPreference(activity, category));
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "updateSegmentCategories failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addAboutCategory(Context context, PreferenceScreen screen) {
|
|
||||||
PreferenceCategory category = new PreferenceCategory(context);
|
|
||||||
screen.addPreference(category);
|
|
||||||
category.setTitle(str("revanced_sb_about"));
|
|
||||||
|
|
||||||
{
|
|
||||||
Preference preference = new Preference(context);
|
|
||||||
category.addPreference(preference);
|
|
||||||
preference.setTitle(str("revanced_sb_about_api"));
|
|
||||||
preference.setSummary(str("revanced_sb_about_api_sum"));
|
|
||||||
preference.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
||||||
i.setData(Uri.parse("https://sponsor.ajay.app"));
|
|
||||||
preference1.getContext().startActivity(i);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openGuidelines() {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
||||||
intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
|
|
||||||
getActivity().startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchAndDisplayStats() {
|
|
||||||
try {
|
|
||||||
statsCategory.removeAll();
|
|
||||||
if (!SponsorBlockSettings.userHasSBPrivateId()) {
|
|
||||||
// User has never voted or created any segments. No stats to show.
|
|
||||||
addLocalUserStats();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Preference loadingPlaceholderPreference = new Preference(this.getActivity());
|
|
||||||
loadingPlaceholderPreference.setEnabled(false);
|
|
||||||
statsCategory.addPreference(loadingPlaceholderPreference);
|
|
||||||
if (Settings.SB_ENABLED.get()) {
|
|
||||||
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_loading"));
|
|
||||||
Utils.runOnBackgroundThread(() -> {
|
|
||||||
UserStats stats = SBRequester.retrieveUserStats();
|
|
||||||
Utils.runOnMainThread(() -> { // get back on main thread to modify UI elements
|
|
||||||
addUserStats(loadingPlaceholderPreference, stats);
|
|
||||||
addLocalUserStats();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_sb_disabled"));
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "fetchAndDisplayStats failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addUserStats(@NonNull Preference loadingPlaceholder, @Nullable UserStats stats) {
|
|
||||||
Utils.verifyOnMainThread();
|
|
||||||
try {
|
|
||||||
if (stats == null) {
|
|
||||||
loadingPlaceholder.setTitle(str("revanced_sb_stats_connection_failure"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
statsCategory.removeAll();
|
|
||||||
Context context = statsCategory.getContext();
|
|
||||||
|
|
||||||
if (stats.totalSegmentCountIncludingIgnored > 0) {
|
|
||||||
// If user has not created any segments, there's no reason to set a username.
|
|
||||||
EditTextPreference preference = new ResettableEditTextPreference(context);
|
|
||||||
statsCategory.addPreference(preference);
|
|
||||||
String userName = stats.userName;
|
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName)));
|
|
||||||
preference.setSummary(str("revanced_sb_stats_username_change"));
|
|
||||||
preference.setText(userName);
|
|
||||||
preference.setOnPreferenceChangeListener((preference1, value) -> {
|
|
||||||
Utils.runOnBackgroundThread(() -> {
|
|
||||||
String newUserName = (String) value;
|
|
||||||
String errorMessage = SBRequester.setUsername(newUserName);
|
|
||||||
Utils.runOnMainThread(() -> {
|
|
||||||
if (errorMessage == null) {
|
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName)));
|
|
||||||
preference.setText(newUserName);
|
|
||||||
Utils.showToastLong(str("revanced_sb_stats_username_changed"));
|
|
||||||
} else {
|
|
||||||
preference.setText(userName); // revert to previous
|
|
||||||
SponsorBlockUtils.showErrorDialog(errorMessage);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// number of segment submissions (does not include ignored segments)
|
|
||||||
Preference preference = new Preference(context);
|
|
||||||
statsCategory.addPreference(preference);
|
|
||||||
String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount);
|
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted)));
|
|
||||||
preference.setSummary(str("revanced_sb_stats_submissions_sum"));
|
|
||||||
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
|
||||||
preference.setSelectable(false);
|
|
||||||
} else {
|
|
||||||
preference.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
||||||
i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId));
|
|
||||||
preference1.getContext().startActivity(i);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// "user reputation". Usually not useful, since it appears most users have zero reputation.
|
|
||||||
// But if there is a reputation, then show it here
|
|
||||||
Preference preference = new Preference(context);
|
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation)));
|
|
||||||
preference.setSelectable(false);
|
|
||||||
if (stats.reputation != 0) {
|
|
||||||
statsCategory.addPreference(preference);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
// time saved for other users
|
|
||||||
Preference preference = new Preference(context);
|
|
||||||
statsCategory.addPreference(preference);
|
|
||||||
|
|
||||||
String stats_saved;
|
|
||||||
String stats_saved_sum;
|
|
||||||
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
|
||||||
stats_saved = str("revanced_sb_stats_saved_zero");
|
|
||||||
stats_saved_sum = str("revanced_sb_stats_saved_sum_zero");
|
|
||||||
} else {
|
|
||||||
stats_saved = str("revanced_sb_stats_saved",
|
|
||||||
SponsorBlockUtils.getNumberOfSkipsString(stats.viewCount));
|
|
||||||
stats_saved_sum = str("revanced_sb_stats_saved_sum", SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved)));
|
|
||||||
}
|
|
||||||
preference.setTitle(fromHtml(stats_saved));
|
|
||||||
preference.setSummary(fromHtml(stats_saved_sum));
|
|
||||||
preference.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
Intent i = new Intent(Intent.ACTION_VIEW);
|
|
||||||
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
|
|
||||||
preference1.getContext().startActivity(i);
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "addUserStats failure", ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addLocalUserStats() {
|
|
||||||
// time the user saved by using SB
|
|
||||||
Preference preference = new Preference(statsCategory.getContext());
|
|
||||||
statsCategory.addPreference(preference);
|
|
||||||
|
|
||||||
Runnable updateStatsSelfSaved = () -> {
|
|
||||||
String formatted = SponsorBlockUtils.getNumberOfSkipsString(Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get());
|
|
||||||
preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted)));
|
|
||||||
String formattedSaved = SponsorBlockUtils.getTimeSavedString(Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / 1000);
|
|
||||||
preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved)));
|
|
||||||
};
|
|
||||||
updateStatsSelfSaved.run();
|
|
||||||
preference.setOnPreferenceClickListener(preference1 -> {
|
|
||||||
new AlertDialog.Builder(preference1.getContext())
|
|
||||||
.setTitle(str("revanced_sb_stats_self_saved_reset_title"))
|
|
||||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
|
||||||
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault();
|
|
||||||
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault();
|
|
||||||
updateStatsSelfSaved.run();
|
|
||||||
})
|
|
||||||
.setNegativeButton(android.R.string.no, null).show();
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package app.revanced.extension.youtube.settings.preference;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple preference that opens a url when clicked.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class UrlLinkPreference extends Preference {
|
||||||
|
|
||||||
|
protected String externalUrl;
|
||||||
|
|
||||||
|
{
|
||||||
|
setOnPreferenceClickListener(pref -> {
|
||||||
|
if (externalUrl == null) {
|
||||||
|
Logger.printException(() -> "URL not set " + getClass().getSimpleName());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
|
i.setData(Uri.parse(externalUrl));
|
||||||
|
pref.getContext().startActivity(i);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
public UrlLinkPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
public UrlLinkPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
public UrlLinkPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import static app.revanced.extension.shared.StringRef.str;
|
|||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.TypedValue;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
@@ -727,15 +726,11 @@ public class SegmentPlaybackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int highlightSegmentTimeBarScreenWidth = -1; // actual pixel width to use
|
/**
|
||||||
private static int getHighlightSegmentTimeBarScreenWidth() {
|
* Actual screen pixel width to use for the highlight segment time bar.
|
||||||
if (highlightSegmentTimeBarScreenWidth == -1) {
|
*/
|
||||||
highlightSegmentTimeBarScreenWidth = (int) TypedValue.applyDimension(
|
private static final int highlightSegmentTimeBarScreenWidth
|
||||||
TypedValue.COMPLEX_UNIT_DIP, HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH,
|
= Utils.dipToPixels(HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH);
|
||||||
Objects.requireNonNull(Utils.getContext()).getResources().getDisplayMetrics());
|
|
||||||
}
|
|
||||||
return highlightSegmentTimeBarScreenWidth;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Injection point.
|
* Injection point.
|
||||||
@@ -757,7 +752,7 @@ public class SegmentPlaybackController {
|
|||||||
final float left = leftPadding + segment.start * videoMillisecondsToPixels;
|
final float left = leftPadding + segment.start * videoMillisecondsToPixels;
|
||||||
final float right;
|
final float right;
|
||||||
if (segment.category == SegmentCategory.HIGHLIGHT) {
|
if (segment.category == SegmentCategory.HIGHLIGHT) {
|
||||||
right = left + getHighlightSegmentTimeBarScreenWidth();
|
right = left + highlightSegmentTimeBarScreenWidth;
|
||||||
} else {
|
} else {
|
||||||
right = leftPadding + segment.end * videoMillisecondsToPixels;
|
right = leftPadding + segment.end * videoMillisecondsToPixels;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import app.revanced.extension.shared.Logger;
|
|||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
import app.revanced.extension.shared.settings.Setting;
|
import app.revanced.extension.shared.settings.Setting;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockPreferenceGroup;
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
|
import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour;
|
||||||
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ public class SponsorBlockSettings {
|
|||||||
@Override
|
@Override
|
||||||
public void settingsImported(@Nullable Context context) {
|
public void settingsImported(@Nullable Context context) {
|
||||||
SegmentCategory.loadAllCategoriesFromSettings();
|
SegmentCategory.loadAllCategoriesFromSettings();
|
||||||
|
SponsorBlockPreferenceGroup.settingsImported = true;
|
||||||
}
|
}
|
||||||
@Override
|
@Override
|
||||||
public void settingsExported(@Nullable Context context) {
|
public void settingsExported(@Nullable Context context) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package app.revanced.extension.youtube.sponsorblock.objects;
|
package app.revanced.extension.youtube.sponsorblock.objects;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.sf;
|
import static app.revanced.extension.shared.StringRef.sf;
|
||||||
|
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.COLOR_DOT_STRING;
|
||||||
import static app.revanced.extension.youtube.settings.Settings.*;
|
import static app.revanced.extension.youtube.settings.Settings.*;
|
||||||
|
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
@@ -9,7 +10,9 @@ import android.text.Spannable;
|
|||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
|
import android.text.style.RelativeSizeSpan;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
@@ -134,7 +137,8 @@ public enum SegmentCategory {
|
|||||||
updateEnabledCategories();
|
updateEnabledCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int applyOpacityToColor(int color, float opacity) {
|
@ColorInt
|
||||||
|
public static int applyOpacityToColor(@ColorInt int color, float opacity) {
|
||||||
if (opacity < 0 || opacity > 1.0f) {
|
if (opacity < 0 || opacity > 1.0f) {
|
||||||
throw new IllegalArgumentException("Invalid opacity: " + opacity);
|
throw new IllegalArgumentException("Invalid opacity: " + opacity);
|
||||||
}
|
}
|
||||||
@@ -165,29 +169,28 @@ public enum SegmentCategory {
|
|||||||
/**
|
/**
|
||||||
* Skipped segment toast, if the skip occurred in the first quarter of the video
|
* Skipped segment toast, if the skip occurred in the first quarter of the video
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public final StringRef skippedToastBeginning;
|
public final StringRef skippedToastBeginning;
|
||||||
/**
|
/**
|
||||||
* Skipped segment toast, if the skip occurred in the middle half of the video
|
* Skipped segment toast, if the skip occurred in the middle half of the video
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public final StringRef skippedToastMiddle;
|
public final StringRef skippedToastMiddle;
|
||||||
/**
|
/**
|
||||||
* Skipped segment toast, if the skip occurred in the last quarter of the video
|
* Skipped segment toast, if the skip occurred in the last quarter of the video
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public final StringRef skippedToastEnd;
|
public final StringRef skippedToastEnd;
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public final Paint paint;
|
public final Paint paint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Category color with opacity applied.
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
private int color;
|
private int color;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
|
* Value must be changed using {@link #setBehaviour(CategoryBehaviour)}.
|
||||||
* Caller must also {@link #updateEnabledCategories()}.
|
* Caller must also {@link #updateEnabledCategories()}.
|
||||||
*/
|
*/
|
||||||
@NonNull
|
|
||||||
public CategoryBehaviour behaviour = CategoryBehaviour.IGNORE;
|
public CategoryBehaviour behaviour = CategoryBehaviour.IGNORE;
|
||||||
|
|
||||||
SegmentCategory(String keyValue, StringRef title, StringRef description,
|
SegmentCategory(String keyValue, StringRef title, StringRef description,
|
||||||
@@ -247,7 +250,7 @@ public enum SegmentCategory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setBehaviour(@NonNull CategoryBehaviour behaviour) {
|
public void setBehaviour(CategoryBehaviour behaviour) {
|
||||||
this.behaviour = Objects.requireNonNull(behaviour);
|
this.behaviour = Objects.requireNonNull(behaviour);
|
||||||
this.behaviorSetting.save(behaviour.reVancedKeyValue);
|
this.behaviorSetting.save(behaviour.reVancedKeyValue);
|
||||||
}
|
}
|
||||||
@@ -273,6 +276,10 @@ public enum SegmentCategory {
|
|||||||
return opacitySetting.get();
|
return opacitySetting.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public float getOpacityDefault() {
|
||||||
|
return opacitySetting.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
public void resetColorAndOpacity() {
|
public void resetColorAndOpacity() {
|
||||||
setColor(colorSetting.defaultValue);
|
setColor(colorSetting.defaultValue);
|
||||||
setOpacity(opacitySetting.defaultValue);
|
setOpacity(opacitySetting.defaultValue);
|
||||||
@@ -291,10 +298,19 @@ public enum SegmentCategory {
|
|||||||
/**
|
/**
|
||||||
* @return Integer color of #RRGGBB format.
|
* @return Integer color of #RRGGBB format.
|
||||||
*/
|
*/
|
||||||
|
@ColorInt
|
||||||
public int getColorNoOpacity() {
|
public int getColorNoOpacity() {
|
||||||
return color & 0x00FFFFFF;
|
return color & 0x00FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Integer color of #RRGGBB format.
|
||||||
|
*/
|
||||||
|
@ColorInt
|
||||||
|
public int getColorNoOpacityDefault() {
|
||||||
|
return Color.parseColor(colorSetting.defaultValue) & 0x00FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Hex color string of #RRGGBB format with no opacity level.
|
* @return Hex color string of #RRGGBB format with no opacity level.
|
||||||
*/
|
*/
|
||||||
@@ -302,22 +318,27 @@ public enum SegmentCategory {
|
|||||||
return String.format(Locale.US, "#%06X", getColorNoOpacity());
|
return String.format(Locale.US, "#%06X", getColorNoOpacity());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SpannableString getCategoryColorDotSpan(String text, int color) {
|
private static SpannableString getCategoryColorDotSpan(String text, @ColorInt int color) {
|
||||||
SpannableString dotSpan = new SpannableString('⬤' + text);
|
SpannableString dotSpan = new SpannableString(COLOR_DOT_STRING + text);
|
||||||
dotSpan.setSpan(new ForegroundColorSpan(color), 0, 1,
|
dotSpan.setSpan(new ForegroundColorSpan(color), 0, 1,
|
||||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
return dotSpan;
|
return dotSpan;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SpannableString getCategoryColorDot(int color) {
|
public static SpannableString getCategoryColorDot(@ColorInt int color) {
|
||||||
return getCategoryColorDotSpan("", color);
|
SpannableString dotSpan = new SpannableString(COLOR_DOT_STRING);
|
||||||
|
dotSpan.setSpan(new ForegroundColorSpan(color), 0, 1,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
dotSpan.setSpan(new RelativeSizeSpan(1.5f), 0, 1,
|
||||||
|
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
return dotSpan;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpannableString getCategoryColorDot() {
|
public SpannableString getCategoryColorDot() {
|
||||||
return getCategoryColorDot(color);
|
return getCategoryColorDot(color);
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpannableString getTitleWithColorDot(int categoryColor) {
|
public SpannableString getTitleWithColorDot(@ColorInt int categoryColor) {
|
||||||
return getCategoryColorDotSpan(" " + title, categoryColor);
|
return getCategoryColorDotSpan(" " + title, categoryColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,46 @@
|
|||||||
package app.revanced.extension.youtube.sponsorblock.objects;
|
package app.revanced.extension.youtube.sponsorblock.objects;
|
||||||
|
|
||||||
import static app.revanced.extension.shared.StringRef.str;
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.getResourceIdentifier;
|
||||||
|
import static app.revanced.extension.shared.settings.preference.ColorPickerPreference.getColorString;
|
||||||
import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor;
|
import static app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory.applyOpacityToColor;
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Typeface;
|
||||||
|
import android.os.Bundle;
|
||||||
import android.preference.ListPreference;
|
import android.preference.ListPreference;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.GridLayout;
|
import android.widget.GridLayout;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
import app.revanced.extension.shared.Utils;
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ColorPickerPreference;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ColorPickerView;
|
||||||
|
|
||||||
@SuppressWarnings("deprecation")
|
@SuppressWarnings("deprecation")
|
||||||
public class SegmentCategoryListPreference extends ListPreference {
|
public class SegmentCategoryListPreference extends ListPreference {
|
||||||
private final SegmentCategory category;
|
private final SegmentCategory category;
|
||||||
private TextView colorDotView;
|
|
||||||
private EditText colorEditText;
|
|
||||||
private EditText opacityEditText;
|
|
||||||
/**
|
/**
|
||||||
* #RRGGBB
|
* RGB format (no alpha).
|
||||||
*/
|
*/
|
||||||
|
@ColorInt
|
||||||
private int categoryColor;
|
private int categoryColor;
|
||||||
/**
|
/**
|
||||||
* [0, 1]
|
* [0, 1]
|
||||||
@@ -37,6 +48,11 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
private float categoryOpacity;
|
private float categoryOpacity;
|
||||||
private int selectedDialogEntryIndex;
|
private int selectedDialogEntryIndex;
|
||||||
|
|
||||||
|
private TextView dialogColorDotView;
|
||||||
|
private EditText dialogColorEditText;
|
||||||
|
private EditText dialogOpacityEditText;
|
||||||
|
private ColorPickerView dialogColorPickerView;
|
||||||
|
|
||||||
public SegmentCategoryListPreference(Context context, SegmentCategory category) {
|
public SegmentCategoryListPreference(Context context, SegmentCategory category) {
|
||||||
super(context);
|
super(context);
|
||||||
this.category = Objects.requireNonNull(category);
|
this.category = Objects.requireNonNull(category);
|
||||||
@@ -53,9 +69,9 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
setEntryValues(isHighlightCategory
|
setEntryValues(isHighlightCategory
|
||||||
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
|
? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce()
|
||||||
: CategoryBehaviour.getBehaviorKeyValues());
|
: CategoryBehaviour.getBehaviorKeyValues());
|
||||||
setSummary(category.description.toString());
|
super.setSummary(category.description.toString());
|
||||||
|
|
||||||
updateTitleFromCategory();
|
updateUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -67,8 +83,20 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
categoryOpacity = category.getOpacity();
|
categoryOpacity = category.getOpacity();
|
||||||
|
|
||||||
Context context = builder.getContext();
|
Context context = builder.getContext();
|
||||||
|
LinearLayout mainLayout = new LinearLayout(context);
|
||||||
|
mainLayout.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
mainLayout.setPadding(70, 0, 70, 0);
|
||||||
|
|
||||||
|
// Inflate the color picker view.
|
||||||
|
View colorPickerContainer = LayoutInflater.from(context)
|
||||||
|
.inflate(getResourceIdentifier("revanced_color_picker", "layout"), null);
|
||||||
|
dialogColorPickerView = colorPickerContainer.findViewById(
|
||||||
|
getResourceIdentifier("color_picker_view", "id"));
|
||||||
|
dialogColorPickerView.setColor(categoryColor);
|
||||||
|
mainLayout.addView(colorPickerContainer);
|
||||||
|
|
||||||
|
// Grid layout for color and opacity inputs.
|
||||||
GridLayout gridLayout = new GridLayout(context);
|
GridLayout gridLayout = new GridLayout(context);
|
||||||
gridLayout.setPadding(70, 0, 150, 0); // Padding for the entire layout.
|
|
||||||
gridLayout.setColumnCount(3);
|
gridLayout.setColumnCount(3);
|
||||||
gridLayout.setRowCount(2);
|
gridLayout.setRowCount(2);
|
||||||
|
|
||||||
@@ -84,19 +112,22 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
||||||
gridParams.columnSpec = GridLayout.spec(1); // Second column.
|
gridParams.columnSpec = GridLayout.spec(1); // Second column.
|
||||||
gridParams.setMargins(0, 0, 10, 0);
|
gridParams.setMargins(0, 0, 10, 0);
|
||||||
colorDotView = new TextView(context);
|
dialogColorDotView = new TextView(context);
|
||||||
colorDotView.setLayoutParams(gridParams);
|
dialogColorDotView.setLayoutParams(gridParams);
|
||||||
gridLayout.addView(colorDotView);
|
gridLayout.addView(dialogColorDotView);
|
||||||
updateCategoryColorDot();
|
updateCategoryColorDot();
|
||||||
|
|
||||||
gridParams = new GridLayout.LayoutParams();
|
gridParams = new GridLayout.LayoutParams();
|
||||||
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
gridParams.rowSpec = GridLayout.spec(0); // First row.
|
||||||
gridParams.columnSpec = GridLayout.spec(2); // Third column.
|
gridParams.columnSpec = GridLayout.spec(2); // Third column.
|
||||||
colorEditText = new EditText(context);
|
dialogColorEditText = new EditText(context);
|
||||||
colorEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS);
|
dialogColorEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
|
||||||
colorEditText.setTextLocale(Locale.US);
|
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||||
colorEditText.setText(category.getColorString());
|
dialogColorEditText.setAutofillHints((String) null);
|
||||||
colorEditText.addTextChangedListener(new TextWatcher() {
|
dialogColorEditText.setTypeface(Typeface.MONOSPACE);
|
||||||
|
dialogColorEditText.setTextLocale(Locale.US);
|
||||||
|
dialogColorEditText.setText(getColorString(categoryColor));
|
||||||
|
dialogColorEditText.addTextChangedListener(new TextWatcher() {
|
||||||
@Override
|
@Override
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
}
|
}
|
||||||
@@ -109,28 +140,30 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
public void afterTextChanged(Editable edit) {
|
public void afterTextChanged(Editable edit) {
|
||||||
try {
|
try {
|
||||||
String colorString = edit.toString();
|
String colorString = edit.toString();
|
||||||
final int colorStringLength = colorString.length();
|
String normalizedColorString = ColorPickerPreference.cleanupColorCodeString(colorString);
|
||||||
|
|
||||||
if (!colorString.startsWith("#")) {
|
if (!normalizedColorString.equals(colorString)) {
|
||||||
edit.insert(0, "#"); // Recursively calls back into this method.
|
edit.replace(0, colorString.length(), normalizedColorString);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final int maxColorStringLength = 7; // #RRGGBB
|
if (normalizedColorString.length() != ColorPickerPreference.COLOR_STRING_LENGTH) {
|
||||||
if (colorStringLength > maxColorStringLength) {
|
// User is still typing out the color.
|
||||||
edit.delete(maxColorStringLength, colorStringLength);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
categoryColor = Color.parseColor(colorString);
|
// Remove the alpha channel.
|
||||||
updateCategoryColorDot();
|
final int newColor = Color.parseColor(colorString) & 0x00FFFFFF;
|
||||||
} catch (IllegalArgumentException ex) {
|
// Changing view color causes callback into this class.
|
||||||
// Ignore.
|
dialogColorPickerView.setColor(newColor);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
// Should never be reached since input is validated before using.
|
||||||
|
Logger.printException(() -> "colorEditText afterTextChanged failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
colorEditText.setLayoutParams(gridParams);
|
dialogColorEditText.setLayoutParams(gridParams);
|
||||||
gridLayout.addView(colorEditText);
|
gridLayout.addView(dialogColorEditText);
|
||||||
|
|
||||||
gridParams = new GridLayout.LayoutParams();
|
gridParams = new GridLayout.LayoutParams();
|
||||||
gridParams.rowSpec = GridLayout.spec(1); // Second row.
|
gridParams.rowSpec = GridLayout.spec(1); // Second row.
|
||||||
@@ -143,9 +176,13 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
gridParams = new GridLayout.LayoutParams();
|
gridParams = new GridLayout.LayoutParams();
|
||||||
gridParams.rowSpec = GridLayout.spec(1); // Second row.
|
gridParams.rowSpec = GridLayout.spec(1); // Second row.
|
||||||
gridParams.columnSpec = GridLayout.spec(2); // Third column.
|
gridParams.columnSpec = GridLayout.spec(2); // Third column.
|
||||||
opacityEditText = new EditText(context);
|
dialogOpacityEditText = new EditText(context);
|
||||||
opacityEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
dialogOpacityEditText.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL
|
||||||
opacityEditText.addTextChangedListener(new TextWatcher() {
|
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||||
|
dialogOpacityEditText.setAutofillHints((String) null);
|
||||||
|
dialogOpacityEditText.setTypeface(Typeface.MONOSPACE);
|
||||||
|
dialogOpacityEditText.setTextLocale(Locale.US);
|
||||||
|
dialogOpacityEditText.addTextChangedListener(new TextWatcher() {
|
||||||
@Override
|
@Override
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
}
|
}
|
||||||
@@ -183,31 +220,40 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateCategoryColorDot();
|
updateCategoryColorDot();
|
||||||
} catch (NumberFormatException ex) {
|
} catch (Exception ex) {
|
||||||
// Should never happen.
|
// Should never happen.
|
||||||
Logger.printException(() -> "Could not parse opacity string", ex);
|
Logger.printException(() -> "opacityEditText afterTextChanged failure", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
opacityEditText.setLayoutParams(gridParams);
|
dialogOpacityEditText.setLayoutParams(gridParams);
|
||||||
gridLayout.addView(opacityEditText);
|
gridLayout.addView(dialogOpacityEditText);
|
||||||
updateOpacityText();
|
updateOpacityText();
|
||||||
|
|
||||||
builder.setView(gridLayout);
|
mainLayout.addView(gridLayout);
|
||||||
|
|
||||||
|
// Set up color picker listener.
|
||||||
|
// Do last to prevent listener callbacks while setting up view.
|
||||||
|
dialogColorPickerView.setOnColorChangedListener(color -> {
|
||||||
|
if (categoryColor == color) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
categoryColor = color;
|
||||||
|
String hexColor = getColorString(color);
|
||||||
|
Logger.printDebug(() -> "onColorChanged: " + hexColor);
|
||||||
|
|
||||||
|
updateCategoryColorDot();
|
||||||
|
dialogColorEditText.setText(hexColor);
|
||||||
|
dialogColorEditText.setSelection(hexColor.length());
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.setView(mainLayout);
|
||||||
builder.setTitle(category.title.toString());
|
builder.setTitle(category.title.toString());
|
||||||
|
|
||||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||||
onClick(dialog, DialogInterface.BUTTON_POSITIVE);
|
onClick(dialog, DialogInterface.BUTTON_POSITIVE);
|
||||||
});
|
});
|
||||||
builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> {
|
builder.setNeutralButton(str("revanced_settings_reset_color"), null);
|
||||||
try {
|
|
||||||
category.resetColorAndOpacity();
|
|
||||||
updateTitleFromCategory();
|
|
||||||
Utils.showToastShort(str("revanced_sb_color_reset"));
|
|
||||||
} catch (Exception ex) {
|
|
||||||
Logger.printException(() -> "setNeutralButton failure", ex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
builder.setNegativeButton(android.R.string.cancel, null);
|
builder.setNegativeButton(android.R.string.cancel, null);
|
||||||
|
|
||||||
selectedDialogEntryIndex = findIndexOfValue(getValue());
|
selectedDialogEntryIndex = findIndexOfValue(getValue());
|
||||||
@@ -218,6 +264,25 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void showDialog(Bundle state) {
|
||||||
|
super.showDialog(state);
|
||||||
|
|
||||||
|
// Do not close dialog when reset is pressed.
|
||||||
|
Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL);
|
||||||
|
button.setOnClickListener(view -> {
|
||||||
|
try {
|
||||||
|
// Setting view color causes callback to update the UI.
|
||||||
|
dialogColorPickerView.setColor(category.getColorNoOpacityDefault());
|
||||||
|
|
||||||
|
categoryOpacity = category.getOpacityDefault();
|
||||||
|
updateOpacityText();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "setOnClickListener failure", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDialogClosed(boolean positiveResult) {
|
protected void onDialogClosed(boolean positiveResult) {
|
||||||
try {
|
try {
|
||||||
@@ -230,42 +295,50 @@ public class SegmentCategoryListPreference extends ListPreference {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String colorString = colorEditText.getText().toString();
|
category.setColor(dialogColorEditText.getText().toString());
|
||||||
if (!colorString.equals(category.getColorString()) || categoryOpacity != category.getOpacity()) {
|
category.setOpacity(categoryOpacity);
|
||||||
category.setColor(colorString);
|
|
||||||
category.setOpacity(categoryOpacity);
|
|
||||||
Utils.showToastShort(str("revanced_sb_color_changed"));
|
|
||||||
}
|
|
||||||
} catch (IllegalArgumentException ex) {
|
} catch (IllegalArgumentException ex) {
|
||||||
Utils.showToastShort(str("revanced_sb_color_invalid"));
|
Utils.showToastShort(str("revanced_settings_color_invalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTitleFromCategory();
|
updateUI();
|
||||||
}
|
}
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "onDialogClosed failure", ex);
|
Logger.printException(() -> "onDialogClosed failure", ex);
|
||||||
|
} finally {
|
||||||
|
dialogColorDotView = null;
|
||||||
|
dialogColorEditText = null;
|
||||||
|
dialogOpacityEditText = null;
|
||||||
|
dialogColorPickerView = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyOpacityToCategoryColor() {
|
@ColorInt
|
||||||
categoryColor = applyOpacityToColor(categoryColor, categoryOpacity);
|
private int applyOpacityToCategoryColor() {
|
||||||
|
return applyOpacityToColor(categoryColor, categoryOpacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateTitleFromCategory() {
|
public void updateUI() {
|
||||||
categoryColor = category.getColorNoOpacity();
|
categoryColor = category.getColorNoOpacity();
|
||||||
categoryOpacity = category.getOpacity();
|
categoryOpacity = category.getOpacity();
|
||||||
applyOpacityToCategoryColor();
|
|
||||||
|
|
||||||
setTitle(category.getTitleWithColorDot(categoryColor));
|
setTitle(category.getTitleWithColorDot(applyOpacityToCategoryColor()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCategoryColorDot() {
|
private void updateCategoryColorDot() {
|
||||||
applyOpacityToCategoryColor();
|
dialogColorDotView.setText(SegmentCategory.getCategoryColorDot(applyOpacityToCategoryColor()));
|
||||||
|
|
||||||
colorDotView.setText(SegmentCategory.getCategoryColorDot(categoryColor));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateOpacityText() {
|
private void updateOpacityText() {
|
||||||
opacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity));
|
dialogOpacityEditText.setText(String.format(Locale.US, "%.2f", categoryOpacity));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@Override
|
||||||
|
public void setSummary(CharSequence summary) {
|
||||||
|
// Ignore calls to set the summary.
|
||||||
|
// Summary is always the description of the category.
|
||||||
|
//
|
||||||
|
// This is required otherwise the ReVanced preference fragment
|
||||||
|
// sets all ListPreference summaries to show the current selection.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,13 +5,19 @@ import androidx.annotation.NonNull;
|
|||||||
import org.json.JSONException;
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SponsorBlock user stats
|
* SponsorBlock user stats
|
||||||
*/
|
*/
|
||||||
public class UserStats {
|
public class UserStats {
|
||||||
@NonNull
|
/**
|
||||||
|
* How long to cache user stats objects.
|
||||||
|
*/
|
||||||
|
private static final long STATS_EXPIRATION_MILLISECONDS = 60 * 60 * 1000; // 60 minutes.
|
||||||
|
|
||||||
|
private final String privateUserId;
|
||||||
public final String publicUserId;
|
public final String publicUserId;
|
||||||
@NonNull
|
|
||||||
public final String userName;
|
public final String userName;
|
||||||
/**
|
/**
|
||||||
* "User reputation". Unclear how SB determines this value.
|
* "User reputation". Unclear how SB determines this value.
|
||||||
@@ -26,7 +32,13 @@ public class UserStats {
|
|||||||
public final int viewCount;
|
public final int viewCount;
|
||||||
public final double minutesSaved;
|
public final double minutesSaved;
|
||||||
|
|
||||||
public UserStats(@NonNull JSONObject json) throws JSONException {
|
/**
|
||||||
|
* When this stat was fetched.
|
||||||
|
*/
|
||||||
|
public final long fetchTime;
|
||||||
|
|
||||||
|
public UserStats(String privateSbId, @NonNull JSONObject json) throws JSONException {
|
||||||
|
privateUserId = privateSbId;
|
||||||
publicUserId = json.getString("userID");
|
publicUserId = json.getString("userID");
|
||||||
userName = json.getString("userName");
|
userName = json.getString("userName");
|
||||||
reputation = (float)json.getDouble("reputation");
|
reputation = (float)json.getDouble("reputation");
|
||||||
@@ -35,11 +47,23 @@ public class UserStats {
|
|||||||
totalSegmentCountIncludingIgnored = segmentCount + ignoredSegmentCount;
|
totalSegmentCountIncludingIgnored = segmentCount + ignoredSegmentCount;
|
||||||
viewCount = json.getInt("viewCount");
|
viewCount = json.getInt("viewCount");
|
||||||
minutesSaved = json.getDouble("minutesSaved");
|
minutesSaved = json.getDouble("minutesSaved");
|
||||||
|
fetchTime = System.currentTimeMillis();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isExpired() {
|
||||||
|
if (STATS_EXPIRATION_MILLISECONDS < System.currentTimeMillis() - fetchTime) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User changed their SB private user id.
|
||||||
|
return !SponsorBlockSettings.userHasSBPrivateId()
|
||||||
|
|| !SponsorBlockSettings.getSBPrivateUserID().equals(privateUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
// Do not include private user id in toString().
|
||||||
return "UserStats{"
|
return "UserStats{"
|
||||||
+ "publicUserId='" + publicUserId + '\''
|
+ "publicUserId='" + publicUserId + '\''
|
||||||
+ ", userName='" + userName + '\''
|
+ ", userName='" + userName + '\''
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ public class SBRequester {
|
|||||||
*/
|
*/
|
||||||
private static final int HTTP_STATUS_CODE_SUCCESS = 200;
|
private static final int HTTP_STATUS_CODE_SUCCESS = 200;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static volatile UserStats lastFetchedStats;
|
||||||
|
|
||||||
private SBRequester() {
|
private SBRequester() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,6 +184,8 @@ public class SBRequester {
|
|||||||
Utils.showToastLong(str("revanced_sb_submit_failed_unknown_error", 0, ex.getMessage()));
|
Utils.showToastLong(str("revanced_sb_submit_failed_unknown_error", 0, ex.getMessage()));
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "failed to submit segments", ex); // Should never happen.
|
Logger.printException(() -> "failed to submit segments", ex); // Should never happen.
|
||||||
|
} finally {
|
||||||
|
lastFetchedStats = null; // Fetch updated stats if needed.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,9 +257,17 @@ public class SBRequester {
|
|||||||
public static UserStats retrieveUserStats() {
|
public static UserStats retrieveUserStats() {
|
||||||
Utils.verifyOffMainThread();
|
Utils.verifyOffMainThread();
|
||||||
try {
|
try {
|
||||||
UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SponsorBlockSettings.getSBPrivateUserID()));
|
UserStats stats = lastFetchedStats;
|
||||||
Logger.printDebug(() -> "user stats: " + stats);
|
if (stats != null && !stats.isExpired()) {
|
||||||
return stats;
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
String privateUserID = SponsorBlockSettings.getSBPrivateUserID();
|
||||||
|
UserStats fetchedStats = new UserStats(privateUserID,
|
||||||
|
getJSONObject(SBRoutes.GET_USER_STATS, privateUserID));
|
||||||
|
Logger.printDebug(() -> "user stats: " + fetchedStats);
|
||||||
|
lastFetchedStats = fetchedStats;
|
||||||
|
return fetchedStats;
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
Logger.printInfo(() -> "failed to retrieve user stats", ex); // info level, do not show a toast
|
Logger.printInfo(() -> "failed to retrieve user stats", ex); // info level, do not show a toast
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package app.revanced.extension.youtube.sponsorblock.ui;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import app.revanced.extension.youtube.settings.preference.UrlLinkPreference;
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public class SponsorBlockAboutPreference extends UrlLinkPreference {
|
||||||
|
{
|
||||||
|
externalUrl = "https://sponsor.ajay.app";
|
||||||
|
}
|
||||||
|
|
||||||
|
public SponsorBlockAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
public SponsorBlockAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
public SponsorBlockAboutPreference(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
public SponsorBlockAboutPreference(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,479 @@
|
|||||||
|
package app.revanced.extension.youtube.sponsorblock.ui;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.preference.*;
|
||||||
|
import android.text.Html;
|
||||||
|
import android.text.InputType;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.EditText;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryListPreference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lots of old code that could be converted to a half dozen custom preferences,
|
||||||
|
* but instead it's wrapped in this group container and all logic is handled here.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class SponsorBlockPreferenceGroup extends PreferenceGroup {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReVanced settings were recently imported and the UI needs to be updated.
|
||||||
|
*/
|
||||||
|
public static boolean settingsImported;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the preferences have been created and added to this group.
|
||||||
|
*/
|
||||||
|
private boolean preferencesInitialized;
|
||||||
|
|
||||||
|
private SwitchPreference sbEnabled;
|
||||||
|
private SwitchPreference addNewSegment;
|
||||||
|
private SwitchPreference votingEnabled;
|
||||||
|
private SwitchPreference autoHideSkipSegmentButton;
|
||||||
|
private SwitchPreference compactSkipButton;
|
||||||
|
private SwitchPreference squareLayout;
|
||||||
|
private SwitchPreference showSkipToast;
|
||||||
|
private SwitchPreference trackSkips;
|
||||||
|
private SwitchPreference showTimeWithoutSegments;
|
||||||
|
private SwitchPreference toastOnConnectionError;
|
||||||
|
|
||||||
|
private ResettableEditTextPreference newSegmentStep;
|
||||||
|
private ResettableEditTextPreference minSegmentDuration;
|
||||||
|
private EditTextPreference privateUserId;
|
||||||
|
private EditTextPreference importExport;
|
||||||
|
private Preference apiUrl;
|
||||||
|
|
||||||
|
private final List<SegmentCategoryListPreference> segmentCategories = new ArrayList<>();
|
||||||
|
private PreferenceCategory segmentCategory;
|
||||||
|
|
||||||
|
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SponsorBlockPreferenceGroup(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressLint("MissingSuperCall")
|
||||||
|
protected View onCreateView(ViewGroup parent) {
|
||||||
|
// Title is not shown.
|
||||||
|
return new View(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateUI() {
|
||||||
|
try {
|
||||||
|
Logger.printDebug(() -> "updateUI");
|
||||||
|
|
||||||
|
final boolean enabled = Settings.SB_ENABLED.get();
|
||||||
|
if (!enabled) {
|
||||||
|
SponsorBlockViewController.hideAll();
|
||||||
|
SegmentPlaybackController.setCurrentVideoId(null);
|
||||||
|
} else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) {
|
||||||
|
SponsorBlockViewController.hideNewSegmentLayout();
|
||||||
|
}
|
||||||
|
// Voting and add new segment buttons automatically show/hide themselves.
|
||||||
|
|
||||||
|
SponsorBlockViewController.updateLayout();
|
||||||
|
|
||||||
|
sbEnabled.setChecked(enabled);
|
||||||
|
|
||||||
|
addNewSegment.setChecked(Settings.SB_CREATE_NEW_SEGMENT.get());
|
||||||
|
addNewSegment.setEnabled(enabled);
|
||||||
|
|
||||||
|
votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get());
|
||||||
|
votingEnabled.setEnabled(enabled);
|
||||||
|
|
||||||
|
autoHideSkipSegmentButton.setEnabled(enabled);
|
||||||
|
autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get());
|
||||||
|
|
||||||
|
compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get());
|
||||||
|
compactSkipButton.setEnabled(enabled);
|
||||||
|
|
||||||
|
squareLayout.setChecked(Settings.SB_SQUARE_LAYOUT.get());
|
||||||
|
squareLayout.setEnabled(enabled);
|
||||||
|
|
||||||
|
showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get());
|
||||||
|
showSkipToast.setEnabled(enabled);
|
||||||
|
|
||||||
|
toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get());
|
||||||
|
toastOnConnectionError.setEnabled(enabled);
|
||||||
|
|
||||||
|
trackSkips.setChecked(Settings.SB_TRACK_SKIP_COUNT.get());
|
||||||
|
trackSkips.setEnabled(enabled);
|
||||||
|
|
||||||
|
showTimeWithoutSegments.setChecked(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get());
|
||||||
|
showTimeWithoutSegments.setEnabled(enabled);
|
||||||
|
|
||||||
|
newSegmentStep.setText((Settings.SB_CREATE_NEW_SEGMENT_STEP.get()).toString());
|
||||||
|
newSegmentStep.setEnabled(enabled);
|
||||||
|
|
||||||
|
minSegmentDuration.setText((Settings.SB_SEGMENT_MIN_DURATION.get()).toString());
|
||||||
|
minSegmentDuration.setEnabled(enabled);
|
||||||
|
|
||||||
|
privateUserId.setText(Settings.SB_PRIVATE_USER_ID.get());
|
||||||
|
privateUserId.setEnabled(enabled);
|
||||||
|
|
||||||
|
// If the user has a private user id, then include a subtext that mentions not to share it.
|
||||||
|
String importExportSummary = SponsorBlockSettings.userHasSBPrivateId()
|
||||||
|
? str("revanced_sb_settings_ie_sum_warning")
|
||||||
|
: str("revanced_sb_settings_ie_sum");
|
||||||
|
importExport.setSummary(importExportSummary);
|
||||||
|
|
||||||
|
apiUrl.setEnabled(enabled);
|
||||||
|
importExport.setEnabled(enabled);
|
||||||
|
segmentCategory.setEnabled(enabled);
|
||||||
|
|
||||||
|
for (SegmentCategoryListPreference category : segmentCategories) {
|
||||||
|
category.updateUI();
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "updateUI failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onAttachedToActivity() {
|
||||||
|
try {
|
||||||
|
super.onAttachedToActivity();
|
||||||
|
|
||||||
|
if (preferencesInitialized) {
|
||||||
|
if (settingsImported) {
|
||||||
|
settingsImported = false;
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preferencesInitialized = true;
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Creating settings preferences");
|
||||||
|
Context context = getContext();
|
||||||
|
SponsorBlockSettings.initialize();
|
||||||
|
|
||||||
|
sbEnabled = new SwitchPreference(context);
|
||||||
|
sbEnabled.setTitle(str("revanced_sb_enable_sb"));
|
||||||
|
sbEnabled.setSummary(str("revanced_sb_enable_sb_sum"));
|
||||||
|
addPreference(sbEnabled);
|
||||||
|
sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_ENABLED.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
PreferenceCategory appearanceCategory = new PreferenceCategory(context);
|
||||||
|
appearanceCategory.setTitle(str("revanced_sb_appearance_category"));
|
||||||
|
addPreference(appearanceCategory);
|
||||||
|
|
||||||
|
votingEnabled = new SwitchPreference(context);
|
||||||
|
votingEnabled.setTitle(str("revanced_sb_enable_voting"));
|
||||||
|
votingEnabled.setSummaryOn(str("revanced_sb_enable_voting_sum_on"));
|
||||||
|
votingEnabled.setSummaryOff(str("revanced_sb_enable_voting_sum_off"));
|
||||||
|
votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_VOTING_BUTTON.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(votingEnabled);
|
||||||
|
|
||||||
|
autoHideSkipSegmentButton = new SwitchPreference(context);
|
||||||
|
autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button"));
|
||||||
|
autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on"));
|
||||||
|
autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off"));
|
||||||
|
autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(autoHideSkipSegmentButton);
|
||||||
|
|
||||||
|
compactSkipButton = new SwitchPreference(context);
|
||||||
|
compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button"));
|
||||||
|
compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on"));
|
||||||
|
compactSkipButton.setSummaryOff(str("revanced_sb_enable_compact_skip_button_sum_off"));
|
||||||
|
compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(compactSkipButton);
|
||||||
|
|
||||||
|
squareLayout = new SwitchPreference(context);
|
||||||
|
squareLayout.setTitle(str("revanced_sb_square_layout"));
|
||||||
|
squareLayout.setSummaryOn(str("revanced_sb_square_layout_sum_on"));
|
||||||
|
squareLayout.setSummaryOff(str("revanced_sb_square_layout_sum_off"));
|
||||||
|
squareLayout.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_SQUARE_LAYOUT.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(squareLayout);
|
||||||
|
|
||||||
|
showSkipToast = new SwitchPreference(context);
|
||||||
|
showSkipToast.setTitle(str("revanced_sb_general_skiptoast"));
|
||||||
|
showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on"));
|
||||||
|
showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off"));
|
||||||
|
showSkipToast.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
Utils.showToastShort(str("revanced_sb_skipped_sponsor"));
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(showSkipToast);
|
||||||
|
|
||||||
|
showTimeWithoutSegments = new SwitchPreference(context);
|
||||||
|
showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without"));
|
||||||
|
showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on"));
|
||||||
|
showTimeWithoutSegments.setSummaryOff(str("revanced_sb_general_time_without_sum_off"));
|
||||||
|
showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
appearanceCategory.addPreference(showTimeWithoutSegments);
|
||||||
|
|
||||||
|
segmentCategory = new PreferenceCategory(context);
|
||||||
|
segmentCategory.setTitle(str("revanced_sb_diff_segments"));
|
||||||
|
addPreference(segmentCategory);
|
||||||
|
|
||||||
|
for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) {
|
||||||
|
SegmentCategoryListPreference categoryPreference = new SegmentCategoryListPreference(context, category);
|
||||||
|
segmentCategories.add(categoryPreference);
|
||||||
|
segmentCategory.addPreference(categoryPreference);
|
||||||
|
}
|
||||||
|
|
||||||
|
PreferenceCategory createSegmentCategory = new PreferenceCategory(context);
|
||||||
|
createSegmentCategory.setTitle(str("revanced_sb_create_segment_category"));
|
||||||
|
addPreference(createSegmentCategory);
|
||||||
|
|
||||||
|
addNewSegment = new SwitchPreference(context);
|
||||||
|
addNewSegment.setTitle(str("revanced_sb_enable_create_segment"));
|
||||||
|
addNewSegment.setSummaryOn(str("revanced_sb_enable_create_segment_sum_on"));
|
||||||
|
addNewSegment.setSummaryOff(str("revanced_sb_enable_create_segment_sum_off"));
|
||||||
|
addNewSegment.setOnPreferenceChangeListener((preference1, o) -> {
|
||||||
|
Boolean newValue = (Boolean) o;
|
||||||
|
if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) {
|
||||||
|
new AlertDialog.Builder(preference1.getContext())
|
||||||
|
.setTitle(str("revanced_sb_guidelines_popup_title"))
|
||||||
|
.setMessage(str("revanced_sb_guidelines_popup_content"))
|
||||||
|
.setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null)
|
||||||
|
.setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines())
|
||||||
|
.setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true))
|
||||||
|
.setCancelable(false)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
Settings.SB_CREATE_NEW_SEGMENT.save(newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
createSegmentCategory.addPreference(addNewSegment);
|
||||||
|
|
||||||
|
newSegmentStep = new ResettableEditTextPreference(context);
|
||||||
|
newSegmentStep.setSetting(Settings.SB_CREATE_NEW_SEGMENT_STEP);
|
||||||
|
newSegmentStep.setTitle(str("revanced_sb_general_adjusting"));
|
||||||
|
newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum"));
|
||||||
|
newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER);
|
||||||
|
newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
try {
|
||||||
|
final int newAdjustmentValue = Integer.parseInt(newValue.toString());
|
||||||
|
if (newAdjustmentValue != 0) {
|
||||||
|
Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
Logger.printInfo(() -> "Invalid new segment step", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils.showToastLong(str("revanced_sb_general_adjusting_invalid"));
|
||||||
|
updateUI();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
createSegmentCategory.addPreference(newSegmentStep);
|
||||||
|
|
||||||
|
Preference guidelinePreferences = new Preference(context);
|
||||||
|
guidelinePreferences.setTitle(str("revanced_sb_guidelines_preference_title"));
|
||||||
|
guidelinePreferences.setSummary(str("revanced_sb_guidelines_preference_sum"));
|
||||||
|
guidelinePreferences.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
openGuidelines();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
createSegmentCategory.addPreference(guidelinePreferences);
|
||||||
|
|
||||||
|
PreferenceCategory generalCategory = new PreferenceCategory(context);
|
||||||
|
generalCategory.setTitle(str("revanced_sb_general"));
|
||||||
|
addPreference(generalCategory);
|
||||||
|
|
||||||
|
toastOnConnectionError = new SwitchPreference(context);
|
||||||
|
toastOnConnectionError.setTitle(str("revanced_sb_toast_on_connection_error_title"));
|
||||||
|
toastOnConnectionError.setSummaryOn(str("revanced_sb_toast_on_connection_error_summary_on"));
|
||||||
|
toastOnConnectionError.setSummaryOff(str("revanced_sb_toast_on_connection_error_summary_off"));
|
||||||
|
toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(toastOnConnectionError);
|
||||||
|
|
||||||
|
trackSkips = new SwitchPreference(context);
|
||||||
|
trackSkips.setTitle(str("revanced_sb_general_skipcount"));
|
||||||
|
trackSkips.setSummaryOn(str("revanced_sb_general_skipcount_sum_on"));
|
||||||
|
trackSkips.setSummaryOff(str("revanced_sb_general_skipcount_sum_off"));
|
||||||
|
trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(trackSkips);
|
||||||
|
|
||||||
|
minSegmentDuration = new ResettableEditTextPreference(context);
|
||||||
|
minSegmentDuration.setSetting(Settings.SB_SEGMENT_MIN_DURATION);
|
||||||
|
minSegmentDuration.setTitle(str("revanced_sb_general_min_duration"));
|
||||||
|
minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum"));
|
||||||
|
minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL);
|
||||||
|
minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
try {
|
||||||
|
Float minTimeDuration = Float.valueOf(newValue.toString());
|
||||||
|
Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration);
|
||||||
|
return true;
|
||||||
|
} catch (NumberFormatException ex) {
|
||||||
|
Logger.printInfo(() -> "Invalid minimum segment duration", ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils.showToastLong(str("revanced_sb_general_min_duration_invalid"));
|
||||||
|
updateUI();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(minSegmentDuration);
|
||||||
|
|
||||||
|
privateUserId = new EditTextPreference(context) {
|
||||||
|
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||||
|
Utils.setEditTextDialogTheme(builder);
|
||||||
|
|
||||||
|
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
||||||
|
try {
|
||||||
|
Utils.setClipboard(getEditText().getText());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "Copy settings failure", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
privateUserId.setTitle(str("revanced_sb_general_uuid"));
|
||||||
|
privateUserId.setSummary(str("revanced_sb_general_uuid_sum"));
|
||||||
|
privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
String newUUID = newValue.toString();
|
||||||
|
if (!SponsorBlockSettings.isValidSBUserId(newUUID)) {
|
||||||
|
Utils.showToastLong(str("revanced_sb_general_uuid_invalid"));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.SB_PRIVATE_USER_ID.save(newUUID);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(privateUserId);
|
||||||
|
|
||||||
|
apiUrl = new Preference(context);
|
||||||
|
apiUrl.setTitle(str("revanced_sb_general_api_url"));
|
||||||
|
apiUrl.setSummary(Html.fromHtml(str("revanced_sb_general_api_url_sum")));
|
||||||
|
apiUrl.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
EditText editText = new EditText(context);
|
||||||
|
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI);
|
||||||
|
editText.setText(Settings.SB_API_URL.get());
|
||||||
|
|
||||||
|
DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> {
|
||||||
|
if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) {
|
||||||
|
Settings.SB_API_URL.resetToDefault();
|
||||||
|
Utils.showToastLong(str("revanced_sb_api_url_reset"));
|
||||||
|
} else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) {
|
||||||
|
String serverAddress = editText.getText().toString();
|
||||||
|
if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) {
|
||||||
|
Utils.showToastLong(str("revanced_sb_api_url_invalid"));
|
||||||
|
} else if (!serverAddress.equals(Settings.SB_API_URL.get())) {
|
||||||
|
Settings.SB_API_URL.save(serverAddress);
|
||||||
|
Utils.showToastLong(str("revanced_sb_api_url_changed"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
new AlertDialog.Builder(context)
|
||||||
|
.setTitle(apiUrl.getTitle())
|
||||||
|
.setView(editText)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setNeutralButton(str("revanced_settings_reset"), urlChangeListener)
|
||||||
|
.setPositiveButton(android.R.string.ok, urlChangeListener)
|
||||||
|
.show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(apiUrl);
|
||||||
|
|
||||||
|
importExport = new EditTextPreference(context) {
|
||||||
|
protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
|
||||||
|
Utils.setEditTextDialogTheme(builder);
|
||||||
|
|
||||||
|
builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> {
|
||||||
|
try {
|
||||||
|
Utils.setClipboard(getEditText().getText());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "Copy settings failure", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
importExport.setTitle(str("revanced_sb_settings_ie"));
|
||||||
|
// Summary is set in updateUI()
|
||||||
|
importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT
|
||||||
|
| InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
||||||
|
| InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||||
|
importExport.getEditText().setAutofillHints((String) null);
|
||||||
|
importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8);
|
||||||
|
importExport.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings());
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
importExport.setOnPreferenceChangeListener((preference1, newValue) -> {
|
||||||
|
SponsorBlockSettings.importDesktopSettings((String) newValue);
|
||||||
|
updateUI();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
generalCategory.addPreference(importExport);
|
||||||
|
|
||||||
|
Utils.setPreferenceTitlesToMultiLineIfNeeded(this);
|
||||||
|
|
||||||
|
updateUI();
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onAttachedToActivity failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openGuidelines() {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
|
intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines"));
|
||||||
|
getContext().startActivity(intent);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package app.revanced.extension.youtube.sponsorblock.ui;
|
||||||
|
|
||||||
|
import static android.text.Html.fromHtml;
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.preference.EditTextPreference;
|
||||||
|
import android.preference.Preference;
|
||||||
|
import android.preference.PreferenceCategory;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.shared.Utils;
|
||||||
|
import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference;
|
||||||
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.objects.UserStats;
|
||||||
|
import app.revanced.extension.youtube.sponsorblock.requests.SBRequester;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User skip stats.
|
||||||
|
*
|
||||||
|
* None of the preferences here show up in search results because
|
||||||
|
* a category cannot be added to another category for the search results.
|
||||||
|
* Additionally the stats must load remotely on a background thread which means the
|
||||||
|
* preferences are not available to collect for search when the settings first load.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "deprecation"})
|
||||||
|
public class SponsorBlockStatsPreferenceCategory extends PreferenceCategory {
|
||||||
|
|
||||||
|
public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SponsorBlockStatsPreferenceCategory(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onAttachedToActivity() {
|
||||||
|
try {
|
||||||
|
super.onAttachedToActivity();
|
||||||
|
|
||||||
|
Logger.printDebug(() -> "Updating SB stats UI");
|
||||||
|
final boolean enabled = Settings.SB_ENABLED.get();
|
||||||
|
setEnabled(enabled);
|
||||||
|
removeAll();
|
||||||
|
|
||||||
|
if (!SponsorBlockSettings.userHasSBPrivateId()) {
|
||||||
|
// User has never voted or created any segments. Only local stats exist.
|
||||||
|
addLocalUserStats();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Preference loadingPlaceholderPreference = new Preference(getContext());
|
||||||
|
loadingPlaceholderPreference.setEnabled(false);
|
||||||
|
addPreference(loadingPlaceholderPreference);
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_loading"));
|
||||||
|
Utils.runOnBackgroundThread(() -> {
|
||||||
|
UserStats stats = SBRequester.retrieveUserStats();
|
||||||
|
Utils.runOnMainThread(() -> { // get back on main thread to modify UI elements
|
||||||
|
addUserStats(loadingPlaceholderPreference, stats);
|
||||||
|
addLocalUserStats();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_sb_disabled"));
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "onAttachedToActivity failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addUserStats(Preference loadingPlaceholder, @Nullable UserStats stats) {
|
||||||
|
Utils.verifyOnMainThread();
|
||||||
|
try {
|
||||||
|
if (stats == null) {
|
||||||
|
loadingPlaceholder.setTitle(str("revanced_sb_stats_connection_failure"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
removeAll();
|
||||||
|
Context context = getContext();
|
||||||
|
|
||||||
|
if (stats.totalSegmentCountIncludingIgnored > 0) {
|
||||||
|
// If user has not created any segments, there's no reason to set a username.
|
||||||
|
String userName = stats.userName;
|
||||||
|
EditTextPreference preference = new ResettableEditTextPreference(context);
|
||||||
|
preference.setTitle(fromHtml(str("revanced_sb_stats_username", userName)));
|
||||||
|
preference.setSummary(str("revanced_sb_stats_username_change"));
|
||||||
|
preference.setText(userName);
|
||||||
|
preference.setOnPreferenceChangeListener((preference1, value) -> {
|
||||||
|
Utils.runOnBackgroundThread(() -> {
|
||||||
|
String newUserName = (String) value;
|
||||||
|
String errorMessage = SBRequester.setUsername(newUserName);
|
||||||
|
Utils.runOnMainThread(() -> {
|
||||||
|
if (errorMessage == null) {
|
||||||
|
preference.setTitle(fromHtml(str("revanced_sb_stats_username", newUserName)));
|
||||||
|
preference.setText(newUserName);
|
||||||
|
Utils.showToastLong(str("revanced_sb_stats_username_changed"));
|
||||||
|
} else {
|
||||||
|
preference.setText(userName); // revert to previous
|
||||||
|
SponsorBlockUtils.showErrorDialog(errorMessage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
addPreference(preference);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Number of segment submissions (does not include ignored segments).
|
||||||
|
Preference preference = new Preference(context);
|
||||||
|
String formatted = SponsorBlockUtils.getNumberOfSkipsString(stats.segmentCount);
|
||||||
|
preference.setTitle(fromHtml(str("revanced_sb_stats_submissions", formatted)));
|
||||||
|
preference.setSummary(str("revanced_sb_stats_submissions_sum"));
|
||||||
|
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
||||||
|
preference.setSelectable(false);
|
||||||
|
} else {
|
||||||
|
preference.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
|
i.setData(Uri.parse("https://sb.ltn.fi/userid/" + stats.publicUserId));
|
||||||
|
preference1.getContext().startActivity(i);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addPreference(preference);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// "user reputation". Usually not useful since it appears most users have zero reputation.
|
||||||
|
// But if there is a reputation then show it here.
|
||||||
|
Preference preference = new Preference(context);
|
||||||
|
preference.setTitle(fromHtml(str("revanced_sb_stats_reputation", stats.reputation)));
|
||||||
|
preference.setSelectable(false);
|
||||||
|
if (stats.reputation != 0) {
|
||||||
|
addPreference(preference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Time saved for other users.
|
||||||
|
Preference preference = new Preference(context);
|
||||||
|
|
||||||
|
String stats_saved;
|
||||||
|
String stats_saved_sum;
|
||||||
|
if (stats.totalSegmentCountIncludingIgnored == 0) {
|
||||||
|
stats_saved = str("revanced_sb_stats_saved_zero");
|
||||||
|
stats_saved_sum = str("revanced_sb_stats_saved_sum_zero");
|
||||||
|
} else {
|
||||||
|
stats_saved = str("revanced_sb_stats_saved",
|
||||||
|
SponsorBlockUtils.getNumberOfSkipsString(stats.viewCount));
|
||||||
|
stats_saved_sum = str("revanced_sb_stats_saved_sum",
|
||||||
|
SponsorBlockUtils.getTimeSavedString((long) (60 * stats.minutesSaved)));
|
||||||
|
}
|
||||||
|
preference.setTitle(fromHtml(stats_saved));
|
||||||
|
preference.setSummary(fromHtml(stats_saved_sum));
|
||||||
|
preference.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
Intent i = new Intent(Intent.ACTION_VIEW);
|
||||||
|
i.setData(Uri.parse("https://sponsor.ajay.app/stats/"));
|
||||||
|
preference1.getContext().startActivity(i);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
addPreference(preference);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "addUserStats failure", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addLocalUserStats() {
|
||||||
|
// Time the user saved by using SB.
|
||||||
|
Preference preference = new Preference(getContext());
|
||||||
|
Runnable updateStatsSelfSaved = () -> {
|
||||||
|
String formatted = SponsorBlockUtils.getNumberOfSkipsString(
|
||||||
|
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get());
|
||||||
|
preference.setTitle(fromHtml(str("revanced_sb_stats_self_saved", formatted)));
|
||||||
|
|
||||||
|
String formattedSaved = SponsorBlockUtils.getTimeSavedString(
|
||||||
|
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / 1000);
|
||||||
|
preference.setSummary(fromHtml(str("revanced_sb_stats_self_saved_sum", formattedSaved)));
|
||||||
|
};
|
||||||
|
updateStatsSelfSaved.run();
|
||||||
|
|
||||||
|
preference.setOnPreferenceClickListener(preference1 -> {
|
||||||
|
new AlertDialog.Builder(preference1.getContext())
|
||||||
|
.setTitle(str("revanced_sb_stats_self_saved_reset_title"))
|
||||||
|
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||||
|
Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.resetToDefault();
|
||||||
|
Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.resetToDefault();
|
||||||
|
updateStatsSelfSaved.run();
|
||||||
|
})
|
||||||
|
.setNegativeButton(android.R.string.no, null).show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
addPreference(preference);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
package app.revanced.extension.youtube.swipecontrols
|
package app.revanced.extension.youtube.swipecontrols
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import app.revanced.extension.shared.Logger
|
import app.revanced.extension.shared.Logger
|
||||||
import app.revanced.extension.shared.StringRef.str
|
import app.revanced.extension.shared.StringRef.str
|
||||||
import app.revanced.extension.shared.Utils
|
import app.revanced.extension.shared.Utils
|
||||||
|
import app.revanced.extension.shared.settings.StringSetting
|
||||||
import app.revanced.extension.youtube.settings.Settings
|
import app.revanced.extension.youtube.settings.Settings
|
||||||
import app.revanced.extension.youtube.shared.PlayerType
|
import app.revanced.extension.youtube.shared.PlayerType
|
||||||
|
|
||||||
@@ -51,105 +51,112 @@ class SwipeControlsConfigurationProvider {
|
|||||||
/**
|
/**
|
||||||
* Indicates whether press-to-swipe mode is enabled, requiring a press before swiping to activate controls.
|
* Indicates whether press-to-swipe mode is enabled, requiring a press before swiping to activate controls.
|
||||||
*/
|
*/
|
||||||
val shouldEnablePressToSwipe: Boolean
|
val shouldEnablePressToSwipe = Settings.SWIPE_PRESS_TO_ENGAGE.get()
|
||||||
get() = Settings.SWIPE_PRESS_TO_ENGAGE.get()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The threshold for detecting swipe gestures, in pixels.
|
* The threshold for detecting swipe gestures, in pixels.
|
||||||
* Loaded once to ensure consistent behavior during rapid scroll events.
|
* Loaded once to ensure consistent behavior during rapid scroll events.
|
||||||
*/
|
*/
|
||||||
val swipeMagnitudeThreshold: Int
|
val swipeMagnitudeThreshold = Settings.SWIPE_MAGNITUDE_THRESHOLD.get()
|
||||||
get() = Settings.SWIPE_MAGNITUDE_THRESHOLD.get()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The sensitivity of volume swipe gestures, determining how much volume changes per swipe.
|
* The sensitivity of volume swipe gestures, determining how much volume changes per swipe.
|
||||||
* Resets to default if set to 0, as it would disable swiping.
|
* Resets to default if set to 0, as it would disable swiping.
|
||||||
*/
|
*/
|
||||||
val volumeSwipeSensitivity: Int
|
val volumeSwipeSensitivity: Int by lazy {
|
||||||
get() {
|
val sensitivity = Settings.SWIPE_VOLUME_SENSITIVITY.get()
|
||||||
val sensitivity = Settings.SWIPE_VOLUME_SENSITIVITY.get()
|
|
||||||
|
|
||||||
if (sensitivity < 1) {
|
if (sensitivity < 1) {
|
||||||
return Settings.SWIPE_VOLUME_SENSITIVITY.resetToDefault()
|
return@lazy Settings.SWIPE_VOLUME_SENSITIVITY.resetToDefault()
|
||||||
}
|
|
||||||
|
|
||||||
return sensitivity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sensitivity
|
||||||
|
}
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region overlay adjustments
|
//region overlay adjustments
|
||||||
/**
|
/**
|
||||||
* Indicates whether haptic feedback should be enabled for swipe control interactions.
|
* Indicates whether haptic feedback should be enabled for swipe control interactions.
|
||||||
*/
|
*/
|
||||||
val shouldEnableHapticFeedback: Boolean
|
val shouldEnableHapticFeedback = Settings.SWIPE_HAPTIC_FEEDBACK.get()
|
||||||
get() = Settings.SWIPE_HAPTIC_FEEDBACK.get()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The duration in milliseconds that the overlay should remain visible after a change.
|
* The duration in milliseconds that the overlay should remain visible after a change.
|
||||||
*/
|
*/
|
||||||
val overlayShowTimeoutMillis: Long
|
val overlayShowTimeoutMillis = Settings.SWIPE_OVERLAY_TIMEOUT.get()
|
||||||
get() = Settings.SWIPE_OVERLAY_TIMEOUT.get()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The background opacity of the overlay, converted from a percentage (0-100) to an alpha value (0-255).
|
* The background opacity of the overlay, converted from a percentage (0-100) to an alpha value (0-255).
|
||||||
* Resets to default and shows a toast if the value is out of range.
|
* Resets to default and shows a toast if the value is out of range.
|
||||||
*/
|
*/
|
||||||
val overlayBackgroundOpacity: Int
|
val overlayBackgroundOpacity: Int by lazy {
|
||||||
get() {
|
var opacity = Settings.SWIPE_OVERLAY_OPACITY.get()
|
||||||
var opacity = Settings.SWIPE_OVERLAY_OPACITY.get()
|
|
||||||
|
|
||||||
if (opacity < 0 || opacity > 100) {
|
if (opacity < 0 || opacity > 100) {
|
||||||
Utils.showToastLong(str("revanced_swipe_overlay_background_opacity_invalid_toast"))
|
Utils.showToastLong(str("revanced_swipe_overlay_background_opacity_invalid_toast"))
|
||||||
opacity = Settings.SWIPE_OVERLAY_OPACITY.resetToDefault()
|
opacity = Settings.SWIPE_OVERLAY_OPACITY.resetToDefault()
|
||||||
}
|
|
||||||
|
|
||||||
opacity = opacity * 255 / 100
|
|
||||||
return Color.argb(opacity, 0, 0, 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
opacity = opacity * 255 / 100
|
||||||
|
Color.argb(opacity, 0, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color of the progress bar in the overlay.
|
* The color of the progress bar in the overlay for brightness.
|
||||||
* Resets to default and shows a toast if the color string is invalid or empty.
|
* Resets to default and shows a toast if the color string is invalid or empty.
|
||||||
*/
|
*/
|
||||||
val overlayProgressColor: Int
|
val overlayBrightnessProgressColor: Int by lazy {
|
||||||
get() {
|
// Use lazy to avoid repeat parsing. Changing color requires app restart.
|
||||||
try {
|
getSettingColor(Settings.SWIPE_OVERLAY_BRIGHTNESS_COLOR)
|
||||||
@SuppressLint("UseKtx")
|
}
|
||||||
val color = Color.parseColor(Settings.SWIPE_OVERLAY_PROGRESS_COLOR.get())
|
|
||||||
return (0xBF000000.toInt() or (color and 0xFFFFFF))
|
/**
|
||||||
} catch (ex: IllegalArgumentException) {
|
* The color of the progress bar in the overlay for volume.
|
||||||
Logger.printDebug({ "Could not parse color" }, ex)
|
* Resets to default and shows a toast if the color string is invalid or empty.
|
||||||
Utils.showToastLong(str("revanced_swipe_overlay_progress_color_invalid_toast"))
|
*/
|
||||||
Settings.SWIPE_OVERLAY_PROGRESS_COLOR.resetToDefault()
|
val overlayVolumeProgressColor: Int by lazy {
|
||||||
return overlayProgressColor // Recursively return.
|
getSettingColor(Settings.SWIPE_OVERLAY_VOLUME_COLOR)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getSettingColor(setting: StringSetting): Int {
|
||||||
|
try {
|
||||||
|
//noinspection UseKtx
|
||||||
|
val color = Color.parseColor(setting.get())
|
||||||
|
return (0xBF000000.toInt() or (color and 0x00FFFFFF))
|
||||||
|
} catch (ex: IllegalArgumentException) {
|
||||||
|
// This code should never be reached.
|
||||||
|
// Color picker rejects and will not save bad colors to a setting.
|
||||||
|
// If a user imports bad data, the color picker preference resets the
|
||||||
|
// bad color before this method can be called.
|
||||||
|
Logger.printDebug({ "Could not parse color: $setting" }, ex)
|
||||||
|
Utils.showToastLong(str("revanced_settings_color_invalid"))
|
||||||
|
setting.resetToDefault()
|
||||||
|
return getSettingColor(setting) // Recursively return.
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The background color used for the filled portion of the progress bar in the overlay.
|
* The background color used for the filled portion of the progress bar in the overlay.
|
||||||
*/
|
*/
|
||||||
val overlayFillBackgroundPaint: Int
|
val overlayFillBackgroundPaint = 0x80D3D3D3.toInt()
|
||||||
get() = 0x80D3D3D3.toInt()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The color used for text and icons in the overlay.
|
* The color used for text and icons in the overlay.
|
||||||
*/
|
*/
|
||||||
val overlayTextColor: Int
|
val overlayTextColor = Color.WHITE
|
||||||
get() = Color.WHITE
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The text size in the overlay, in density-independent pixels (dp).
|
* The text size in the overlay, in density-independent pixels (dp).
|
||||||
* Must be between 1 and 30 dp; resets to default and shows a toast if invalid.
|
* Must be between 1 and 30 dp; resets to default and shows a toast if invalid.
|
||||||
*/
|
*/
|
||||||
val overlayTextSize: Int
|
val overlayTextSize: Int by lazy {
|
||||||
get() {
|
val size = Settings.SWIPE_OVERLAY_TEXT_SIZE.get()
|
||||||
val size = Settings.SWIPE_OVERLAY_TEXT_SIZE.get()
|
if (size < 1 || size > 30) {
|
||||||
if (size < 1 || size > 30) {
|
Utils.showToastLong(str("revanced_swipe_text_overlay_size_invalid_toast"))
|
||||||
Utils.showToastLong(str("revanced_swipe_text_overlay_size_invalid_toast"))
|
return@lazy Settings.SWIPE_OVERLAY_TEXT_SIZE.resetToDefault()
|
||||||
return Settings.SWIPE_OVERLAY_TEXT_SIZE.resetToDefault()
|
|
||||||
}
|
|
||||||
return size
|
|
||||||
}
|
}
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines the style of the swipe controls overlay, determining its layout and appearance.
|
* Defines the style of the swipe controls overlay, determining its layout and appearance.
|
||||||
@@ -199,28 +206,25 @@ class SwipeControlsConfigurationProvider {
|
|||||||
/**
|
/**
|
||||||
* A minimal vertical progress bar.
|
* A minimal vertical progress bar.
|
||||||
*/
|
*/
|
||||||
VERTICAL_MINIMAL(isMinimal = true, isVertical = true)
|
VERTICAL_MINIMAL(isMinimal = true, isVertical = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current style of the overlay, determining its layout and appearance.
|
* The current style of the overlay, determining its layout and appearance.
|
||||||
*/
|
*/
|
||||||
val overlayStyle: SwipeOverlayStyle
|
val overlayStyle = Settings.SWIPE_OVERLAY_STYLE.get()
|
||||||
get() = Settings.SWIPE_OVERLAY_STYLE.get()
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region behaviour
|
//region behaviour
|
||||||
/**
|
/**
|
||||||
* Indicates whether the brightness level should be saved and restored when entering or exiting fullscreen mode.
|
* Indicates whether the brightness level should be saved and restored when entering or exiting fullscreen mode.
|
||||||
*/
|
*/
|
||||||
val shouldSaveAndRestoreBrightness: Boolean
|
val shouldSaveAndRestoreBrightness = Settings.SWIPE_SAVE_AND_RESTORE_BRIGHTNESS.get()
|
||||||
get() = Settings.SWIPE_SAVE_AND_RESTORE_BRIGHTNESS.get()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates whether auto-brightness should be enabled when the brightness gesture reaches its lowest value.
|
* Indicates whether auto-brightness should be enabled when the brightness gesture reaches its lowest value.
|
||||||
*/
|
*/
|
||||||
val shouldLowestValueEnableAutoBrightness: Boolean
|
val shouldLowestValueEnableAutoBrightness = Settings.SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS.get()
|
||||||
get() = Settings.SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS.get()
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The saved brightness value for the swipe gesture, used to restore brightness in fullscreen mode.
|
* The saved brightness value for the swipe gesture, used to restore brightness in fullscreen mode.
|
||||||
@@ -229,4 +233,4 @@ class SwipeControlsConfigurationProvider {
|
|||||||
get() = Settings.SWIPE_BRIGHTNESS_VALUE.get()
|
get() = Settings.SWIPE_BRIGHTNESS_VALUE.get()
|
||||||
set(value) = Settings.SWIPE_BRIGHTNESS_VALUE.save(value)
|
set(value) = Settings.SWIPE_BRIGHTNESS_VALUE.save(value)
|
||||||
//endregion
|
//endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class SwipeControlsOverlayLayout(
|
|||||||
|
|
||||||
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider())
|
constructor(context: Context) : this(context, SwipeControlsConfigurationProvider())
|
||||||
|
|
||||||
// Drawable icons for brightness and volume
|
// Drawable icons for brightness and volume.
|
||||||
private val autoBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_auto")
|
private val autoBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_auto")
|
||||||
private val lowBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_low")
|
private val lowBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_low")
|
||||||
private val mediumBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_medium")
|
private val mediumBrightnessIcon: Drawable = getDrawable("revanced_ic_sc_brightness_medium")
|
||||||
@@ -50,7 +50,7 @@ class SwipeControlsOverlayLayout(
|
|||||||
private val normalVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_normal")
|
private val normalVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_normal")
|
||||||
private val fullVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_high")
|
private val fullVolumeIcon: Drawable = getDrawable("revanced_ic_sc_volume_high")
|
||||||
|
|
||||||
// Function to retrieve drawable resources by name
|
// Function to retrieve drawable resources by name.
|
||||||
private fun getDrawable(name: String): Drawable {
|
private fun getDrawable(name: String): Drawable {
|
||||||
val drawable = resources.getDrawable(
|
val drawable = resources.getDrawable(
|
||||||
Utils.getResourceIdentifier(context, name, "drawable"),
|
Utils.getResourceIdentifier(context, name, "drawable"),
|
||||||
@@ -60,19 +60,19 @@ class SwipeControlsOverlayLayout(
|
|||||||
return drawable
|
return drawable
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize progress bars
|
// Initialize progress bars.
|
||||||
private val circularProgressView: CircularProgressView
|
private val circularProgressView: CircularProgressView
|
||||||
private val horizontalProgressView: HorizontalProgressView
|
private val horizontalProgressView: HorizontalProgressView
|
||||||
private val verticalBrightnessProgressView: VerticalProgressView
|
private val verticalBrightnessProgressView: VerticalProgressView
|
||||||
private val verticalVolumeProgressView: VerticalProgressView
|
private val verticalVolumeProgressView: VerticalProgressView
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// Initialize circular progress bar
|
// Initialize circular progress bar.
|
||||||
circularProgressView = CircularProgressView(
|
circularProgressView = CircularProgressView(
|
||||||
context,
|
context,
|
||||||
config.overlayBackgroundOpacity,
|
config.overlayBackgroundOpacity,
|
||||||
config.overlayStyle.isMinimal,
|
config.overlayStyle.isMinimal,
|
||||||
config.overlayProgressColor,
|
config.overlayBrightnessProgressColor, // Placeholder, updated in showFeedbackView.
|
||||||
config.overlayFillBackgroundPaint,
|
config.overlayFillBackgroundPaint,
|
||||||
config.overlayTextColor,
|
config.overlayTextColor,
|
||||||
config.overlayTextSize
|
config.overlayTextSize
|
||||||
@@ -80,18 +80,18 @@ class SwipeControlsOverlayLayout(
|
|||||||
layoutParams = LayoutParams(100f.toDisplayPixels().toInt(), 100f.toDisplayPixels().toInt()).apply {
|
layoutParams = LayoutParams(100f.toDisplayPixels().toInt(), 100f.toDisplayPixels().toInt()).apply {
|
||||||
addRule(CENTER_IN_PARENT, TRUE)
|
addRule(CENTER_IN_PARENT, TRUE)
|
||||||
}
|
}
|
||||||
visibility = GONE // Initially hidden
|
visibility = GONE // Initially hidden.
|
||||||
}
|
}
|
||||||
addView(circularProgressView)
|
addView(circularProgressView)
|
||||||
|
|
||||||
// Initialize horizontal progress bar
|
// Initialize horizontal progress bar.
|
||||||
val screenWidth = resources.displayMetrics.widthPixels
|
val screenWidth = resources.displayMetrics.widthPixels
|
||||||
val layoutWidth = (screenWidth * 4 / 5).toInt() // Cap at ~360dp
|
val layoutWidth = (screenWidth * 4 / 5).toInt() // Cap at ~360dp.
|
||||||
horizontalProgressView = HorizontalProgressView(
|
horizontalProgressView = HorizontalProgressView(
|
||||||
context,
|
context,
|
||||||
config.overlayBackgroundOpacity,
|
config.overlayBackgroundOpacity,
|
||||||
config.overlayStyle.isMinimal,
|
config.overlayStyle.isMinimal,
|
||||||
config.overlayProgressColor,
|
config.overlayBrightnessProgressColor, // Placeholder, updated in showFeedbackView.
|
||||||
config.overlayFillBackgroundPaint,
|
config.overlayFillBackgroundPaint,
|
||||||
config.overlayTextColor,
|
config.overlayTextColor,
|
||||||
config.overlayTextSize
|
config.overlayTextSize
|
||||||
@@ -104,16 +104,16 @@ class SwipeControlsOverlayLayout(
|
|||||||
topMargin = 20f.toDisplayPixels().toInt()
|
topMargin = 20f.toDisplayPixels().toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
visibility = GONE // Initially hidden
|
visibility = GONE // Initially hidden.
|
||||||
}
|
}
|
||||||
addView(horizontalProgressView)
|
addView(horizontalProgressView)
|
||||||
|
|
||||||
// Initialize vertical progress bar for brightness (right side)
|
// Initialize vertical progress bar for brightness (right side).
|
||||||
verticalBrightnessProgressView = VerticalProgressView(
|
verticalBrightnessProgressView = VerticalProgressView(
|
||||||
context,
|
context,
|
||||||
config.overlayBackgroundOpacity,
|
config.overlayBackgroundOpacity,
|
||||||
config.overlayStyle.isMinimal,
|
config.overlayStyle.isMinimal,
|
||||||
config.overlayProgressColor,
|
config.overlayBrightnessProgressColor,
|
||||||
config.overlayFillBackgroundPaint,
|
config.overlayFillBackgroundPaint,
|
||||||
config.overlayTextColor,
|
config.overlayTextColor,
|
||||||
config.overlayTextSize
|
config.overlayTextSize
|
||||||
@@ -123,16 +123,16 @@ class SwipeControlsOverlayLayout(
|
|||||||
rightMargin = 40f.toDisplayPixels().toInt()
|
rightMargin = 40f.toDisplayPixels().toInt()
|
||||||
addRule(CENTER_VERTICAL)
|
addRule(CENTER_VERTICAL)
|
||||||
}
|
}
|
||||||
visibility = GONE // Initially hidden
|
visibility = GONE // Initially hidden.
|
||||||
}
|
}
|
||||||
addView(verticalBrightnessProgressView)
|
addView(verticalBrightnessProgressView)
|
||||||
|
|
||||||
// Initialize vertical progress bar for volume (left side)
|
// Initialize vertical progress bar for volume (left side).
|
||||||
verticalVolumeProgressView = VerticalProgressView(
|
verticalVolumeProgressView = VerticalProgressView(
|
||||||
context,
|
context,
|
||||||
config.overlayBackgroundOpacity,
|
config.overlayBackgroundOpacity,
|
||||||
config.overlayStyle.isMinimal,
|
config.overlayStyle.isMinimal,
|
||||||
config.overlayProgressColor,
|
config.overlayVolumeProgressColor,
|
||||||
config.overlayFillBackgroundPaint,
|
config.overlayFillBackgroundPaint,
|
||||||
config.overlayTextColor,
|
config.overlayTextColor,
|
||||||
config.overlayTextSize
|
config.overlayTextSize
|
||||||
@@ -142,12 +142,12 @@ class SwipeControlsOverlayLayout(
|
|||||||
leftMargin = 40f.toDisplayPixels().toInt()
|
leftMargin = 40f.toDisplayPixels().toInt()
|
||||||
addRule(CENTER_VERTICAL)
|
addRule(CENTER_VERTICAL)
|
||||||
}
|
}
|
||||||
visibility = GONE // Initially hidden
|
visibility = GONE // Initially hidden.
|
||||||
}
|
}
|
||||||
addView(verticalVolumeProgressView)
|
addView(verticalVolumeProgressView)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler and callback for hiding progress bars
|
// Handler and callback for hiding progress bars.
|
||||||
private val feedbackHideHandler = Handler(Looper.getMainLooper())
|
private val feedbackHideHandler = Handler(Looper.getMainLooper())
|
||||||
private val feedbackHideCallback = Runnable {
|
private val feedbackHideCallback = Runnable {
|
||||||
circularProgressView.visibility = GONE
|
circularProgressView.visibility = GONE
|
||||||
@@ -165,29 +165,42 @@ class SwipeControlsOverlayLayout(
|
|||||||
|
|
||||||
val viewToShow = when {
|
val viewToShow = when {
|
||||||
config.overlayStyle.isCircular -> circularProgressView
|
config.overlayStyle.isCircular -> circularProgressView
|
||||||
config.overlayStyle.isVertical -> if (isBrightness) verticalBrightnessProgressView else verticalVolumeProgressView
|
config.overlayStyle.isVertical ->
|
||||||
|
if (isBrightness)
|
||||||
|
verticalBrightnessProgressView
|
||||||
|
else
|
||||||
|
verticalVolumeProgressView
|
||||||
else -> horizontalProgressView
|
else -> horizontalProgressView
|
||||||
}
|
}
|
||||||
viewToShow.apply {
|
viewToShow.apply {
|
||||||
|
// Set the appropriate progress color.
|
||||||
|
if (this is CircularProgressView || this is HorizontalProgressView) {
|
||||||
|
setProgressColor(
|
||||||
|
if (isBrightness)
|
||||||
|
config.overlayBrightnessProgressColor
|
||||||
|
else
|
||||||
|
config.overlayVolumeProgressColor
|
||||||
|
)
|
||||||
|
}
|
||||||
setProgress(progress, max, value, isBrightness)
|
setProgress(progress, max, value, isBrightness)
|
||||||
this.icon = icon
|
this.icon = icon
|
||||||
visibility = VISIBLE
|
visibility = VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle volume change
|
// Handle volume change.
|
||||||
override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
|
override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) {
|
||||||
val volumePercentage = (newVolume.toFloat() / maximumVolume) * 100
|
val volumePercentage = (newVolume.toFloat() / maximumVolume) * 100
|
||||||
val icon = when {
|
val icon = when {
|
||||||
newVolume == 0 -> mutedVolumeIcon
|
newVolume == 0 -> mutedVolumeIcon
|
||||||
volumePercentage < 33 -> lowVolumeIcon
|
volumePercentage < 25 -> lowVolumeIcon
|
||||||
volumePercentage < 66 -> normalVolumeIcon
|
volumePercentage < 50 -> normalVolumeIcon
|
||||||
else -> fullVolumeIcon
|
else -> fullVolumeIcon
|
||||||
}
|
}
|
||||||
showFeedbackView("$newVolume", newVolume, maximumVolume, icon, isBrightness = false)
|
showFeedbackView("$newVolume", newVolume, maximumVolume, icon, isBrightness = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle brightness change
|
// Handle brightness change.
|
||||||
override fun onBrightnessChanged(brightness: Double) {
|
override fun onBrightnessChanged(brightness: Double) {
|
||||||
if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) {
|
if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) {
|
||||||
val displayText = if (config.overlayStyle.isVertical) "А"
|
val displayText = if (config.overlayStyle.isVertical) "А"
|
||||||
@@ -195,18 +208,19 @@ class SwipeControlsOverlayLayout(
|
|||||||
showFeedbackView(displayText, 0, 100, autoBrightnessIcon, isBrightness = true)
|
showFeedbackView(displayText, 0, 100, autoBrightnessIcon, isBrightness = true)
|
||||||
} else {
|
} else {
|
||||||
val brightnessValue = round(brightness).toInt()
|
val brightnessValue = round(brightness).toInt()
|
||||||
|
val clampedProgress = max(0, brightnessValue)
|
||||||
val icon = when {
|
val icon = when {
|
||||||
brightnessValue < 25 -> lowBrightnessIcon
|
clampedProgress < 25 -> lowBrightnessIcon
|
||||||
brightnessValue < 50 -> mediumBrightnessIcon
|
clampedProgress < 50 -> mediumBrightnessIcon
|
||||||
brightnessValue < 75 -> highBrightnessIcon
|
clampedProgress < 75 -> highBrightnessIcon
|
||||||
else -> fullBrightnessIcon
|
else -> fullBrightnessIcon
|
||||||
}
|
}
|
||||||
val displayText = if (config.overlayStyle.isVertical) "$brightnessValue" else "$brightnessValue%"
|
val displayText = if (config.overlayStyle.isVertical) "$clampedProgress" else "$clampedProgress%"
|
||||||
showFeedbackView(displayText, brightnessValue, 100, icon, isBrightness = true)
|
showFeedbackView(displayText, clampedProgress, 100, icon, isBrightness = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Begin swipe session
|
// Begin swipe session.
|
||||||
override fun onEnterSwipeSession() {
|
override fun onEnterSwipeSession() {
|
||||||
if (config.shouldEnableHapticFeedback) {
|
if (config.shouldEnableHapticFeedback) {
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@@ -233,25 +247,41 @@ abstract class AbstractProgressView(
|
|||||||
defStyleAttr: Int = 0
|
defStyleAttr: Int = 0
|
||||||
) : View(context, attrs, defStyleAttr) {
|
) : View(context, attrs, defStyleAttr) {
|
||||||
|
|
||||||
// Combined paint creation function for both fill and stroke styles
|
// Combined paint creation function for both fill and stroke styles.
|
||||||
private fun createPaint(color: Int, style: Paint.Style = Paint.Style.FILL, strokeCap: Paint.Cap = Paint.Cap.BUTT, strokeWidth: Float = 0f) = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
private fun createPaint(
|
||||||
|
color: Int,
|
||||||
|
style: Paint.Style = Paint.Style.FILL,
|
||||||
|
strokeCap: Paint.Cap = Paint.Cap.BUTT,
|
||||||
|
strokeWidth: Float = 0f
|
||||||
|
) = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
this.style = style
|
this.style = style
|
||||||
this.color = color
|
this.color = color
|
||||||
this.strokeCap = strokeCap
|
this.strokeCap = strokeCap
|
||||||
this.strokeWidth = strokeWidth
|
this.strokeWidth = strokeWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize paints
|
// Initialize paints.
|
||||||
val backgroundPaint = createPaint(overlayBackgroundOpacity, style = Paint.Style.FILL)
|
val backgroundPaint = createPaint(
|
||||||
val progressPaint = createPaint(overlayProgressColor, style = Paint.Style.STROKE, strokeCap = Paint.Cap.ROUND, strokeWidth = 6f.toDisplayPixels())
|
overlayBackgroundOpacity,
|
||||||
val fillBackgroundPaint = createPaint(overlayFillBackgroundPaint, style = Paint.Style.FILL)
|
style = Paint.Style.FILL
|
||||||
val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
)
|
||||||
color = overlayTextColor
|
val progressPaint = createPaint(
|
||||||
|
overlayProgressColor,
|
||||||
|
style = Paint.Style.STROKE,
|
||||||
|
strokeCap = Paint.Cap.ROUND,
|
||||||
|
strokeWidth = 6f.toDisplayPixels()
|
||||||
|
)
|
||||||
|
val fillBackgroundPaint = createPaint(
|
||||||
|
overlayFillBackgroundPaint,
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
)
|
||||||
|
val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = overlayTextColor
|
||||||
textAlign = Paint.Align.CENTER
|
textAlign = Paint.Align.CENTER
|
||||||
textSize = overlayTextSize.toFloat().toDisplayPixels()
|
textSize = overlayTextSize.toFloat().toDisplayPixels()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rect for text measurement
|
// Rect for text measurement.
|
||||||
protected val textBounds = Rect()
|
protected val textBounds = Rect()
|
||||||
|
|
||||||
protected var progress = 0
|
protected var progress = 0
|
||||||
@@ -268,13 +298,18 @@ abstract class AbstractProgressView(
|
|||||||
invalidate()
|
invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setProgressColor(color: Int) {
|
||||||
|
progressPaint.color = color
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
protected fun measureTextWidth(text: String, paint: Paint): Int {
|
protected fun measureTextWidth(text: String, paint: Paint): Int {
|
||||||
paint.getTextBounds(text, 0, text.length, textBounds)
|
paint.getTextBounds(text, 0, text.length, textBounds)
|
||||||
return textBounds.width()
|
return textBounds.width()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas) {
|
override fun onDraw(canvas: Canvas) {
|
||||||
// Base class implementation can be empty
|
// Base class implementation can be empty.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,8 +428,8 @@ class HorizontalProgressView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate required width based on content
|
* Calculate required width based on content.
|
||||||
* @return Required width to display all elements
|
* @return Required width to display all elements.
|
||||||
*/
|
*/
|
||||||
private fun calculateRequiredWidth(): Float {
|
private fun calculateRequiredWidth(): Float {
|
||||||
textWidth = measureTextWidth(displayText, textPaint).toFloat()
|
textWidth = measureTextWidth(displayText, textPaint).toFloat()
|
||||||
@@ -537,8 +572,8 @@ class VerticalProgressView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate required height based on content
|
* Calculate required height based on content.
|
||||||
* @return Required height to display all elements
|
* @return Required height to display all elements.
|
||||||
*/
|
*/
|
||||||
private fun calculateRequiredHeight(): Float {
|
private fun calculateRequiredHeight(): Float {
|
||||||
return if (!isMinimalStyle) {
|
return if (!isMinimalStyle) {
|
||||||
@@ -633,4 +668,4 @@ class VerticalProgressView(
|
|||||||
super.setProgress(value, max, text, isBrightnessMode)
|
super.setProgress(value, max, text, isBrightnessMode)
|
||||||
requestLayout()
|
requestLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ import android.view.View;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import app.revanced.extension.shared.Logger;
|
import app.revanced.extension.shared.Logger;
|
||||||
|
import app.revanced.extension.youtube.patches.VideoInformation;
|
||||||
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
|
||||||
import app.revanced.extension.youtube.settings.Settings;
|
import app.revanced.extension.youtube.settings.Settings;
|
||||||
|
|
||||||
|
import static app.revanced.extension.shared.StringRef.str;
|
||||||
|
import static app.revanced.extension.shared.Utils.showToastShort;
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
public class PlaybackSpeedDialogButton {
|
public class PlaybackSpeedDialogButton {
|
||||||
@Nullable
|
@Nullable
|
||||||
@@ -23,8 +27,27 @@ public class PlaybackSpeedDialogButton {
|
|||||||
"revanced_playback_speed_dialog_button",
|
"revanced_playback_speed_dialog_button",
|
||||||
"revanced_playback_speed_dialog_button_placeholder",
|
"revanced_playback_speed_dialog_button_placeholder",
|
||||||
Settings.PLAYBACK_SPEED_DIALOG_BUTTON::get,
|
Settings.PLAYBACK_SPEED_DIALOG_BUTTON::get,
|
||||||
view -> CustomPlaybackSpeedPatch.showOldPlaybackSpeedMenu(),
|
view -> {
|
||||||
null
|
try {
|
||||||
|
CustomPlaybackSpeedPatch.showModernCustomPlaybackSpeedDialog(view.getContext());
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "speed button onClick failure", ex);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
view -> {
|
||||||
|
try {
|
||||||
|
final float speed = (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get() ||
|
||||||
|
VideoInformation.getPlaybackSpeed() == Settings.PLAYBACK_SPEED_DEFAULT.get())
|
||||||
|
? 1.0f
|
||||||
|
: Settings.PLAYBACK_SPEED_DEFAULT.get();
|
||||||
|
|
||||||
|
VideoInformation.overridePlaybackSpeed(speed);
|
||||||
|
showToastShort(str("revanced_custom_playback_speeds_reset_toast", (speed + "x")));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Logger.printException(() -> "speed button reset failure", ex);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
);
|
);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
Logger.printException(() -> "initializeButton failure", ex);
|
Logger.printException(() -> "initializeButton failure", ex);
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ org.gradle.jvmargs = -Xms512M -Xmx2048M
|
|||||||
org.gradle.parallel = true
|
org.gradle.parallel = true
|
||||||
android.useAndroidX = true
|
android.useAndroidX = true
|
||||||
kotlin.code.style = official
|
kotlin.code.style = official
|
||||||
version = 5.23.0-dev.6
|
version = 5.26.0-dev.4
|
||||||
|
|||||||
58
package-lock.json
generated
58
package-lock.json
generated
@@ -9,7 +9,7 @@
|
|||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"gradle-semantic-release-plugin": "^1.10.1",
|
"gradle-semantic-release-plugin": "^1.10.1",
|
||||||
"semantic-release": "^24.2.1"
|
"semantic-release": "^24.2.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -1964,9 +1964,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chalk": {
|
"node_modules/chalk": {
|
||||||
"version": "5.3.0",
|
"version": "5.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
|
||||||
"integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
|
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -3460,9 +3460,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "12.0.2",
|
"version": "15.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz",
|
||||||
"integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==",
|
"integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -3473,24 +3473,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/marked-terminal": {
|
"node_modules/marked-terminal": {
|
||||||
"version": "7.1.0",
|
"version": "7.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz",
|
||||||
"integrity": "sha512-+pvwa14KZL74MVXjYdPR3nSInhGhNvPce/3mqLVZT2oUvt654sL1XImFuLZ1pkA866IYZ3ikDTOFUIC7XzpZZg==",
|
"integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-escapes": "^7.0.0",
|
"ansi-escapes": "^7.0.0",
|
||||||
"chalk": "^5.3.0",
|
"ansi-regex": "^6.1.0",
|
||||||
|
"chalk": "^5.4.1",
|
||||||
"cli-highlight": "^2.1.11",
|
"cli-highlight": "^2.1.11",
|
||||||
"cli-table3": "^0.6.5",
|
"cli-table3": "^0.6.5",
|
||||||
"node-emoji": "^2.1.3",
|
"node-emoji": "^2.2.0",
|
||||||
"supports-hyperlinks": "^3.0.0"
|
"supports-hyperlinks": "^3.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"marked": ">=1 <14"
|
"marked": ">=1 <16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/marked-terminal/node_modules/ansi-regex": {
|
||||||
|
"version": "6.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||||
|
"integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/meow": {
|
"node_modules/meow": {
|
||||||
@@ -3607,9 +3621,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-emoji": {
|
"node_modules/node-emoji": {
|
||||||
"version": "2.1.3",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz",
|
||||||
"integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==",
|
"integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -6760,9 +6774,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/semantic-release": {
|
"node_modules/semantic-release": {
|
||||||
"version": "24.2.1",
|
"version": "24.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.5.tgz",
|
||||||
"integrity": "sha512-z0/3cutKNkLQ4Oy0HTi3lubnjTsdjjgOqmxdPjeYWe6lhFqUPfwslZxRHv3HDZlN4MhnZitb9SLihDkZNxOXfQ==",
|
"integrity": "sha512-9xV49HNY8C0/WmPWxTlaNleiXhWb//qfMzG2c5X8/k7tuWcu8RssbuS+sujb/h7PiWSXv53mrQvV9hrO9b7vuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -6784,8 +6798,8 @@
|
|||||||
"hosted-git-info": "^8.0.0",
|
"hosted-git-info": "^8.0.0",
|
||||||
"import-from-esm": "^2.0.0",
|
"import-from-esm": "^2.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"marked": "^12.0.0",
|
"marked": "^15.0.0",
|
||||||
"marked-terminal": "^7.0.0",
|
"marked-terminal": "^7.3.0",
|
||||||
"micromatch": "^4.0.2",
|
"micromatch": "^4.0.2",
|
||||||
"p-each-series": "^3.0.0",
|
"p-each-series": "^3.0.0",
|
||||||
"p-reduce": "^3.0.0",
|
"p-reduce": "^3.0.0",
|
||||||
|
|||||||
@@ -4,6 +4,6 @@
|
|||||||
"@semantic-release/changelog": "^6.0.3",
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
"@semantic-release/git": "^10.0.1",
|
"@semantic-release/git": "^10.0.1",
|
||||||
"gradle-semantic-release-plugin": "^1.10.1",
|
"gradle-semantic-release-plugin": "^1.10.1",
|
||||||
"semantic-release": "^24.2.1"
|
"semantic-release": "^24.2.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -240,6 +240,10 @@ public final class app/revanced/patches/instagram/ads/HideAdsPatchKt {
|
|||||||
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/instagram/misc/signature/SignatureCheckPatchKt {
|
||||||
|
public static final fun getSignatureCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/irplus/ad/RemoveAdsPatchKt {
|
public final class app/revanced/patches/irplus/ad/RemoveAdsPatchKt {
|
||||||
public static final fun getRemoveAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getRemoveAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
@@ -280,10 +284,22 @@ public final class app/revanced/patches/messenger/inputfield/DisableTypingIndica
|
|||||||
public static final fun getDisableTypingIndicatorPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getDisableTypingIndicatorPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/messenger/metaai/RemoveMetaAIPatchKt {
|
||||||
|
public static final fun getRemoveMetaAIPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/messenger/misc/extension/ExtensionPatchKt {
|
||||||
|
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/messenger/navbar/RemoveMetaAITabPatchKt {
|
public final class app/revanced/patches/messenger/navbar/RemoveMetaAITabPatchKt {
|
||||||
public static final fun getRemoveMetaAITabPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getRemoveMetaAITabPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/meta/ads/HideAdsPatchKt {
|
||||||
|
public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/mifitness/misc/locale/ForceEnglishLocalePatchKt {
|
public final class app/revanced/patches/mifitness/misc/locale/ForceEnglishLocalePatchKt {
|
||||||
public static final fun getForceEnglishLocalePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getForceEnglishLocalePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
@@ -428,6 +444,14 @@ public final class app/revanced/patches/primevideo/misc/extension/ExtensionPatch
|
|||||||
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/primevideo/misc/permissions/RenamePermissionsPatchKt {
|
||||||
|
public static final fun getRenamePermissionsPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/protonmail/account/RemoveFreeAccountsLimitPatchKt {
|
||||||
|
public static final fun getRemoveFreeAccountsLimitPatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/protonmail/signature/RemoveSentFromSignaturePatchKt {
|
public final class app/revanced/patches/protonmail/signature/RemoveSentFromSignaturePatchKt {
|
||||||
public static final fun getRemoveSentFromSignaturePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
public static final fun getRemoveSentFromSignaturePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
}
|
}
|
||||||
@@ -569,6 +593,7 @@ public final class app/revanced/patches/reddit/layout/disablescreenshotpopup/Dis
|
|||||||
|
|
||||||
public final class app/revanced/patches/reddit/layout/premiumicon/UnlockPremiumIconPatchKt {
|
public final class app/revanced/patches/reddit/layout/premiumicon/UnlockPremiumIconPatchKt {
|
||||||
public static final fun getUnlockPremiumIconPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getUnlockPremiumIconPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
public static final fun getUnlockPremiumIconsPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/reddit/misc/extension/ExtensionPatchKt {
|
public final class app/revanced/patches/reddit/misc/extension/ExtensionPatchKt {
|
||||||
@@ -648,6 +673,10 @@ public final class app/revanced/patches/shared/misc/mapping/ResourceMappingPatch
|
|||||||
public static final fun getResourceMappings ()Ljava/util/List;
|
public static final fun getResourceMappings ()Ljava/util/List;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/shared/misc/pairip/license/DisableLicenseCheckPatchKt {
|
||||||
|
public static final fun getDisableLicenseCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/shared/misc/settings/SettingsPatchKt {
|
public final class app/revanced/patches/shared/misc/settings/SettingsPatchKt {
|
||||||
public static final fun settingsPatch (Ljava/util/List;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch;
|
public static final fun settingsPatch (Ljava/util/List;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
public static final fun settingsPatch (Lkotlin/Pair;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch;
|
public static final fun settingsPatch (Lkotlin/Pair;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
@@ -844,6 +873,10 @@ public final class app/revanced/patches/soundcloud/offlinesync/EnableOfflineSync
|
|||||||
public static final fun getEnableOfflineSync ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getEnableOfflineSync ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/spotify/layout/hide/createbutton/HideCreateButtonPatchKt {
|
||||||
|
public static final fun getHideCreateButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/spotify/layout/theme/CustomThemePatchKt {
|
public final class app/revanced/patches/spotify/layout/theme/CustomThemePatchKt {
|
||||||
public static final fun getCustomThemePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
public static final fun getCustomThemePatch ()Lapp/revanced/patcher/patch/ResourcePatch;
|
||||||
}
|
}
|
||||||
@@ -868,10 +901,18 @@ public final class app/revanced/patches/spotify/misc/fix/SpoofSignaturePatchKt {
|
|||||||
public static final fun getSpoofSignaturePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getSpoofSignaturePatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/spotify/misc/fix/login/FixFacebookLoginPatchKt {
|
||||||
|
public static final fun getFixFacebookLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt {
|
public final class app/revanced/patches/spotify/misc/privacy/SanitizeSharingLinksPatchKt {
|
||||||
public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/spotify/misc/widgets/FixThirdPartyLaunchersWidgetsKt {
|
||||||
|
public static final fun getFixThirdPartyLaunchersWidgets ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabPatchKt {
|
public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabPatchKt {
|
||||||
public static final fun getPremiumNavbarTabPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getPremiumNavbarTabPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
@@ -1216,6 +1257,10 @@ public final class app/revanced/patches/youtube/layout/hide/player/flyoutmenupan
|
|||||||
public static final fun getHidePlayerFlyoutMenuPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getHidePlayerFlyoutMenuPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/youtube/layout/hide/relatedvideooverlay/HideRelatedVideoOverlayPatchKt {
|
||||||
|
public static final fun getHideRelatedVideoOverlayPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatchKt {
|
public final class app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatchKt {
|
||||||
public static final fun getDisableRollingNumberAnimationPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getDisableRollingNumberAnimationPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
@@ -1371,6 +1416,10 @@ public final class app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatchKt {
|
|||||||
public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public final class app/revanced/patches/youtube/misc/hapticfeedback/DisableHapticFeedbackPatchKt {
|
||||||
|
public static final fun getDisableHapticFeedbackPatch ()Lapp/revanced/patcher/patch/BytecodePatch;
|
||||||
|
}
|
||||||
|
|
||||||
public final class app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHookKt {
|
public final class app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHookKt {
|
||||||
public static final fun addImageUrlErrorCallbackHook (Ljava/lang/String;)V
|
public static final fun addImageUrlErrorCallbackHook (Ljava/lang/String;)V
|
||||||
public static final fun addImageUrlHook (Ljava/lang/String;Z)V
|
public static final fun addImageUrlHook (Ljava/lang/String;Z)V
|
||||||
@@ -1460,8 +1509,10 @@ public final class app/revanced/patches/youtube/misc/settings/PreferenceScreen :
|
|||||||
public final fun getGENERAL_LAYOUT ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getGENERAL_LAYOUT ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getMISC ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getMISC ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getPLAYER ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getPLAYER ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
|
public final fun getRETURN_YOUTUBE_DISLIKE ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getSEEKBAR ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getSEEKBAR ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getSHORTS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getSHORTS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
|
public final fun getSPONSORBLOCK ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getSWIPE_CONTROLS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getSWIPE_CONTROLS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
public final fun getVIDEO ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
public final fun getVIDEO ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;
|
||||||
}
|
}
|
||||||
@@ -1601,8 +1652,23 @@ public final class app/revanced/util/BytecodeUtilsKt {
|
|||||||
public static final fun indexOfFirstResourceIdOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I
|
public static final fun indexOfFirstResourceIdOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I
|
||||||
public static final fun injectHideViewCall (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;IILjava/lang/String;Ljava/lang/String;)V
|
public static final fun injectHideViewCall (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;IILjava/lang/String;Ljava/lang/String;)V
|
||||||
public static final fun literal (Lapp/revanced/patcher/FingerprintBuilder;Lkotlin/jvm/functions/Function0;)V
|
public static final fun literal (Lapp/revanced/patcher/FingerprintBuilder;Lkotlin/jvm/functions/Function0;)V
|
||||||
|
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;B)V
|
||||||
|
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;C)V
|
||||||
|
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;D)V
|
||||||
|
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;F)V
|
||||||
|
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
|
||||||
|
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;J)V
|
||||||
|
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V
|
||||||
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V
|
public static final fun returnEarly (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V
|
||||||
public static synthetic fun returnEarly$default (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ZILjava/lang/Object;)V
|
public static synthetic fun returnEarly$default (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ZILjava/lang/Object;)V
|
||||||
|
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;B)V
|
||||||
|
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;C)V
|
||||||
|
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;D)V
|
||||||
|
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;F)V
|
||||||
|
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V
|
||||||
|
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;J)V
|
||||||
|
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;S)V
|
||||||
|
public static final fun returnLate (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Z)V
|
||||||
public static final fun transformMethods (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)V
|
public static final fun transformMethods (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)V
|
||||||
public static final fun traverseClassHierarchy (Lapp/revanced/patcher/patch/BytecodePatchContext;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)V
|
public static final fun traverseClassHierarchy (Lapp/revanced/patcher/patch/BytecodePatchContext;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)V
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ val hideAdbStatusPatch = bytecodePatch(
|
|||||||
.takeIf { it.opcode == Opcode.INVOKE_STATIC }
|
.takeIf { it.opcode == Opcode.INVOKE_STATIC }
|
||||||
?.getReference<MethodReference>()
|
?.getReference<MethodReference>()
|
||||||
?.takeIf {
|
?.takeIf {
|
||||||
it.anyMethodSignatureMatches(it,
|
it.anyMethodSignatureMatches(
|
||||||
SETTINGS_GLOBAL_GET_INT_OR_THROW_METHOD_REFERENCE,
|
SETTINGS_GLOBAL_GET_INT_OR_THROW_METHOD_REFERENCE,
|
||||||
SETTINGS_GLOBAL_GET_INT_OR_DEFAULT_METHOD_REFERENCE
|
SETTINGS_GLOBAL_GET_INT_OR_DEFAULT_METHOD_REFERENCE
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import app.revanced.patcher.patch.stringOption
|
|||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val spoofBuildInfoPatch = bytecodePatch(
|
val spoofBuildInfoPatch = bytecodePatch(
|
||||||
name = "Spoof build info",
|
name = "Spoof build info",
|
||||||
description = "Spoof the information about the current build.",
|
description = "Spoofs the information about the current build.",
|
||||||
use = false,
|
use = false,
|
||||||
) {
|
) {
|
||||||
val board by stringOption(
|
val board by stringOption(
|
||||||
@@ -141,14 +141,14 @@ val spoofBuildInfoPatch = bytecodePatch(
|
|||||||
val socManufacturer by stringOption(
|
val socManufacturer by stringOption(
|
||||||
key = "soc-manufacturer",
|
key = "soc-manufacturer",
|
||||||
default = null,
|
default = null,
|
||||||
title = "SOC Manufacturer",
|
title = "SOC manufacturer",
|
||||||
description = "The manufacturer of the device's primary system-on-chip.",
|
description = "The manufacturer of the device's primary system-on-chip.",
|
||||||
)
|
)
|
||||||
|
|
||||||
val socModel by stringOption(
|
val socModel by stringOption(
|
||||||
key = "soc-model",
|
key = "soc-model",
|
||||||
default = null,
|
default = null,
|
||||||
title = "SOC Model",
|
title = "SOC model",
|
||||||
description = "The model name of the device's primary system-on-chip.",
|
description = "The model name of the device's primary system-on-chip.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -36,12 +36,12 @@ val spoofSimCountryPatch = bytecodePatch(
|
|||||||
|
|
||||||
val networkCountryIso by isoCountryPatchOption(
|
val networkCountryIso by isoCountryPatchOption(
|
||||||
"networkCountryIso",
|
"networkCountryIso",
|
||||||
"Network ISO Country Code",
|
"Network ISO country code",
|
||||||
)
|
)
|
||||||
|
|
||||||
val simCountryIso by isoCountryPatchOption(
|
val simCountryIso by isoCountryPatchOption(
|
||||||
"simCountryIso",
|
"simCountryIso",
|
||||||
"Sim ISO Country Code",
|
"SIM ISO country code",
|
||||||
)
|
)
|
||||||
|
|
||||||
dependsOn(
|
dependsOn(
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ val removeShareTargetsPatch = resourcePatch(
|
|||||||
try {
|
try {
|
||||||
document("res/xml/shortcuts.xml")
|
document("res/xml/shortcuts.xml")
|
||||||
} catch (_: FileNotFoundException) {
|
} catch (_: FileNotFoundException) {
|
||||||
return@execute Logger.getLogger(this::class.java.name).warning("The app has no shortcuts")
|
return@execute Logger.getLogger(this::class.java.name).warning(
|
||||||
|
"The app has no shortcuts. No changes applied.")
|
||||||
}.use { document ->
|
}.use { document ->
|
||||||
val rootNode = document.getNode("shortcuts") as? Element ?: return@use
|
val rootNode = document.getNode("shortcuts") as? Element ?: return@use
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
package app.revanced.patches.angulus.ads
|
package app.revanced.patches.angulus.ads
|
||||||
|
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.patches.shared.misc.pairip.license.disableLicenseCheckPatch
|
||||||
import app.revanced.util.returnEarly
|
import app.revanced.util.returnEarly
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val angulusPatch = bytecodePatch(name = "Hide ads") {
|
val angulusPatch = bytecodePatch(name = "Hide ads") {
|
||||||
compatibleWith("com.drinkplusplus.angulus")
|
compatibleWith("com.drinkplusplus.angulus")
|
||||||
|
|
||||||
|
dependsOn(disableLicenseCheckPatch)
|
||||||
|
|
||||||
execute {
|
execute {
|
||||||
// Always return 0 as the daily measurement count.
|
// Always return 0 as the daily measurement count.
|
||||||
getDailyMeasurementCountFingerprint.method.returnEarly()
|
getDailyMeasurementCountFingerprint.method.returnEarly(0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,9 @@
|
|||||||
package app.revanced.patches.instagram.ads
|
package app.revanced.patches.instagram.ads
|
||||||
|
|
||||||
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
|
|
||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
|
||||||
|
@Deprecated("Patch was moved to different package: app.revanced.patches.meta.ads.hideAdsPatch")
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val hideAdsPatch = bytecodePatch(
|
val hideAdsPatch = bytecodePatch {
|
||||||
name = "Hide ads",
|
dependsOn(app.revanced.patches.meta.ads.hideAdsPatch)
|
||||||
description = "Hides ads in stories, discover, profile, etc. " +
|
|
||||||
"An ad can still appear once when refreshing the home feed.",
|
|
||||||
) {
|
|
||||||
compatibleWith("com.instagram.android")
|
|
||||||
|
|
||||||
execute {
|
|
||||||
adInjectorFingerprint.method.addInstructions(
|
|
||||||
0,
|
|
||||||
"""
|
|
||||||
const/4 v0, 0x0
|
|
||||||
return v0
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package app.revanced.patches.instagram.misc.signature
|
||||||
|
|
||||||
|
import app.revanced.patcher.fingerprint
|
||||||
|
import app.revanced.util.getReference
|
||||||
|
import app.revanced.util.indexOfFirstInstruction
|
||||||
|
import com.android.tools.smali.dexlib2.iface.reference.MethodReference
|
||||||
|
|
||||||
|
internal val isValidSignatureClassFingerprint = fingerprint {
|
||||||
|
strings("The provider for uri '", "' is not trusted: ")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val isValidSignatureMethodFingerprint = fingerprint {
|
||||||
|
parameters("L", "Z")
|
||||||
|
returns("Z")
|
||||||
|
custom { method, _ ->
|
||||||
|
method.indexOfFirstInstruction {
|
||||||
|
getReference<MethodReference>()?.name == "keySet"
|
||||||
|
} >= 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package app.revanced.patches.instagram.misc.signature
|
||||||
|
|
||||||
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
|
import app.revanced.util.returnEarly
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
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"))
|
||||||
|
|
||||||
|
execute {
|
||||||
|
isValidSignatureMethodFingerprint
|
||||||
|
.match(isValidSignatureClassFingerprint.classDef)
|
||||||
|
.method
|
||||||
|
.returnEarly(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import app.revanced.patcher.patch.bytecodePatch
|
|||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val unlockPremiumPatch = bytecodePatch(
|
val unlockPremiumPatch = bytecodePatch(
|
||||||
name = "Unlock premium",
|
name = "Unlock Premium",
|
||||||
) {
|
) {
|
||||||
compatibleWith("com.adobe.lrmobile"("10.0.2"))
|
compatibleWith("com.adobe.lrmobile"("10.0.2"))
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,14 @@ import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction
|
|||||||
import app.revanced.patcher.patch.bytecodePatch
|
import app.revanced.patcher.patch.bytecodePatch
|
||||||
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This patch will be deleted soon.
|
||||||
|
*
|
||||||
|
* Pull requests to update this patch to the latest app target are invited.
|
||||||
|
*/
|
||||||
|
@Deprecated("This patch only works with an outdated app target that is no longer fully supported by Facebook.")
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
val disableSwitchingEmojiToStickerPatch = bytecodePatch(
|
val disableSwitchingEmojiToStickerPatch = bytecodePatch(
|
||||||
name = "Disable switching emoji to sticker",
|
|
||||||
description = "Disables switching from emoji to sticker search mode in message input field.",
|
description = "Disables switching from emoji to sticker search mode in message input field.",
|
||||||
) {
|
) {
|
||||||
compatibleWith("com.facebook.orca"("439.0.0.29.119"))
|
compatibleWith("com.facebook.orca"("439.0.0.29.119"))
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package app.revanced.patches.messenger.metaai
|
||||||
|
|
||||||
|
import com.android.tools.smali.dexlib2.Opcode
|
||||||
|
import app.revanced.patcher.fingerprint
|
||||||
|
|
||||||
|
internal val getMobileConfigBoolFingerprint = fingerprint {
|
||||||
|
parameters("J")
|
||||||
|
returns("Z")
|
||||||
|
opcodes(Opcode.RETURN)
|
||||||
|
custom { method, classDef ->
|
||||||
|
method.implementation ?: return@custom false // unsure if this is necessary
|
||||||
|
classDef.interfaces.contains("Lcom/facebook/mobileconfig/factory/MobileConfigUnsafeContext;")
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user